404 Not Found

404 Not Found


nginx

PostgreSQL Views and Materialized Views

1. What You'll Learn


2. The Story

Alice is a data engineer at an e-commerce platform. The operations team reviews a sales summary report every day at 8:00, which joins 5 tables, aggregates 3 million order records, and takes 2 hours to query.

Her boss said: "Can the report come back instantly?"

Alice defined the query as a materialized view, refreshed automatically every day at 4:00 AM. Querying the precomputed report dropped from 2 hours to 3 seconds. But materialized-view data isn't real-time—that's the trade-off between regular views and materialized views.


3. Concept: Regular Views

(1) CREATE VIEW Syntax

SQL
CREATE [OR REPLACE] VIEW view_name [(column_aliases)] AS
  SELECT ...;

A view is a stored query definition—it stores no data. Each time you query a view, PostgreSQL expands (rewrites) the view definition into the underlying query and executes it.

Property Description
What's stored Query text only
Data freshness Real-time, always reflects the latest base-table data
Performance Same as executing the underlying query directly
Space used Almost none

▶ Example: Create a Sales Summary View

SQL
CREATE VIEW v_daily_sales AS
SELECT
  order_date,
  region,
  COUNT(*)               AS order_count,
  SUM(amount)            AS total_revenue,
  AVG(amount)            AS avg_order_value
FROM orders
GROUP BY order_date, region;

Output:

TEXT
 count 
-------
     5
(1 row)

▶ Example: Query the View

SQL
SELECT * FROM v_daily_sales
WHERE order_date >= '2025-01-01'
ORDER BY total_revenue DESC;

Output:

TEXT
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

Equivalent to writing the underlying SELECT directly—PostgreSQL expands it automatically.

(2) ALTER VIEW and DROP VIEW

Operation Syntax Description
Rename ALTER VIEW v RENAME TO v_new Rename the view
Set default column ALTER VIEW v ALTER COLUMN c SET DEFAULT d Change a column default
Set owner ALTER VIEW v OWNER TO role Change the owner
Drop DROP VIEW [IF EXISTS] v [CASCADE] CASCADE drops dependent views

▶ Example: Modify and Drop a View

SQL
ALTER VIEW v_daily_sales RENAME TO v_daily_summary;

DROP VIEW IF EXISTS v_daily_summary CASCADE;

Output:

TEXT
-- SQL statement executed successfully

(3) View Expansion Mechanism

100%
flowchart LR
    A["SELECT * FROM v_daily_sales"] --> B["Rewrite<br/>expand view definition"]
    B --> C["SELECT order_date, region, COUNT(*)...<br/>FROM orders<br/>GROUP BY ..."]
    C --> D[Optimizer]
    D --> E[Execute]
    style B fill:#fff9c4
    style D fill:#c8e6c9

4. Concept: Updatable Views

(1) Which Views Are Updatable

In PostgreSQL, simple views that meet all the following conditions are automatically updatable (support INSERT / UPDATE / DELETE):

Condition Description
FROM a single base table only No JOIN
No GROUP BY / HAVING No aggregation
No DISTINCT No de-duplication
No window functions No OVER
No set operations No UNION / INTERSECT / EXCEPT
SELECT columns are base-table columns No expressions/computed columns

▶ Example: Updatable View

SQL
CREATE VIEW v_active_users AS
SELECT user_id, name, email, status
FROM users
WHERE status = 'active';

Output:

TEXT
CREATE TABLE
SQL
UPDATE v_active_users SET name = 'Alice Wang' WHERE user_id = 1;

DELETE FROM v_active_users WHERE user_id = 99;

INSERT INTO v_active_users (user_id, name, email, status)
VALUES (101, 'Charlie', 'charlie@example.com', 'active');

(2) WITH CHECK OPTION

By default, rows updated/inserted through a view can "escape" the view's scope (e.g., change status to 'inactive', and that row no longer appears in the view). WITH CHECK OPTION forbids such escaping.

Option Behavior
No CHECK OPTION Escaping allowed; updated rows may no longer appear in the view
WITH CHECK OPTION Escaping forbidden; updated rows must still satisfy the view condition
WITH CASCADED CHECK OPTION Current view + dependent views checked (recursive)
WITH LOCAL CHECK OPTION Only the current view's condition checked

▶ Example: WITH CHECK OPTION Prevents Escaping

SQL
CREATE VIEW v_active_users_strict AS
SELECT user_id, name, email, status
FROM users
WHERE status = 'active'
WITH CHECK OPTION;
SQL
UPDATE v_active_users_strict SET status = 'inactive' WHERE user_id = 1;
TEXT
ERROR: new row violates check option for view "v_active_users_strict"
DETAIL: Failing row contains (1, ..., inactive).

