PostgreSQL 教程走到第 11 篇,终于轮到视图了。前面聊过安装部署、基础 SQL、事务隔离级别,甚至碰了碰索引原理,但视图这个主题我一直压着没写,原因很简单:这东西看起来人人都会,可真到实战里,能把它用明白的人不多。很多人对视图的理解停留在“一条存起来的 SELECT 语句”,这个认知本身没错,但远远不够。PostgreSQL 16 里的视图,涉及到权限体系、查询重写、依赖管理、物化刷新机制,甚至能影响整个项目的架构设计。这篇我打算把语法、原理、实战案例和踩坑记录一次讲透,不只是告诉你 CREATE VIEW 怎么用,而是讲清楚什么时候该用、什么时候千万别用、为什么权限会报错、视图到底能不能加速查询。内容偏实用派,适合已经能熟练写 SQL 但想进阶的开发者,也适合正在用 PostgreSQL 做项目的后端工程师。
1. 视图到底解决了什么问题——先搞清楚为什么要用它
1.1 视图不是表,但比表更“好用”
我刚接触 PostgreSQL 的时候,对视图的第一印象就是“省事”。原本要写一大段 JOIN 的查询,建个视图之后,每次只查视图就行,SQL 短了,人也轻松了。后来项目做大了才发现,视图真正的价值远不止“少打字”。它是一个逻辑抽象层,把底层表结构的变化和后端业务的查询语句隔离开。今天你有个订单表,明天需求变了,订单拆成主表和子表,如果业务代码里直接写了十几处关联查询,改起来能改到怀疑人生。但如果你从一开始就给业务层提供一个订单视图,底层表拆了,只需要改视图的定义,业务代码一行不动,这才是视图最值钱的地方。
还有一个经常被忽略的作用:安全。视图可以隐藏敏感字段。比如用户表里有密码哈希、手机号、身份证号,这些字段不想让某个低权限角色看到,那就不给这个角色基表的 SELECT 权限,只给它一个不含敏感列的视图的查询权限。这样它照样能查业务数据,但永远碰不到不该看的东西。这种“通过视图做列级权限隔离”的做法,在银行、医疗这类数据敏感行业里几乎是标配。
1.2 什么时候该建视图,什么时候不该建
视图不是万能药,建多了反而是灾难。我在实际项目里见过一种“视图套视图”的写法,几十个视图层层嵌套,底层一张表改动,上面十几个视图全部失效,排查问题的时候顺着依赖关系往下追,追到一半人就麻了。所以我自己的经验是:视图适合建在业务语义稳定、查询逻辑固定的场景。比如说“有效订单”“当前库存”“用户最近一次登录”这类定义明确、长期不变的东西,非常适合做成视图。反过来,如果是临时排查问题、一次性取数,或者查询条件高度动态、每次都要拼不同 WHERE 的,那就老老实实写 SQL,别硬套视图。
还有一种情况要特别注意:视图不要用来掩盖糟糕的表结构设计。如果你的字段命名混乱、表之间关联关系绕了三层以上,正确做法是先去重构表结构,而不是建一个复杂的视图把问题藏起来。视图是抽象,不是遮羞布,这个定位一定要摆正,否则后续维护成本会成倍增加。
2. 视图语法拆解:从最基础的 CREATE VIEW 说起
2.1 一个最简的视图怎么建
PostgreSQL 里创建视图的完整语法长这样:
CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] [ RECURSIVE ] VIEW name [ (column_name [, ...] ) ] [ WITH ( view_option_name [= view_option_value] [, ... ] ) ] AS query [ WITH [ CASCADED | LOCAL ] CHECK OPTION ]看着选项很多,但日常用得最多的核心就这一段:
CREATE VIEW view_name AS SELECT column1, column2, ... FROM table_name WHERE condition;举个例子。假设有一个电商项目的订单表 orders,字段包括订单号、用户ID、订单金额、订单状态、创建时间。开发中经常要查“已支付订单”的列表,每次都要写 WHERE status = 'PAID',烦不烦?烦。建个视图:
CREATE VIEW paid_orders AS SELECT order_no, user_id, amount, created_at FROM orders WHERE status = 'PAID';以后想查已支付订单,直接:
SELECT * FROM paid_orders WHERE created_at >= '2025-01-01';看起来简单,但这里埋着一个新手必踩的坑:视图内部的 WHERE 和外层查询追加的 WHERE 是分开执行的。视图先把满足自身条件的行查出来,再在这个结果集上应用外层的过滤条件。大多数情况下数据库优化器会把它合并成一个整体执行计划,性能不受影响,但你不能在心理上依赖“视图会代替我做所有事”。
2.2 WITH CHECK OPTION:防住那些偷偷溜走的行
这是视图语法里最容易被忽略、同时也最实用的一项。默认情况下,如果你往一个带 WHERE 条件的视图里 INSERT 数据,PostgreSQL 只检查基表本身的约束,不会管这行数据是不是满足视图的过滤条件。这就会导致一个诡异的现象:你往 paid_orders 视图里插入了一条 status = 'PENDING' 的记录,插入成功了,但这条记录在 paid_orders 里根本查不到。
这不是 Bug,这是 SQL 标准的默认行为。如果你想避免这种情况,就要加上 WITH CHECK OPTION:
CREATE VIEW paid_orders AS SELECT order_no, user_id, amount, status, created_at FROM orders WHERE status = 'PAID' WITH CHECK OPTION;这样再往视图里插入或更新数据时,PostgreSQL 会强制校验新行的 status 必须是 'PAID',不满足就直接报错。这个特性的典型场景是做“按地区分表的逻辑视图”或者“只允许操作自己负责的数据”的业务约束。还有个细节:WITH 后面可以写 CASCADED(默认)或 LOCAL。CASCADED 表示校验所有底层视图的过滤条件,LOCAL 只校验当前视图自己的。我建议默认用 CASCADED,语义更严谨,不容易出漏子。
2.3 改视图和删视图:OR REPLACE 的边界
代码上线后发现视图写错了,需要修改。很多人第一反应是先 DROP 再 CREATE。结果一执行,业务系统里凡是依赖这个视图的权限授权、物化视图、下游对象,全部跟着失效。更好的做法是使用 OR REPLACE:
CREATE OR REPLACE VIEW paid_orders AS SELECT order_no, user_id, amount, status, created_at, updated_at FROM orders WHERE status = 'PAID';需要注意,OR REPLACE 只能替换视图的 SELECT 部分,不能修改视图的列名、列类型,也不能改变视图的属性(比如把普通视图换成物化视图)。如果你想改列名,目前的办法只能是 DROP 之后重建。删视图的时候还要特别小心:如果其他视图依赖这个视图,DROP 会失败并提示依赖关系存在。这时候可以结合 CASCADE 强制级联删除,但我强烈建议慎用,因为 CASCADE 会把下游依赖的视图一起删掉,一旦删完发现删多了,恢复起来很麻烦。
3. 权限与安全:视图在权限体系中的“过滤器”角色
3.1 创建视图权限不足?先搞明白 PostgreSQL 的权限模型
热词里专门提到了“创建视图权限不足”,这个问题我一年下来至少帮人排查十几次。现象很简单:执行 CREATE VIEW 报错,说没有权限。但很多人查了半天自己的角色明明有表的 SELECT 权限,为什么建视图还报错?因为 PostgreSQL 里 CREATE VIEW 一共牵扯到三类权限:
| 权限项 | 说明 |
|---|---|
| 基表的 SELECT 权限 | 视图本质上要读取基表数据,没有这个权限就是巧妇难为无米之炊 |
| Schema 的 USAGE 权限 | 建出来的视图要放进某个 schema 里,没有 USAGE 权限就进不去 |
| Schema 的 CREATE 权限 | 只有 USAGE 不够,还需要在目标 schema 上拥有 CREATE 权限才能创建新对象 |
顺带说一句,如果你要创建的视图引用了函数,比如CREATE VIEW some_view AS SELECT my_func(id) FROM ...,那还需要这个函数的 EXECUTE 权限,否则一样报错。处理办法也很直接,用超级用户或者 schema 属主执行授权:
GRANT USAGE ON SCHEMA public TO app_user; GRANT CREATE ON SCHEMA public TO app_user; GRANT SELECT ON orders TO app_user;这里要注意 PostgreSQL 15 之后的一个变化:不再默认把 public schema 的 CREATE 权限授予所有角色。如果你是升级到 PG15+ 之后发现自己建视图突然报权限错误,先别怀疑用户配置,去检查 schema 的默认权限就知道了。我自己从 PG14 升到 PG16 的时候就踩过这个坑,查了半天才发现是版本行为变化导致的。
3.2 视图的默认权限和安全定义者模式
默认情况下,视图继承创建者(owner)的权限。如果视图创建者是超级用户,那普通用户只要被授予了视图的 SELECT 权限,就能通过这个视图读到基表数据——哪怕基表本身没有给这个用户授权。这就是视图做权限隔离的基础原理。但这里有一个必须警惕的风险:如果你创建一个视图是为了“掩藏敏感列”,那基表的权限一定要收紧。假设用户 A 能直接查询 users 表,你建一个只含名字的视图給用户 B 没有意义,因为用户 B 如果有别的途径访问到表本身,视图的隔离就被绕过了。
PostgreSQL 还支持一种更激进的权限模式:安全定义者(SECURITY DEFINER)。普通视图默认是 SECURITY INVOKER,也就是查询时使用调用者自身的权限做校验;而 SECURITY DEFINER 视图在查询时使用视图创建者的权限。这个特性在跨 schema 读数据、或者需要让低权限账号访问特定高权限数据的场景下很有用:
CREATE VIEW confidential_view AS SELECT * FROM private_schema.sensitive_data SECURITY DEFINER;但我要提醒一句:SECURITY DEFINER 是把双刃剑。如果你的视图定义里包含动态拼接 SQL 或者调用了不安全的函数,很可能会成为权限提升的突破口。我自己在项目里几乎没有业务视图使用 SECURITY DEFINER,只有那种非常明确、查询逻辑完全可控的管理类视图才用,而且会用 REVOKE 把不必要的权限收干净。
3.3 基于视图的“行级”数据隔离思路
PostgreSQL 本身有行级安全策略(RLS),但有时候你不想开启 RLS,或者基表不允许加策略,这时候视图可以结合current_user之类的函数实现简单的“伪行级隔离”。比如一家多商户平台,订单表是公用的,每个商户只能看自己的数据:
CREATE VIEW merchant_orders AS SELECT order_no, amount, status FROM orders WHERE merchant_id = ( SELECT merchant_id FROM merchant_users WHERE username = current_user );查询视图的时候,PostgreSQL 会动态根据当前用户名把结果过滤到对应的商户数据。这个方案性能上不占优势,因为每条查询都要执行一次子查询,但胜在实现简单、不需要开启 RLS、对业务的侵入性低。如果你的项目足够复杂,并且在意性能,还是直接上 RLS 更专业。视图方案适合中小项目快速落地。
4. 视图真的能加快查询速度吗——深入性能和物化视图
4.1 普通视图不会缓存数据,别指望它加速
这个问题在热词里出现了:视图可以加快查询速度吗?直接给答案:普通视图不能加速,它只是 SQL 文本的封装。每次你查一个普通视图,PostgreSQL 都会重新执行视图内部定义的那条查询。换句话说,视图本身不存储任何数据,磁盘上只有一份定义,查视图和查底层的原始查询在性能上没有本质区别。
那为什么有时候用了视图感觉变快了?两种可能:一是你原来写的是SELECT * FROM tbl WHERE ...,每次都要全表扫描,而视图内部写好了更优的过滤条件,优化器合并后产生了更好的执行计划;二是你修改了查询条件后正好命中了索引。这些都是外部因素,不是视图本身的“功劳”。所以如果有人跟你说“建了视图就变快”,你就明白这属于玄学,要么是心理安慰,要么是巧合。
真正能加速的是另一类对象——物化视图。
4.2 物化视图:把查询结果物理存下来
物化视图和普通视图最大的区别在于:物化视图会把查询结果真正落盘存储。查询的时候不再执行原始 SQL,而是直接读磁盘上的预计算结果,速度自然快得多。old版本的语法和普通视图很像:
CREATE MATERIALIZED VIEW monthly_sales_summary AS SELECT seller_id, DATE_TRUNC('month', order_date) AS mon, SUM(amount) AS total_amount FROM orders GROUP BY seller_id, DATE_TRUNC('month', order_date);创建完成后,物化视图里保存了计算好的数据。但它不是实时刷新的——基表数据变了,物化视图里的数据依然是旧快照。手动刷新:
REFRESH MATERIALIZED VIEW monthly_sales_summary;如果你用的是 PostgreSQL 9.3 以上版本,还能用 CONCURRENTLY 模式,它的意义是刷新期间不锁住查询:
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales_summary;用 CONCURRENTLY 的前提是物化视图上必须创建唯一索引,否则会报错。这个模式最大的价值在于,运营系统的报表在白天随时要查,如果刷新锁住了表,查询就会卡住,线上事故就是这么出来的。我自己在项目里几乎全部用 CONCURRENTLY 刷新,宁可多两步先建索引,也不愿意承受刷新期间查询被阻塞的代价。
4.3 什么时候必须用物化视图
判断标准就一条:结果集大、计算代价高、刷新频率低。典型场景包括大宽表的聚合报表、跨多表的复杂统计、数据仓库层的汇总表。比如一个订单明细表有几千万行,每次统计“每个商户上个月的销售额”都要全表扫描,聚合计算可能跑十几秒,用户等不起,这时候物化视图一开,查询时间可能从十几秒降到几十毫秒,效果立竿见影。但物化视图也有代价:占用磁盘空间,刷新任务需要调度,数据存在延迟。所以千万不能无脑把每个视图都改成物化视图。如果基表每分钟都在变,且业务要求实时看到最新数据,物化视图就不合适,老老实实用普通视图或者直接查询。
4.4 视图嵌套别太深,优化器不是万能的
PostgreSQL 的查询优化器能力很强,但它不是神。视图套视图套视图,每套一层,优化器的“脑力”就被消耗一分。嵌套到三四层以上,执行计划开始出现“胎教级错误”——明明可以先过滤再 JOIN,优化器偏要先 JOIN 再过滤,结果中间结果集膨胀到内存溢出。我在 PG16 上实测过一个 5 层嵌套视图,查询耗时从预期的毫秒级变成了 40 多秒,后来把视图拆平、手写直接 SQL,稳稳跑到 100ms 以内。所以实际工作中,我给自己定的原则是:视图嵌套最多两层。超过两层,要么重构查询逻辑,要么直接上物化视图。
5. 从简单到复杂:四个实战案例拆解
5.1 场景一:报表看板,用视图把多表关联藏起来
运营看板需要展示“每日订单总额、每日新增用户数、每日退款金额”。如果让运营同学直接去写三表关联的 SQL,估计他们能把你拉黑。建三个视图把它们包起来,底层随便怎么 JOIN,上层只要查视图即可。示例:
CREATE VIEW daily_order_stats AS SELECT o.order_date, COUNT(o.order_no) AS order_cnt, SUM(o.amount) AS order_amount FROM orders o GROUP BY o.order_date; CREATE VIEW daily_user_stats AS SELECT u.created_at::date AS reg_date, COUNT(*) AS new_user_cnt FROM users u GROUP BY u.created_at::date;报表端如果需要一次性拿到三个统计信息,还可以再建一层“报表聚合视图”,把上面两个 JOIN 起来。注意了,这已经是两层嵌套,再往上我就不建议了。
5.2 场景二:软删除的统一过滤视图
很多项目用 is_deleted 标志位做软删除,结果每个查询都要写一句WHERE is_deleted = false。漏一次就会把“已删除”的数据查出来,出线上事故。我习惯的做法是:每个核心表建一个“有效数据”视图,比如:
CREATE VIEW active_orders AS SELECT * FROM orders WHERE is_deleted = false;然后所有业务查询只查 active_orders,不直接碰 orders。这样一劳永逸,即使以后有人往 orders 里插入 is_deleted = true 的数据,业务侧也不会被污染。这套方案我在好几个项目里都验证过,省心程度超出预期。
5.3 场景三:递归视图,算组织架构树
PostgreSQL 支持递归视图。语法上很像递归 CTE,但把递归的查询封装成了一个可重复使用的对象。比如员工表 structure 有 employee_id、manager_id 字段,要算某个部门下面的所有下级员工:
CREATE RECURSIVE VIEW org_chart AS SELECT employee_id, manager_id, 1 AS depth FROM structure WHERE manager_id IS NULL UNION ALL SELECT s.employee_id, s.manager_id, oc.depth + 1 FROM structure s JOIN org_chart oc ON s.manager_id = oc.employee_id;递归视图最常见的用途就是组织架构、菜单层级、BOM 物料清单这类树形结构。不过递归视图有个天生的约束:不能使用聚合函数、窗口函数、ORDER BY、LIMIT、DISTINCT。所以你只能拿它做“展开树”这种纯递归的事情,复杂加工还得靠 CTE。
5.4 场景四:跨库查询,用外部表和视图组合
PostgreSQL 可以通过 postgres_fdw 扩展把远程数据库的表映射成本地外部表,然后再建一个视图把这些外部表和其他本地表组合起来。比如本地有员工本地表,销售数据在另一个数据库里,可以这样:
CREATE EXTENSION postgres_fdw; CREATE SERVER remote_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '192.168.1.101', dbname 'sales_db', port '5432'); CREATE USER MAPPING FOR current_user SERVER remote_server OPTIONS (user 'readonly_user', password 'xxx'); CREATE FOREIGN TABLE remote_sales ( sale_id int, region varchar(50), amount numeric ) SERVER remote_server OPTIONS (schema_name 'public', table_name 'sales');然后把外部表和本地表 JOIN 成视图,业务侧就能像查本地表一样进行跨库查询。这个方法特别适合“多个业务子系统之间数据联动”的场景,省去中间同步表的环节,数据实时性也得到了保障。
6. 常见问题与排查技巧实录
6.1 视图里的 ORDER BY 失效之谜
不少人给视图的定义里写了 ORDER BY,结果查视图的时候发现顺序根本不对。原因是 PostgreSQL 认为视图的内部排序没有意义——外层查询可能有自己的 WHERE、JOIN、LIMIT,重新调整顺序是合法的。唯一能稳定保证顺序的方式是查询视图时自己写 ORDER BY。这不是 Bug,是数据库的正确行为。所以我的建议:视图定义里别写 ORDER BY,没意义,还容易被误解。
6.2 视图里的字段类型隐式转换
视图里 SELECT 某些表达式时,PostgreSQL 会自动推断列类型。比如amount / 100可能是 numeric 类型,但如果你原表里 amount 是 integer,那结果可能是 integer,小数点被吞了。建视图时发现数据不对,先检查列的类型是不是被隐式转换了。想精确控制,就在视图里显式 CAST:
CREATE VIEW order_view AS SELECT amount / 100.0 AS amount_yuan FROM orders;这种细节点排查起来很隐蔽,我建议建完视图后用这条查询看一下列的类型:
SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'order_view';6.3 视图依赖导致基表无法修改
基表表名改了、字段删了,但下层有视图依赖,ALTER TABLE 会报错提示有依赖对象存在。处理方式有两种:一是先把视图 DROP 掉再改表,二是用 DROP ... CASCADE 连带删除依赖对象。现实中我通常是先查出依赖关系,评估影响范围再做处理:
SELECT dependent_ns.nspname AS dependent_schema, dependent_view.relname AS dependent_view, source_ns.nspname AS source_schema, source_table.relname AS source_table FROM pg_depend JOIN pg_rewrite ON pg_depend.objid = pg_rewrite.oid JOIN pg_class AS dependent_view ON pg_rewrite.ev_class = dependent_view.oid JOIN pg_namespace AS dependent_ns ON dependent_view.relnamespace = dependent_ns.oid JOIN pg_class AS source_table ON pg_depend.refobjid = source_table.oid JOIN pg_namespace AS source_ns ON source_table.relnamespace = source_ns.oid;这招在大型系统里做“改动前风险评估”特别好用。列出来就知道这张表的改动会影响哪些视图,提前跟业务方沟通好,避免线上炸锅。
6.4 视图更新:哪些视图能 INSERT / UPDATE / DELETE
PostgreSQL 对自动更新视图有条件要求:视图必须直接引用基表的列,不能有聚合、DISTINCT、GROUP BY、窗口函数,且基表字段不能是表达式。满足条件后,你可以直接对视图执行 UPDATE 甚至 DELETE,PostgreSQL 会把操作映射到基表上。如果你需要强制走更复杂的逻辑,可以用 INSTEAD OF 触发器,在触发器中自己处理 INSERT/UPDATE/DELETE 的映射。这个技术在做“对视图写入”的复杂业务里非常有用,但也可以说是所有视图话题里最考验经验的点。
6.5 如何查看视图定义和物化视图状态
排查问题时需要快速看到视图的完整定义,用这条命令:
SELECT pg_get_viewdef('view_name'::regclass, true);第二个参数 true 表示美化缩进,读起来更舒服。物化视图还可以查它的刷新状态和上次刷新时间,在 pg_matviews 系统视图里:
SELECT matviewname, ispopulated FROM pg_matviews;ispopulated 是 false 说明物化视图还没被刷新过,数据是空的。
7. 一些实战后的心里话
从入门到现在,我在 PostgreSQL 里建过的视图没有一百也有八十了。说实话,视图这个东西,真正让我体会到价值不是在某一次“加速查询”里,而是在维护一个两三年历史、几十个模块的项目时,视图带来的稳定性和安全感。底层的表怎么变动,业务侧就是不受影响,这种感觉做过大项目的人都懂。但反过来,视图用滥了,依赖关系乱成一锅粥,系统出个问题想定位都无从下手。所以我个人的态度是:视图是抽象工具,不是存储优化手段;物化视图是性能工具,不是万能缓存。两者配合使用,控制好层级和依赖,才能让 PostgreSQL 这套系统长期健康地跑下去。
最后再分享一个小技巧:新版本 PostgreSQL 里,如果你不确定自己的视图建得合不合理,可以定期查一下 pg_stat_user_tables,看看那些视图关联的基表有没有高频全表扫描。如果发现底层表出现 Seq Scan 且数据量大、执行时间长,说明你的视图或索引策略需要重新评估了。数据不会骗人,跑一跑监控,比什么经验都靠谱。