ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PostgreSQL视图实战指南:从原理、性能到权限管理全解析

PostgreSQL视图实战指南:从原理、性能到权限管理全解析 写在前面先给结论PostgreSQL 视图不是一个多么高深的概念,但你真要把它用对、用好、用明白,至少得搞懂三件事——它底层到底存了什么、它跟物化视图的性能差距在哪、以及它在权限模型里究竟扮演什么角色。很多人在网上搜PostgreSQL 视图教程,看到的都是 CREATE VIEW 加 SELECT,抄完一运行,要么报创建视图权限不足,要么查询一点没变快,然后就一脸懵。这篇笔记以 PostgreSQL 16 为例,从语法拆到实战,再从实战绕回排查技巧,尽量把视图这个主题讲透。如果你正卡在视图到底怎么用或者视图能不能加速查询这两个问题上,这篇文章应该能帮你省掉不少百度时间。1. 视图存在的理由一句话讲清楚它的本质1.1 视图不是表,也不是快照,而是一条命名的查询很多人第一次接触视图,会把它理解成一张保存在数据库里的虚拟表,这个说法没错,但容易造成误解我觉得更准确的理解是——视图的本质就是一条被保存下来的 SELECT 语句。你创建一个普通视图时,PostgreSQL 并不会把查询结果拷贝到磁盘里,也不会额外占用一份物理存储空间,它只是在系统表里记录了这条查询的定义。当你执行SELECT * FROM my_view;时,PostgreSQL 的优化器会把视图定义展开,和外部查询一起重写,然后生成一个整体的执行计划。这个过程跟你在外层查询里直接写那段子查询几乎没有区别。搞清楚这一点非常重要,因为后续所有关于性能的争议,都源于此普通视图本身不存储数据,所以它不可能天然加速查询。[\text{普通视图} \text{命名的查询宏}]整天抓运维的同事问我PostgreSQL 视图能不能当临时表用,我的回答永远是临时表好歹把数据落到了内存或磁盘,视图就是一个定义,连落盘都不落,你拿它当表用,方向就错了。1.2 视图能解决的三个核心问题第一是简化复杂查询。一个上百行的多表 JOIN 查询,如果在大屏报表、BI 工具、或者多个接口里反复出现,每次把那段 SQL 粘来粘去既丑又难维护。把它封装成视图之后,业务方只需要关心视图名和字段名,底层逻辑藏在视图定义里,改起来也集中。第二是逻辑隔离。底层表结构一旦调整,比如把字段从 name 拆成 first_name 和 last_name,只要视图的输出字段保持不变,上层应用几乎不用动。这种逻辑层对物理表结构的屏蔽,在系统长期演进中价值极大。第三是权限控制。你可以只给某个角色SELECT某个视图的权限,而把底层表上的权限全部收回。这样对方能查到你想让他查的数据,但永远看不到底层表的全貌。这比给张全表、期望他自觉不看去查靠谱得多。1.3 盲目用视图的三种反面场景第一种是视图套视图,套个三四层。每次查询都要展开多层定义,优化器处理成本上升,人阅读这种 SQL 也会想打人。第二种是想用普通视图固化排序结果,比如在视图定义里写 ORDER BY,然后指望每次查询都按这个顺序返回。视图本身不保证输出顺序,外层查询一旦有过滤或排序,内层排序很可能被忽略,视图有序是个伪需求。第三种是纯跑批、一次性分析场景也要建视图,结果没人复用、也没什么权限隔离需求,白养一堆定义,不如直接写 SQL。2. PostgreSQL 16 中视图的完整语法拆解2.1 CREATE VIEW 基本语法与字段命名在 PostgreSQL 里创建一个视图,最小化语法如下CREATE VIEW order_user_view AS SELECT o.order_id, u.user_name, o.amount FROM orders o JOIN users u ON o.user_id u.id;默认情况下,视图的字段名会直接采用 SELECT 输出列的名字。如果你对输出的列名不满意,可以在视图名后面显式指定CREATE VIEW order_user_view (order_no, buyer, total_amount) AS SELECT o.order_id, u.user_name, o.amount FROM orders o JOIN users u ON o.user_id u.id;这里有个实用细节如果你在 SELECT 子句里用了函数或者表达式,比如sum(amount)、count(*),系统会自动给列起个不太好认的名字。显式指定列名,能避免下游拿到的字段名奇奇怪怪,尤其是对接 BI 工具时,列名稳定比什么都重要。完整语法其实还有几个可选项CREATE [ OR REPLACE ] [ TEMP ] [ RECURSIVE ] VIEW name [ ( column_name [, ...] ) ] WITH ( view_option_name [ view_option_value] [, ... ] ) AS query WITH [ CASCADED | LOCAL ] CHECK OPTION;TEMP 表示创建临时视图,会话结束自动消失,适合在某个复杂事务的中间环节做逻辑隔离。RECURSIVE 是递归视图,后面专门讲。2.2 OR REPLACE 的隐藏限制只能加列,不能随便删改列视图定义不可能一成不变,需求变了,视图就得跟着改。PostgreSQL 支持CREATE OR REPLACE VIEWCREATE OR REPLACE VIEW order_user_view AS SELECT o.order_id, u.user_name, u.user_phone, o.amount FROM orders o JOIN users u ON o.user_id u.id;但这里有个非常容易踩的坑OR REPLACE只能给视图增加新的列,或者保持原有列不变,它绝对不能直接修改已存在列的类型,也绝对不能用它来删除某个列。比如原来视图有 3 列,你现在 SELECT 只出 2 列,执行不会成功,会报 cannot drop columns from view。为什么这么设计因为视图一旦发布出去,可能已经被下游报表、视图、甚至触发器引用,突然删列会静默破坏依赖。PostgreSQL 宁愿让你明确地 DROP VIEW 再重建,也不要让你在 OR REPLACE 里顺手把列干掉。我见过不少新手在这里反复报错,其实不用抱怨,这是数据库在替你守底线。2.3 WITH CHECK OPTION可更新视图的最后一道防线视图不仅能查,部分情况下还能自动更新。如果你在视图上执行 INSERT 或 UPDATE,PostgreSQL 默认允许把变更直接透传到底层表,只要该视图满足可自动更新的条件稍后我们讲条件。然而透传有个隐患你可能通过视图插入一条不符合视图过滤条件的数据。比如你建了一个只包含已支付订单的视图paid_orders_view,正常情况下不该往里面塞未支付订单。但数据库在默认情况下并不会阻止你这么做。这个时候就要用WITH CHECK OPTIONCREATE VIEW paid_orders_view AS SELECT id, order_no, status, amount FROM orders WHERE status PAID WITH CHECK OPTION;加上它之后,任何通过这个视图执行的 INSERT 或 UPDATE,如果结果导致该行不再满足WHERE status PAID,就会直接报错。你或许觉得我明明给了条件,数据库应该懂我,但数据库不会脑补业务意图,它只执行你显式告诉它的规则。2.4 物化视图真正把数据存下来的另类视图普通视图不存数据,物化视图恰恰相反。它的全称叫 MATERIALIZED VIEW,创建时会执行查询并把你需要的结果集实际存到磁盘上。CREATE MATERIALIZED VIEW mv_order_daily AS SELECT order_date, sum(amount) AS total_amount FROM orders GROUP BY order_date WITH DATA;WITH DATA表示立即执行并填充数据,如果换成WITH NO DATA,则只建定义不跑数据。之后你想刷新数据,用REFRESH MATERIALIZED VIEW重跑一遍查询,或者用REFRESH MATERIALIZED VIEW CONCURRENTLY实现在线无锁刷新。物化视图的代价是数据会有延迟、需要手动刷新,但它能带来实打实的查询性能提升,尤其是报表统计、聚合查询这种场景。关于它的用法,第 4 节会展开讲。2.5 权限模型security_invoker 与 security_barrierPostgreSQL 15、16 里视图权限这块有不少新东西,简单说两句。默认情况下,视图的行为是创建者权限模式用户执行视图时,底层表权限按视图创建者的权限来判断。这意味着你只要让用户能 SELECT 视图,即使他没底层表权限,也能通过视图读到数据。这也是视图能做权限裁剪的原因。但如果创建者想用视图故意规避行级安全策略,这就很危险了,于是 PG 提供了security_barrier选项CREATE VIEW user_info_secure WITH (security_barrier true) AS SELECT * FROM users;开启之后,优化器不能把外层条件随意下推到视图内部的表访问之前,从而防止恶意函数在过滤前偷看到底层数据。PostgreSQL 15 开始还提供了security_invoker模式改成调用者权限,也就是执行视图的人有多少底层表权限,他通过视图也只能看到那么多。这在多个团队共用一个视图,但彼此权限隔离的场景下非常实用CREATE VIEW user_info_invoker WITH (security_invoker true) AS SELECT * FROM users;选哪种没有绝对答案,默认security_invokerfalse适合做数据出口控制,security_invokertrue适合做多租户隔离,思想反过来了。3. 从业务出发的实战案例五段可以直接抄的 SQL3.1 案例一订单多表联查的日常封装业务里最常见的需求,是把订单主表、用户表、支付表、商品快照表一次性 JOIN 出完整信息。给你看一个我实际项目中用过很多次的封装方式CREATE OR REPLACE VIEW v_order_full AS SELECT o.id AS order_id, o.order_no, u.id AS user_id, u.user_name, p.pay_time, p.pay_status, oi.product_name, oi.price, oi.quantity, oi.price * oi.quantity AS line_amount FROM orders o JOIN users u ON o.user_id u.id LEFT JOIN payment p ON p.order_id o.id JOIN order_item oi ON oi.order_id o.id;这里有几个值得注意的细节每列都给了显式别名,下游 API 对接的时候,字段名一致性好,不容易出错。支付表用 LEFT JOIN,因为订单未必都完成了支付,用 INNER JOIN 会把未支付订单直接过滤掉,业务报表立刻就少数据。这属于看着小,坑很大的细节。金额计算放进视图字段line_amount,比在每个统计 SQL 里重复写乘法靠谱,至少口径能统一。这样建完视图之后,报表组的人不需要理解订单模型,直接SELECT * FROM v_order_full WHERE pay_time 2025-01-01就完事。本质上,这个视图相当于给业务侧提供了一套宽表或者说一个逻辑数据模型。3.2 案例二敏感信息脱敏视图用户手机号、身份证号属于敏感信息,能不给底层表权限就不要给。但在很多后台管理系统里,客服人员确实需要看到用户,又不需要看到完整手机号。脱敏视图是非常典型的解法CREATE VIEW v_user_masked AS SELECT id, user_name, left(phone, 3) || **** || right(phone, 4) AS phone_masked, left(id_card, 4) || ********** AS id_card_masked, created_at FROM users;给客服角色授权时,只授予这个视图的 SELECT 权限,同时对 users 表执行REVOKE SELECT ON users FROM customer_service_role。这样客服怎么查都碰不到明文手机号。你这个场景里,如果用户通过视图看到的脱敏数据还是能满足业务需要,那数据泄露面就被控制住了。如果有些字段无论如何都要明文,可以考虑再加一层更细的权限控制,但视图至少帮你把 90% 的无意泄露挡在了门外。3.3 案例三报表统计提速,物化视图的刷新策略统计类场景,物化视图几乎是标配。拿日活、GMV 日报举例CREATE MATERIALIZED VIEW mv_daily_report AS SELECT order_date, count(DISTINCT user_id) AS daily_uv, sum(amount) AS daily_gmv, count(*) AS order_cnt FROM orders WHERE status PAID GROUP BY order_date WITH DATA;这个查询涉及几百万行甚至更多数据,如果每次都直接去跑原始表,BI 看板打开一次就要等几十秒,用户早就没耐心了。用物化视图,把聚合结果提前算好,之后报表查询只是扫一个小表,秒出。刷新策略我一般这样定凌晨低峰期直接REFRESH MATERIALIZED VIEW mv_daily_report;。它简单粗暴,缺点是刷新期间视图上的查询会阻塞。如果报表系统 7×24 小时都有人用,那就换成REFRESH MATERIALIZED VIEW CONCURRENTLY,它要求在物化视图上有唯一索引,但刷新过程中读请求不阻塞,适合在线业务。CREATE UNIQUE INDEX idx_mv_daily_report_date ON mv_daily_report (order_date); REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_report;注意,CONCURRENTLY刷新比普通刷新慢不少,因为它得用临时空间做增量比对。所以怎么选,取决于你能不能容忍刷新期的读阻塞,而不是哪个更快。3.4 案例四递归视图解析组织架构PostgreSQL 支持递归视图,这对于树形结构的数据非常有用,比如组织架构、菜单层级、分类目录。语法上比普通视图多了一个 RECURSIVE 关键字,并且强烈建议显式声明列名CREATE RECURSIVE VIEW v_employee_tree (id, name, manager_id, depth) AS SELECT id, name, manager_id, 1 FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.id, e.name, e.manager_id, e_tree.depth 1 FROM employees e JOIN v_employee_tree e_tree ON e.manager_id e_tree.id;这段递归逻辑其实拆开看就两个部分种子部分先取出根节点manager_id 为空的人即老板递归部分反复 JOIN 自己这张视图一层一层把下级捞出来depth 记录层级深度。实际使用的时候,我一般会额外加一个排序字段或者 path 字段,因为单纯 depth 只能知道在第几层,排不出树形结构的先后顺序。不过作为入门示例,这个已经足够帮你理解递归视图的核心套路。3.5 案例五可更新视图与 WITH CHECK OPTION 的结合假设运营后台需要提供一个补录退款订单的入口,但这个入口只允许操作status REFUNDED的订单,其他订单不能碰。可以用一个可更新视图叠加 CHECK OPTION 来收口CREATE VIEW v_refund_orders AS SELECT id, order_no, user_id, amount, status FROM orders WHERE status REFUNDED WITH CHECK OPTION;之后应用程序只拿到这个视图的 INSERT、UPDATE 权限,没有 orders 表的任何权限。运营在后台改单,最多只能把状态改成同样是REFUNDED的值如果试图把某个已退款单改成别的状态,或者往里插入一条非退款状态的记录,数据库会直接报错。把业务规则下沉到数据库这一层,不管前端、后端怎么写,规则都不会被绕过。4. 视图能加快查询速度吗——这里给出能抄作业的结论4.1 普通视图不会提速,它只是宏替换热搜里经常有人问视图可以加快查询速度吗我在第 2 节已经铺垫过了普通视图不存数据,本质就是 SQL 宏。你查询视图,数据库解析时把它展开成底层查询,优化器该怎么扫表还是怎么扫表。如果底层查询本身走了全表扫描,套上视图不会让它自动变成索引扫描。举个例子,你建了个视图v_big_orders从一个 1000 万行的表里筛数据,执行计划里核心步骤依然是原表的顺序扫描或者索引扫描,和你直接写那条 SELECT 几乎没有区别。所以,如果有人告诉你我建了视图后查询快了很多,那不是视图的功劳,而是他恰好在视图里写了更合理的条件、或者底层表加了索引、或者数据库统计信息刷新后选了更好的计划。4.2 物化视图提速的原理用存储空间换查询时间真正能提速的是物化视图,因为它把查询结果预计算并落盘了。查询物化视图时,优化器面对的是一个小得多、且可能建有索引的结果集。相当于你提前把这道难题的答案抄在纸上,别人来问的时候直接看答案,不需要重新算一遍。物化视图提速的关键配套是索引。没有唯一索引或普通索引,物化视图依然可能全表扫描,只不过这个表比原始表小很多,所以通常还是能感受到明显加速。想要极致性能,就针对物化视图里被 WHERE、GROUP BY、ORDER BY 频繁用到的列建索引。4.3 视图性能优化的几个实操原则第一,控制嵌套深度。视图套视图尽量不要超过两层,一旦超过,执行计划会变得非常复杂,而且排错时你很难判断到底是哪一层查询写得有问题。第二,把条件尽量写在物理表那一层。不要在视图里 SELECT 全量数据,然后外层再 WHERE 筛选。视图内部如果能提前过滤掉大部分数据,外层查询压力小得多。同理,视图里的 JOIN 也要尽量走索引。第三,对物化视图刷新频率要有预期。数据实时性要求高的场景不适合用物化视图,比如实时候补库存这种业务,你搬个延迟 10 分钟的数据上去,用户会投诉的。物化视图适合分钟级、小时级、天级延迟都没关系的场景。5. 创建视图权限不足怎么破权限模型、更新限制与排查速查5.1 创建视图权限不足的三种常见原因很多人在自己的机器上用超级用户 postgres 登录,建视图一切正常,一到公司测试环境用业务账号建视图,就报 permission denied。根据 PostgreSQL 的权限模型,创建视图至少要满足以下条件你需要在目标 schema 上有 CREATE 权限。比如默认publicschema,如果管理员收紧了 public 的 CREATE 权限,普通用户就建不了视图。公司环境很常见,因为安全加固会把 public schema 的默认 CREATE 权限收掉。视图查询里涉及的所有基础表,你都必须有对应的 SELECT 权限如果需要更新,还得有 INSERT/UPDATE。如果你用 OR REPLACE 替换别人建的视图,你还得是属主或者有相应权限。排查时先看 schema 权限SELECT grantee, privilege_type FROM information_schema.schema_privileges WHERE schema_name public;再看表权限SELECT grantee, table_name, privilege_type FROM information_schema.table_privileges WHERE table_name IN (orders, users);通常缺什么补什么,用管理员账号执行类似下面的语句GRANT USAGE, CREATE ON SCHEMA public TO app_user; GRANT SELECT ON orders, users TO app_user;之后业务账号就能建视图了。前提是这符合你的权限规范,别为省事直接给业务账号开了超级用户。5.2 视图无法自动更新条件和绕行方案PostgreSQL 里不是所有视图都能自动更新,官方条件是视图必须基于单个基础表或可更新视图,不能有 GROUP BY、DISTINCT、聚合函数、UNION、LIMIT、OFFSET 等会让行映射变模糊的东西。底层列也不能是表达式派生出来的,必须能明确映射回物理表的原始列。如果你的视图比较复杂,又想允许通过它写数据,可以考虑 INSTEAD OF 触发器。举个例子CREATE TRIGGER trg_v_complex_insert INSTEAD OF INSERT ON v_complex_view FOR EACH ROW EXECUTE FUNCTION fn_handle_insert();这个触发器拦截插入操作,转而在触发器函数里写多条逻辑。这样你依然能在应用层面向视图操作,底层怎么落库由你控制,是复杂视图可写性的标准解法。5.3 视图使用踩坑速查表问题现象核心原因推荐解法创建视图报 permission deniedschema 或基础表缺权限按 5.1 逐项排查,补齐 USAGE/CREATE/SELECTOR REPLACE 报 cannot drop columns视图删了输出列必须 DROP VIEW 后重建,并检查下游依赖视图查询比直接查表还慢嵌套太深、条件没下推、统计信息旧减少嵌套、内层过滤、ANALYZE 刷新统计信息通过视图插入数据报错底层是可更新视图,但条件不通过确认是否满足可更新条件,必要时用 INSTEAD OF 触发器物化视图刷新阻塞查询普通 REFRESH 持有锁建唯一索引后用 REFRESH MATERIALIZED VIEW CONCURRENTLY视图里 ORDER BY 不生效视图不保证输出顺序外层查询显式 ORDER BY视图能读出底层敏感字段只授权了视图,没检查底层表权限链确认是否希望创建者权限暴露数据,考虑 security_barrier / security_invoker这张表我建议你收藏,基本覆盖了日常工作 80% 的视图坑。每次遇到视图问题,先对照着自查一遍,比自己瞎试快很多。5.4 一个补充PostgreSQL 版本选择的现实建议虽然这篇笔记围绕 PostgreSQL 16 写,但热搜里还有很多人问postgresql 下载哪个版本。我的建议是如果你是学习者,直接装 16 或者 17 都行,语法层面差异不大如果是生产环境,跟着你所在云厂商或发行版的默认版本走,不一定最新,但要稳定。至于 PostgreSQL 16 便携版,做个本地实验、临时体验新特性确实方便,但不建议拿到生产环境用,因为便携版的目录结构、服务注册、自动启停等都和标准安装不一样,出了问题不太好排查。也有很多人在 Docker 里跑 PostgreSQL,这个没问题,只是要注意容器内同样遵循完整的权限模型,容器封装不会替你把权限问题解决掉。最后说点个人经验做数据库这行越久,我越觉得视图不是一个建完就完的对象,它是数据模型、权限体系和查询性能三方博弈的十字路口。普通视图不能加速,但它能封装复杂度、收敛权限物化视图可以加速,但它有数据新鲜度和存储成本的代价权限模型又决定了视图到底是一道门还是一个后门。我在实际项目中踩过不少坑,印象最深的不是复杂语法,反而是那些看起来特别基础的东西——比如 OR REPLACE 不能删列、CONCURRENTLY 刷新必须有唯一索引、安全屏障视图要配合行级安全策略才真正有效。如果你只是刚接触到视图,我建议先把普通视图、物化视图、WITH CHECK OPTION 这几个点练熟,再逐步深入权限和触发器层面的玩法。数据库的学习没有捷径,但把每个概念背后的取舍想清楚,后面遇到问题时,你会比别人少走很多弯路。
返回列表