Save a query result as a view
Store a reusable result and refresh it when its source changes.
A materialized view saves a query result so your application can read it again.
- Prepare the source tablesCheck the fields used by the summary query.
- Define and install the viewSave a supported SELECT and its initial result.
- Read the stored summaryUse the saved result for the intended report.
- Refresh when freshness requires itRecompute the view after relevant source changes.
Decide whether to use one
Use a view when the same result is read repeatedly and you can control when it refreshes. Run a normal query when each request differs or every answer must reflect the latest write.
Prepare and test the query
Register the source table, add representative rows and run the SELECT successfully before installing the view. This example totals paid orders by customer.
Example source table
namespace = "shop"
table = "orders"
primary_key = ["id"]
[[columns]]
name = "id"
ty = "str"
required = true
[[columns]]
name = "customer"
ty = "str"
[[columns]]
name = "amount_cents"
ty = "i64" # money in minor units - never f64
[[columns]]
name = "status"
ty = "str"
[[columns]]
name = "notes"
ty = "str"
[[columns]]
name = "placed_ms"
ty = "u64" # epoch milliseconds
# Makes the WHERE status = 'paid' inside the view cheap to re-run.
[[indexes]]
name = "by_status"
columns = ["status"]SELECT customer,
COUNT(*) AS orders,
SUM(amount_cents) AS total_cents
FROM shop.orders
WHERE status = 'paid'
GROUP BY customerInstall the view
Choose a unique name and use refresh_mode: "on_demand". The install call computes the initial result. Keep the returned refresh_ts so your application can show when the result was last updated.
Connection used in these examples
These examples use the tenant-scoped engine API and its older SDK methods. Use the connection details supplied for that interface. For the current Connect setup, see authentication. Do not combine the two URL formats.
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/sql/materialized-views" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "mv_paid_by_customer",
"query": "SELECT customer, COUNT(*) AS orders, SUM(amount_cents) AS total_cents FROM shop.orders WHERE status = '\''paid'\'' GROUP BY customer",
"refresh_mode": "on_demand"
}'rows_materialized counts the saved result rows, not the source rows scanned.
Read the saved result
Read the view by name through its HTTP route. That response contains the complete stored result, without pagination.
SQL also supports SELECT * FROM view_name, selected column names, column aliases and an optional LIMIT. Use the unqualified view name. This reads the saved snapshot; it does not refresh an on-demand view.
A view is not a fully queryable table: WHERE, joins, grouped calculations, computed expressions, ORDER BY and OFFSET over the snapshot are refused. Put those operations in the view definition or query its source table. On multi-shard deployments, use the HTTP read route when the SQL connection is not on the view's home shard.
Read examples
curl "https://$OC_HOST/v1/tenants/$OC_TENANT/sql/materialized-views/mv_paid_by_customer" \
-H "Authorization: Bearer $OC_TOKEN"Refresh after source changes
An on-demand view does not update itself. Call refresh after your import or update job, or schedule it in your application. Refresh runs the full query again.
Refresh examples
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/sql/materialized-views/mv_paid_by_customer/refresh" \
-H "Authorization: Bearer $OC_TOKEN"Understand the two modes
| Mode | Behaviour |
|---|---|
on_demand | Default. Changes only when you call refresh. |
incremental | Preview. Updates during supported writes when the required server capability is enabled. |
Incremental preview requirements
Incremental support depends on both the query shape and server configuration.
- It supports eligible single-table filter/projection plans and eligible
COUNT,SUMorAVGaggregates. - Aggregate support has a separate gate that is off by default.
- Joins,
MIN,MAX,DISTINCT, subqueries, set operations,HAVING,ORDER BYandLIMITare excluded from this preview. - Eligible filters can use an index or a table scan. Eligibility depends on the supported query shape, not simply on whether a scan appears in the plan.
- Floating-point
SUMandAVGcan differ slightly from a full recomputation; refresh when exact reconciliation is needed.
422 means the required preview is disabled. 400 means the shape or setting is unsupported. Use on_demand in either case.
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/sql/materialized-views" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "mv_paid_live",
"query": "SELECT customer, SUM(amount_cents) AS total_cents FROM shop.orders WHERE status = '\''paid'\'' GROUP BY customer",
"refresh_mode": "incremental"
}'Limits to plan for
- List views with
GET /sql/materialized-viewsand remove one withDELETE /sql/materialized-views/:name, relative to the tenant-scoped API. There is no in-place definition edit; use a new name or deliberately drop and recreate the view. - SQL supports
CREATE MATERIALIZED VIEW,REFRESH MATERIALIZED VIEWandDROP MATERIALIZED VIEW. SQL creation chooses incremental maintenance only when the query and server gates allow it; otherwise it uses on-demand refresh.CREATE OR REPLACEis not supported. - Keep the stored result small. The HTTP read returns it whole, and SQL snapshot reads have a separate 4 MiB internal response cap; a
LIMITdoes not bypass that read cap. - A view cannot use another view as a source or span unsupported shards.
- Dropping a source table does not remove an on-demand snapshot; it may still serve the previous result.
- Use the explicit HTTP install call. Older SDK helpers may send obsolete refresh-mode names.
| Status | Check |
|---|---|
400 | Query support, refresh-mode spelling or snapshot size. |
404 | Exact view name. |
409 | The name is already installed; choose another. |
422 | Incremental preview is disabled for this kind of view. |
501 | Unsupported shard arrangement. |