▶ Example: Insert into Updatable View Then Query

SQL
INSERT INTO v_active_users (user_id, name, email, status)
VALUES (200, 'Bob', 'bob@example.com', 'active');

SELECT * FROM v_active_users WHERE user_id = 200;
TEXT
 user_id | name |       email        | status
---------+------+--------------------+--------
     200 | Bob  | bob@example.com    | active

5. Concept: Materialized Views

(1) MATERIALIZED VIEW Overview

A materialized view actually stores the query result on disk; queries read the precomputed data directly, without re-executing the underlying query. This is a PostgreSQL specialty.

Property Regular view Materialized view
What's stored Query text Query text + result data
Data freshness Real-time Updates only on refresh
Query performance Same as underlying query Extremely fast (reads precomputed)
Space used Almost none Same size as result set
Updatable Simple views can be No direct DML
Refresh method Not needed REFRESH MATERIALIZED VIEW

▶ Example: Create a Materialized View

SQL
CREATE MATERIALIZED VIEW mv_monthly_sales AS
SELECT
  DATE_TRUNC('month', order_date)::date AS month,
  region,
  COUNT(*)    AS order_count,
  SUM(amount) AS total_revenue,
  AVG(amount) AS avg_order_value
FROM orders
GROUP BY DATE_TRUNC('month', order_date), region
WITH DATA;

Output:

TEXT
 count 
-------
     5
(1 row)

▶ Example: Query the Materialized View

SQL
SELECT * FROM mv_monthly_sales
WHERE month >= '2025-01-01'
ORDER BY total_revenue DESC;

Output:

TEXT
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

Returns in milliseconds, because the data is already precomputed and stored.

(2) REFRESH

Syntax Behavior Lock Speed
REFRESH MATERIALIZED VIEW mv Full refresh, replaces all data Takes exclusive lock, blocks reads Slower
REFRESH MATERIALIZED VIEW CONCURRENTLY mv Incremental refresh (needs unique index) Doesn't block reads Faster

CONCURRENTLY is a PostgreSQL specialty: the view stays queryable during refresh—business isn't blocked.

▶ Example: Full Refresh

SQL
REFRESH MATERIALIZED VIEW mv_monthly_sales;

Output:

TEXT
-- SQL statement executed successfully

All SELECTs on this materialized view are blocked during the refresh.

▶ Example: CONCURRENTLY Incremental Refresh

SQL
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_monthly_sales;

Output:

TEXT
-- SQL statement executed successfully

Prerequisite: the materialized view must have at least one unique index.

SQL
CREATE UNIQUE INDEX idx_mv_monthly_sales_pk
  ON mv_monthly_sales (month, region);

REFRESH MATERIALIZED VIEW CONCURRENTLY mv_monthly_sales;

(3) WITH DATA vs WITH NO DATA

Option Behavior
WITH DATA Populate data immediately on creation (default)
WITH NO DATA Don't populate on creation; must REFRESH before first query

▶ Example: Deferred Population

SQL
CREATE MATERIALIZED VIEW mv_expensive_report AS
SELECT ... FROM ... WITH NO DATA;

REFRESH MATERIALIZED VIEW mv_expensive_report;

Output:

TEXT
CREATE TABLE

▶ Example: Automatic Scheduled Refresh (pg_cron extension)

SQL
SELECT cron.schedule(
  'refresh_monthly_sales',
  '0 4 * * *',
  $$REFRESH MATERIALIZED VIEW CONCURRENTLY mv_monthly_sales$$
);

Output:

TEXT
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

Refreshes automatically every day at 4:00 AM.


6. Views vs Materialized Views vs Temp Tables

Dimension Regular view Materialized view Temp table
Stores query definition Yes Yes No
Stores data No Yes Yes
Data freshness Real-time Manual refresh Manual maintenance
Query performance Depends on underlying query Extremely fast Fast
Cross-session persistence Yes Yes No (gone after session)
Supports indexes No (uses base-table indexes) Yes Yes
Supports DML Simple views updatable No Yes
Typical scenario Simplify queries Report precomputation Temporary intermediate results
100%
classDiagram
    class View {
        +stores query text
        +real-time data
        +simple view DML-able
        +WITH CHECK OPTION
    }
    class MaterializedView {
        +stores query text+data
        +REFRESH refresh
        +CONCURRENTLY
        +indexable
        +WITH DATA/NO DATA
    }
    class TempTable {
        +stores data only
        +gone after session
        +fully DML-able
        +indexable
    }
    View <|-- MaterializedView : extends
    MaterializedView ..|> TempTable : similar perf

7. View Management Best Practices

(1) Naming Conventions

Type Recommended prefix Example
Regular view v_ v_active_users
Materialized view mv_ mv_monthly_sales
Temp table tmp_ tmp_import_data

