首页 / PostgreSQL 入门教程 / 视图(VIEW)与物化视图

PostgreSQL 入门教程

视图(VIEW)与物化视图

本教程共 50 篇 · 第 38 篇 · 更新于 2026-07-31 · 约 8 分钟阅读

PostgreSQLPostgreSQL 入门教程视图物化视图CHECK OPTIONREFRESH

38. 视图(VIEW)与物化视图

本节目标:学会用视图把常用查询封装成”虚拟表”,并理解物化视图如何在性能与实时性之间做权衡。

什么是视图

视图(VIEW)是一张”虚拟表”。它本身不存任何数据,只保存一条 SELECT 查询。

说白了,视图就是给一段查询起了个名字。以后想用这段结果,直接 SELECT * FROM 视图名 就行,不用把长 SQL 重写一遍。

视图常用来做三件事:

  1. 简化复杂查询,把多张表 JOIN 的结果藏在一个名字后面。
  2. 统一数据出口。底表改了结构,只要视图定义跟着改,上层调用方无感知。
  3. 控制权限。让用户只看见部分列或行,而不是整张表。
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 多表

视图背后可以是一条很复杂的查询。比如把 usersorders 连起来,做一个”每个用户的订单统计”视图:

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 VIEWCREATE

删除视图:

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 还有两个级别:LOCALCASCADED

  • LOCAL:只检查”这一层视图自己”有没有定义 CHECK OPTION。
  • CASCADED(默认):会顺着视图链,把每一层视图的筛选条件都检查一遍。

举个实际例子。视图 v1 基于 users 且带 age >= 18v2 基于 v1 且带 age <= 60。当 v2CASCADED CHECK OPTION 时,写入数据要同时满足 v1v2 的条件;用 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 基于 v1v1 基于 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,是做”近实时报表”的常见组合:白天查的是已刷新好的快照,又快又稳。

常见误区

  1. 以为视图能加速查询。视图不存数据,慢查询套层视图还是慢,要先优化底层 SQL 和索引。
  2. 对不可更新视图做写入。带聚合 / JOIN / GROUP BY / DISTINCT 的视图不能 UPDATE,想改数据得靠触发器(见第 45 章)或改底层表。
  3. 物化视图忘了刷新。查出来是旧数据是常态,不是 bug,记得 REFRESH。
  4. 滥用物化视图做实时展示。用户看到的数据可能是几小时前的,上线前要想清楚业务能不能接受延迟。