视图(VIEW)与物化视图
本教程共 50 篇 · 第 38 篇 · 更新于 2026-07-31 · 约 8 分钟阅读
38. 视图(VIEW)与物化视图
本节目标:学会用视图把常用查询封装成”虚拟表”,并理解物化视图如何在性能与实时性之间做权衡。
什么是视图
视图(VIEW)是一张”虚拟表”。它本身不存任何数据,只保存一条 SELECT 查询。
说白了,视图就是给一段查询起了个名字。以后想用这段结果,直接 SELECT * FROM 视图名 就行,不用把长 SQL 重写一遍。
视图常用来做三件事:
- 简化复杂查询,把多张表 JOIN 的结果藏在一个名字后面。
- 统一数据出口。底表改了结构,只要视图定义跟着改,上层调用方无感知。
- 控制权限。让用户只看见部分列或行,而不是整张表。
Tip视图里的数据不是副本。每次查视图,数据库都会重新跑它背后的那条 SELECT。视图越多、底层越复杂,查询开销和直接查原表是一样的。
视图背后的真相
视图的定义存在系统目录里。你可以用 \d+ 视图名 看它的定义,也能直接查 pg_views:
postgres=# \d+ adult_users
postgres=# SELECT definition FROM pg_views WHERE viewname = 'adult_users';
当你查询视图时,PostgreSQL 的查询规划器会把视图定义”展开”拼进你的 SQL,再统一优化执行。所以视图本身不会让查询变快,它的价值在于”好用”和”安全”,不在于”快”。
创建视图
语法很简单。我们先建好统一的示例表 users:
postgres=# CREATE TABLE users (
postgres=# id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
postgres=# username TEXT NOT NULL,
postgres=# email TEXT,
postgres=# age INT,
postgres=# created_at TIMESTAMP DEFAULT now()
postgres=# );
postgres=# INSERT INTO users (username, email, age) VALUES
postgres=# ('alice', 'alice@x.com', 28),
postgres=# ('bob', 'bob@x.com', 35),
postgres=# ('carol', 'carol@x.com', 17);
建一个只看成年用户的视图:
postgres=# CREATE VIEW adult_users AS
postgres=# SELECT id, username, age
postgres=# FROM users
postgres=# WHERE age >= 18;
查视图和查普通表一模一样:
postgres=# SELECT * FROM adult_users;
id | username | age
----+----------+-----
1 | alice | 28
2 | bob | 35
(2 rows)
视图也能 JOIN 多表
视图背后可以是一条很复杂的查询。比如把 users 和 orders 连起来,做一个”每个用户的订单统计”视图:
postgres=# CREATE TABLE orders (
postgres=# id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
postgres=# user_id INT,
postgres=# amount NUMERIC(10,2),
postgres=# status TEXT,
postgres=# created_at TIMESTAMP DEFAULT now()
postgres=# );
postgres=# INSERT INTO orders (user_id, amount, status) VALUES
postgres=# (1, 99.00, 'paid'),
postgres=# (1, 50.00, 'paid'),
postgres=# (2, 20.00, 'refunded');
postgres=# CREATE VIEW user_order_stat AS
postgres=# SELECT u.id,
postgres=# u.username,
postgres=# COUNT(o.id) AS order_cnt,
postgres=# COALESCE(SUM(o.amount), 0) AS total_amount
postgres=# FROM users u
postgres=# LEFT JOIN orders o ON o.user_id = u.id
postgres=# GROUP BY u.id, u.username;
postgres=# SELECT * FROM user_order_stat;
Note这个视图里用了 JOIN 和 GROUP BY,所以它属于”不可更新视图”,不能对它做 INSERT / UPDATE。后面会讲哪些视图能更新。
修改与删除视图
视图定义要改,用 CREATE OR REPLACE VIEW 重建,名字不变:
postgres=# CREATE OR REPLACE VIEW adult_users AS
postgres=# SELECT id, username, email, age
postgres=# FROM users
postgres=# WHERE age >= 18;
Warning
CREATE OR REPLACE VIEW只能改 SELECT 部分,而且新定义的列数、列顺序必须和原来一致,否则会报错。要彻底改结构(比如加列、减列),得先DROP VIEW再CREATE。
删除视图:
postgres=# DROP VIEW IF EXISTS adult_users;
可更新视图与 CHECK OPTION
简单视图(只来自单表、不含聚合 / JOIN / DISTINCT / GROUP BY)是可以更新的。对视图做 INSERT / UPDATE / DELETE,会直接作用到底表。
但有个坑:你插入的数据可能不满足视图的 WHERE 条件,于是它”消失”在视图里,却真实存在底表。比如往 adult_users 里插一个 15 岁的用户,视图里看不到他,底表 users 里却多了一条。
WITH CHECK OPTION 就是用来堵这个坑的。它要求通过视图写入的数据,必须能通过视图的筛选条件,否则拒绝写入:
postgres=# CREATE VIEW adult_users AS
postgres=# SELECT id, username, age
postgres=# FROM users
postgres=# WHERE age >= 18
postgres=# WITH CHECK OPTION;
postgres=# INSERT INTO adult_users (username, age) VALUES ('tom', 15);
ERROR: new row violates check option for view "adult_users"
CHECK OPTION 还有两个级别:LOCAL 和 CASCADED。
LOCAL:只检查”这一层视图自己”有没有定义 CHECK OPTION。CASCADED(默认):会顺着视图链,把每一层视图的筛选条件都检查一遍。
举个实际例子。视图 v1 基于 users 且带 age >= 18;v2 基于 v1 且带 age <= 60。当 v2 用 CASCADED CHECK OPTION 时,写入数据要同时满足 v1 和 v2 的条件;用 LOCAL 时,只有 v2 自己定义了 CHECK OPTION 才检查 v2 的条件。
Note日常用默认的
CASCADED最省心,能保证数据始终对得起每一层视图的筛选条件。
用视图做权限隔离
视图是权限控制的好工具。比如你不想让报表账号直接碰 users 全表,可以只给它一个视图的查询权:
postgres=# REVOKE ALL ON users FROM report_role;
postgres=# GRANT SELECT ON adult_users TO report_role;
这样 report_role 只能通过视图看成年用户,既满足了需求,又挡住了敏感行和列。底层表结构变了,只要视图还在,报表脚本一行都不用改。
物化视图(MATERIALIZED VIEW)
普通视图每次都重跑查询,底层表很大时就慢。物化视图(Materialized View)不一样:它把查询结果真的存到磁盘上,像一张实实在在的表。
postgres=# CREATE MATERIALIZED VIEW mv_user_stat AS
postgres=# SELECT u.username,
postgres=# COUNT(o.id) AS order_cnt,
postgres=# COALESCE(SUM(o.amount), 0) AS total_amount
postgres=# FROM users u
postgres=# LEFT JOIN orders o ON o.user_id = u.id
postgres=# GROUP BY u.username;
物化视图的代价是:数据不是实时的,底表变了它不会自动更新。要刷新:
postgres=# REFRESH MATERIALIZED VIEW mv_user_stat;
创建时也可以先不填充数据,等需要时再刷新:
postgres=# CREATE MATERIALIZED VIEW mv_user_stat AS
postgres=# SELECT u.username, COUNT(o.id) AS order_cnt
postgres=# FROM users u LEFT JOIN orders o ON o.user_id = u.id
postgres=# GROUP BY u.username
postgres=# WITH NO DATA;
postgres=# REFRESH MATERIALIZED VIEW mv_user_stat; -- 必须先刷新才能查询
想要刷新时不阻塞读写,加 CONCURRENTLY,但它要求物化视图上必须有唯一索引:
postgres=# CREATE UNIQUE INDEX ON mv_user_stat (username);
postgres=# REFRESH MATERIALIZED VIEW CONCURRENTLY mv_user_stat;
Tip没有唯一索引就写
CONCURRENTLY,会直接报错。生产环境刷新大物化视图,记得先建唯一索引,再用CONCURRENTLY,否则刷新期间表会被锁住,读写都卡。
删除物化视图:
postgres=# DROP MATERIALIZED VIEW IF EXISTS mv_user_stat;
视图 vs 物化视图
| 对比项 | 视图 VIEW | 物化视图 MATERIALIZED VIEW |
|---|---|---|
| 是否存数据 | 不存,每次重算 | 存到磁盘 |
| 查询速度 | 慢(底层大时) | 快 |
| 数据实时性 | 实时 | 需要 REFRESH 才更新 |
| 典型用途 | 简化查询、控权限 | 报表、统计加速 |
Tip要实时且逻辑简单,用视图;要快但能接受”稍微旧一点”的数据,用物化视图。物化视图常配合定时任务(比如 pg_cron)定期 REFRESH,做成 T+1 报表很合适。
嵌套视图与依赖管理
视图背后可以是另一个视图。比如 v2 基于 v1,v1 基于 users。这种”嵌套视图”能一层层封装逻辑,但有个隐患:CREATE OR REPLACE VIEW v1 改了底层视图定义,PostgreSQL 不会自动去更新依赖它的 v2。如果改出的结构对不上,v2 查询时直接报错。
想查看谁依赖了某张视图,用 \d+ 视图名,结果里能看到它的定义和依赖项。删底层对象时加 CASCADE 会连依赖的视图一起删,动手前想清楚:
postgres=# DROP VIEW v1 CASCADE; -- 依赖 v1 的 v2 也会被删
给物化视图建索引
物化视图本质是张真表,所以能像普通表一样建索引,进一步加速对它的查询。比如按统计金额排序展示时,给 total_amount 建个索引:
postgres=# CREATE INDEX ON mv_user_stat (total_amount);
Tip物化视图 + 索引 + 定时 REFRESH,是做”近实时报表”的常见组合:白天查的是已刷新好的快照,又快又稳。
常见误区
- 以为视图能加速查询。视图不存数据,慢查询套层视图还是慢,要先优化底层 SQL 和索引。
- 对不可更新视图做写入。带聚合 / JOIN / GROUP BY / DISTINCT 的视图不能 UPDATE,想改数据得靠触发器(见第 45 章)或改底层表。
- 物化视图忘了刷新。查出来是旧数据是常态,不是 bug,记得 REFRESH。
- 滥用物化视图做实时展示。用户看到的数据可能是几小时前的,上线前要想清楚业务能不能接受延迟。