(2) View Dependencies and Security

Operation Risk Solution
Drop base table View becomes invalid DROP TABLE CASCADE auto-drops dependent views
Alter base-table column View may error Update definition with CREATE OR REPLACE VIEW
Privilege control View can limit column visibility GRANT SELECT ON view TO role

▶ Example: Column-Level Privileges with a View

SQL
CREATE VIEW v_user_public AS
SELECT user_id, name
FROM users;

GRANT SELECT ON v_user_public TO reporter_role;

REVOKE SELECT ON users FROM reporter_role;

Output:

TEXT
CREATE TABLE

reporter_role can only see user_id and name, not sensitive columns like email.


8. Comprehensive Example

Alice's report optimization—materialized-view precomputation + CONCURRENTLY refresh + view to simplify queries:

SQL
CREATE MATERIALIZED VIEW mv_sales_report AS
SELECT
  DATE_TRUNC('month', o.order_date)::date AS month,
  p.category,
  o.region,
  COUNT(*)                                AS order_count,
  COUNT(DISTINCT o.customer_id)           AS unique_customers,
  SUM(o.amount)                           AS total_revenue,
  SUM(o.amount) FILTER (WHERE o.amount >= 50000) AS big_deal_revenue,
  AVG(o.amount)                           AS avg_order_value,
  SUM(oi.quantity * oi.unit_price)        AS total_gmv
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
GROUP BY DATE_TRUNC('month', o.order_date), p.category, o.region
WITH DATA;

CREATE UNIQUE INDEX idx_mv_sales_report_pk
  ON mv_sales_report (month, category, region);

CREATE VIEW v_sales_dashboard AS
SELECT
  month,
  region,
  SUM(total_revenue)  AS region_revenue,
  SUM(order_count)    AS region_orders,
  SUM(unique_customers) AS region_customers
FROM mv_sales_report
GROUP BY month, region;

REFRESH MATERIALIZED VIEW CONCURRENTLY mv_sales_report;

9. Execution Flow

The creation and refresh flow of a materialized view:

100%
flowchart TD
    A[CREATE MATERIALIZED VIEW] --> B[Execute underlying query]
    B --> C[Write results to disk]
    C --> D[Can create indexes]
    D --> E[Query reads disk data directly]
    E --> F{Need refresh?}
    F -->|REFRESH| G[Re-execute underlying query]
    G --> H[Replace old data]
    H --> E
    F -->|CONCURRENTLY| I[Incremental compare refresh]
    I --> J[Doesn't block reads]
    J --> E

    style A fill:#e1f5fe
    style E fill:#c8e6c9
    style I fill:#fff9c4
Step Description
Create Execute underlying query, persist result to disk
Query Read disk data directly, no underlying query executed
Refresh Re-execute underlying query, replace old data
CONCURRENTLY Incremental refresh, doesn't block queries during refresh

❓ FAQ

Q Do regular views affect performance?
A No. A view is just query rewriting—performance is identical to writing the underlying SQL directly. A complex view adds no extra overhead.
Q Why can't I find inserted data after inserting through a view?
A The inserted row may not satisfy the view's WHERE condition. Use WITH CHECK OPTION to prevent this.
Q Why does CONCURRENTLY refresh need a unique index?
A CONCURRENTLY does an incremental refresh by comparing old and new data; the unique index identifies each row. Without it, you get an error.
Q Does a materialized view support direct UPDATE/DELETE?
A No. A materialized view is read-only; it can only be refreshed via REFRESH. To change it, refresh the base-table data then REFRESH again.
Q What's the difference between WITH CASCADED CHECK OPTION and WITH LOCAL CHECK OPTION?
A CASCADED checks the current view and all dependent views' conditions; LOCAL checks only the current view's condition. With nested views, CASCADED is safer.
Q Can a materialized view be built on another materialized view?
A Yes. PostgreSQL supports nested materialized views, but you must refresh them manually in dependency order.

📖 Summary


📝 Exercises

  1. ⭐ Create a view v_recent_orders that queries orders from the last 30 days, including customer_name and product_name.
  2. ⭐ Create an updatable view v_active_customers that shows only customers with status = 'active', and add WITH CHECK OPTION.
  3. ⭐⭐ Create a materialized view mv_daily_category_sales that summarizes sales by day and category, and add a unique index to support CONCURRENTLY refresh.
  4. ⭐⭐ Design a nested-view scheme: a materialized view precomputes detail data, and a regular view does dimension aggregation on top of the materialized view.
  5. ⭐⭐⭐ Write a pg_cron scheduled job that refreshes mv_daily_category_sales every day at 3:00 AM, and logs an alert when the refresh takes longer than 5 minutes.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