From 60e6bdd3a0a34f9ae0bc87126b2f33d54e15a7b3 Mon Sep 17 00:00:00 2001 From: Lukas Vogel Date: Mon, 28 Sep 2026 12:33:00 +0200 Subject: [PATCH] document materialized views --- content/compatibility/sql_features.md | 1 + content/compatibility/system_table.md | 2 +- content/references/objects/_index.md | 3 +- .../references/objects/materialized_views.md | 272 ++++++++++++++++++ content/references/objects/views.md | 1 + 5 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 content/references/objects/materialized_views.md diff --git a/content/compatibility/sql_features.md b/content/compatibility/sql_features.md index 4d3bfc8c..6f7171c9 100644 --- a/content/compatibility/sql_features.md +++ b/content/compatibility/sql_features.md @@ -111,6 +111,7 @@ the [system table compatibility](../system_table) page. | Table Partitioning | Partial | Only hash partitioning. Only at CREATE TABLE time | | Foreign Data Wrappers | No | | | Views | Yes | [Documentation](/docs/references/objects/views/) | +| Materialized Views | Yes | [Documentation](/docs/references/objects/materialized_views/) | | Databases | Yes | [Documentation](/docs/references/objects/databases/) | | Functions & Procedures | Yes | [Documentation](/docs/references/objects/functions/)
Also in cedar_script language | | Custom Types | No | | diff --git a/content/compatibility/system_table.md b/content/compatibility/system_table.md index fd8a56ce..7d9625e5 100644 --- a/content/compatibility/system_table.md +++ b/content/compatibility/system_table.md @@ -160,7 +160,7 @@ The views instead use more human-readable symbolic names. | [pg_ident_file_mappings](https://www.postgresql.org/docs/current/view-pg-ident-file-mappings.html) | 🟡 | Summarizes client user name mapping configuration. | | [pg_indexes](https://www.postgresql.org/docs/current/view-pg-indexes.html) | 🟢 | Shows information about indexes. | | [pg_locks](https://www.postgresql.org/docs/current/view-pg-locks.html) | 🟡 | Displays locks currently held or awaited. | -| [pg_matviews](https://www.postgresql.org/docs/current/view-pg-matviews.html) | 🟡 | Lists materialized views. | +| [pg_matviews](https://www.postgresql.org/docs/current/view-pg-matviews.html) | 🟢 | Lists materialized views. | | [pg_policies](https://www.postgresql.org/docs/current/view-pg-policies.html) | 🟡 | Displays information about policies. | | [pg_prepared_statements](https://www.postgresql.org/docs/current/view-pg-prepared-statements.html) | 🟡 | Lists prepared statements. | | [pg_prepared_xacts](https://www.postgresql.org/docs/current/view-pg-prepared-xacts.html) | 🔴 | Shows prepared transactions. | diff --git a/content/references/objects/_index.md b/content/references/objects/_index.md index a96203e8..b99e815c 100644 --- a/content/references/objects/_index.md +++ b/content/references/objects/_index.md @@ -10,8 +10,9 @@ Each page covers all operations on that object type: creating, altering, and dro * [Databases](./databases) — `CREATE`, `ALTER`, `DROP DATABASE` * [Functions](./functions) — `CREATE`, `ALTER`, `DROP FUNCTION`/`PROCEDURE`, `DO`, `CALL` * [Indexes](./indexes) — `CREATE`, `DROP INDEX`, index types, partial and expression indexes +* [Materialized Views](./materialized_views) — `CREATE`, `ALTER`, `DROP MATERIALIZED VIEW`, `REFRESH` * [Policies](./policies) — `CREATE`, `ALTER`, `DROP POLICY`, row level security * [Roles](./roles) — `CREATE`, `ALTER`, `DROP ROLE`, `GRANT`, `REVOKE` * [Schemas](./schemas) — `CREATE`, `ALTER`, `DROP SCHEMA`, `search_path` * [Tables](./tables) — `CREATE`, `ALTER`, `DROP TABLE`, constraints, partitioning -* [Views](./views) — `CREATE`, `DROP VIEW`, materialized views, `REFRESH` +* [Views](./views) — `CREATE`, `DROP VIEW` diff --git a/content/references/objects/materialized_views.md b/content/references/objects/materialized_views.md new file mode 100644 index 00000000..4846f640 --- /dev/null +++ b/content/references/objects/materialized_views.md @@ -0,0 +1,272 @@ +--- +title: "Reference: Materialized Views" +linkTitle: "Materialized Views" +weight: 35 +--- + +A materialized view stores the result of a query as a table. +Unlike a regular [view](/docs/references/objects/views), CedarDB does not rerun the query when you read from a materialized view. +Instead, it returns the rows that it computed when the view was created or last refreshed. +Changes to the underlying tables become visible only after you run `REFRESH MATERIALIZED VIEW`. + +```sql +CREATE TABLE trees ( + id integer PRIMARY KEY, + species text NOT NULL, + height_m numeric NOT NULL +); +INSERT INTO trees VALUES (1, 'Oak', 12.4), (2, 'Oak', 20.1), (3, 'Birch', 9.0); + +CREATE MATERIALIZED VIEW species_stats AS + SELECT species, count(*) AS tree_count, avg(height_m) AS avg_height_m + FROM trees + GROUP BY species; + +SELECT * FROM species_stats ORDER BY species; +``` + +```text + species | tree_count | avg_height_m +---------+------------+-------------- + Birch | 1 | 9.000000 + Oak | 2 | 16.250000 +``` + +## Using Materialized Views + +Materialized views are useful for expensive queries whose results you read much more often than the underlying data changes. +Typical examples are aggregations for dashboards and reports, or pre-joined data for an application. + +You can read from a materialized view like from any other table, join it with other tables and views, and create indexes on it. +You cannot change its contents with `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, or `COPY FROM`. +The only way to change the contents is `REFRESH MATERIALIZED VIEW`. + +After creating a materialized view, you can find it in the `pg_matviews` system view. +In `pg_class`, materialized views have the `relkind` value `m`. + +## CREATE MATERIALIZED VIEW + +`CREATE MATERIALIZED VIEW` defines a new materialized view and, by default, populates it with the result of the query. + +```text +CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] [ ( [, ...] ) ] + [ WITH ( = [, ...] ) ] + AS + [ WITH [ NO ] DATA ] +``` + +| Parameter | Description | +|-----------------------------|-------------------------------------------------------------------------------------------------------------| +| `IF NOT EXISTS` | Do not throw an error if a relation with the same name already exists. CedarDB does not run the query then. | +| `` | The name of the materialized view, optionally schema-qualified. | +| `` | Names for the columns of the view. Columns without a name in the list keep the name from the query. | +| `WITH ()` | Storage options, as for [`CREATE TABLE`](/docs/references/objects/tables/#options), e.g., `server`. | +| `` | A `SELECT`, `VALUES`, or `TABLE` query that defines the contents. | +| `WITH DATA` | Run the query and populate the view. This is the default. | +| `WITH NO DATA` | Create the view without running the query. The view is unpopulated until you refresh it. | + +Rename the output columns: + +```sql +CREATE TABLE trees (id integer, species text, height_m numeric); + +CREATE MATERIALIZED VIEW tallest (tree_species, max_height_m) AS + SELECT species, max(height_m) FROM trees GROUP BY species; +``` + +Store the contents of a materialized view on remote storage, such as S3, instead of local disk. +This works for materialized views just like for [tables](/docs/references/objects/tables/#options), and requires a server previously created with +[CREATE SERVER](/docs/references/advanced/createserver): + +```sql +CREATE TABLE trees (id integer, species text, height_m numeric); + +CREATE MATERIALIZED VIEW tree_heights WITH (server = remote_storage) AS + SELECT id, height_m FROM trees; +``` + +A materialized view can read from tables, views, and other materialized views. +It cannot read from temporary tables, neither directly nor through a view, because a materialized view outlives the session of a temporary table. + +### Unpopulated materialized views + +With `WITH NO DATA`, CedarDB only checks the query for errors and derives the columns, but does not run it. +Reading from an unpopulated materialized view is an error: + +```sql +CREATE TABLE trees (id integer, species text); +INSERT INTO trees VALUES (1, 'Oak'); + +CREATE MATERIALIZED VIEW oaks AS + SELECT id FROM trees WHERE species = 'Oak' + WITH NO DATA; + +SELECT * FROM oaks; +-- ERROR: materialized view "oaks" has not been populated + +REFRESH MATERIALIZED VIEW oaks; +SELECT * FROM oaks; +-- id +-- ---- +-- 1 +``` + +The `ispopulated` column of `pg_matviews` and the `relispopulated` column of `pg_class` show whether a materialized view is populated. + +### Indexes + +You can create regular and unique [indexes](/docs/references/objects/indexes) on a materialized view. +CedarDB maintains them on every refresh. +If a refresh produces rows that violate a unique index, the refresh fails and the view keeps its previous contents: + +```sql +CREATE TABLE trees (id integer, species text); +INSERT INTO trees VALUES (1, 'Oak'), (2, 'Birch'); + +CREATE MATERIALIZED VIEW tree_species AS SELECT id, species FROM trees; +CREATE UNIQUE INDEX ON tree_species (species); + +INSERT INTO trees VALUES (3, 'Oak'); +REFRESH MATERIALIZED VIEW tree_species; +-- ERROR: duplicate key value violates unique constraint "tree_species_species_key" +``` + +### Permissions + +To create a materialized view, you need the `CREATE` privilege on the target schema and the `SELECT` privilege on all relations that the query reads. +The creating role becomes the owner of the view. + +Other roles only need the `SELECT` privilege on the materialized view itself to read it. +They do not need any privileges on the underlying tables: + +```sql +GRANT SELECT ON species_stats TO dashboard_reader; +``` + +## REFRESH MATERIALIZED VIEW + +`REFRESH MATERIALIZED VIEW` replaces the contents of a materialized view with the current result of its query. + +```text +REFRESH MATERIALIZED VIEW [ CONCURRENTLY ] + [ WITH [ NO ] DATA ] +``` + +| Parameter | Description | +|----------------|--------------------------------------------------------------------------------------------------| +| `CONCURRENTLY` | Accepted for PostgreSQL compatibility. CedarDB never blocks readers during a refresh, see below. | +| `` | The name of the materialized view, optionally schema-qualified. | +| `WITH DATA` | Rerun the query and populate the view with its result. This is the default. | +| `WITH NO DATA` | Discard the contents of the view and mark it as unpopulated. | + +```sql +CREATE TABLE trees (id integer, species text); +INSERT INTO trees VALUES (1, 'Oak'); + +CREATE MATERIALIZED VIEW tree_count AS SELECT count(*) AS total FROM trees; + +INSERT INTO trees VALUES (2, 'Birch'); +SELECT total FROM tree_count; +-- 1 + +REFRESH MATERIALIZED VIEW tree_count; +SELECT total FROM tree_count; +-- 2 +``` + +A refresh is transactional like any other write. +Concurrent transactions keep reading the previous contents until the refresh commits. +If you roll back the transaction, the view keeps its previous contents. + +When a materialized view reads from another materialized view, refreshing the outer view uses the current contents of the inner one. +Refresh the views in dependency order, starting with the ones that only read from tables. + +CedarDB does not refresh materialized views automatically. +To keep a view up to date, run `REFRESH MATERIALIZED VIEW` periodically or after loading new data. + +### Permissions + +Only the owner of a materialized view can refresh it. +The owner needs the `SELECT` privilege on all relations that the query reads. + +## ALTER MATERIALIZED VIEW + +`ALTER MATERIALIZED VIEW` changes the properties of an existing materialized view. +Specify `IF EXISTS` to not throw an error if the materialized view does not exist. + +Rename a materialized view: + +```sql +ALTER MATERIALIZED VIEW species_stats RENAME TO species_summary; +``` + +Rename a column of a materialized view. +This only renames the column of the view, not the column of the underlying table: + +```sql +ALTER MATERIALIZED VIEW species_stats RENAME COLUMN tree_count TO number_of_trees; +``` + +Move a materialized view to another schema: + +```sql +ALTER MATERIALIZED VIEW species_stats SET SCHEMA reporting; +``` + +Change the owner of a materialized view: + +```sql +ALTER MATERIALIZED VIEW species_stats OWNER TO analyst; +``` + +`ALTER TABLE` can also rename a materialized view, move it to another schema, and change its owner. +It rejects changes that only apply to tables, such as adding columns or constraints, since the columns of a materialized view are defined by its query. + +### Permissions + +To alter a materialized view, you must be its owner. +Superusers can alter any materialized view. + +## DROP MATERIALIZED VIEW + +`DROP MATERIALIZED VIEW` removes a materialized view, its data, and its indexes. + +```text +DROP MATERIALIZED VIEW [ IF EXISTS ] [, ...] [ CASCADE | RESTRICT ] +``` + +```sql +DROP MATERIALIZED VIEW species_stats; +``` + +Do not throw an error if the materialized view does not exist: + +```sql +DROP MATERIALIZED VIEW IF EXISTS species_stats; +``` + +If other views or materialized views read from the materialized view, the drop fails. +Use `CASCADE` to drop the dependent objects as well: + +```sql +DROP MATERIALIZED VIEW species_stats CASCADE; +``` + +Use the right `DROP` statement for the object type: `DROP TABLE` and `DROP VIEW` do not drop materialized views. + +### Permissions + +To drop a materialized view, you must own it, own its schema, or be a superuser. + +## Dependencies + +A materialized view depends on all tables and views its query reads. +While the materialized view exists, CedarDB refuses to drop these relations or the columns the view reads, unless you use `CASCADE`. + +You also cannot rename these relations or move them to another schema while the materialized view exists. + +## PostgreSQL Differences + +- `REFRESH MATERIALIZED VIEW` never blocks readers, as CedarDB uses snapshot isolation. + `CONCURRENTLY` is accepted as a synonym for a regular refresh, does not require a unique index, and can be combined with `WITH NO DATA`. +- CedarDB refuses to rename or move a table, view, or materialized view to another schema while a materialized view reads from it. diff --git a/content/references/objects/views.md b/content/references/objects/views.md index e7bb4ed4..0f6e1d74 100644 --- a/content/references/objects/views.md +++ b/content/references/objects/views.md @@ -7,6 +7,7 @@ weight: 30 You can use create view to define a *virtual* table, specified by a query. When you reference the view, CedarDB reruns the query as if you would have specified the table as a subselect. Views are similar to common table expressions (CTEs), but they survive connection and server restarts. +To store the result of a query instead of rerunning it, use a [materialized view](/docs/references/objects/materialized_views). Usage example: