当前位置: 首页 > news >正文

DeepSeek总结的关于 PostgreSQL 视图的强硬观点(下)

来源:https://boringsql.com/posts/strong-views/

提示建议使用CASCADE

ALTERTABLEcustomersDROPCOLUMNnameCASCADE;NOTICE:dropcascadesto3other objects DETAIL:dropcascadestoviewactive_customersdropcascadestoviewcustomer_ordersdropcascadestoviewcustomer_summary

所有三个视图都被删除,而非修改,连同它们的权限、RLS 策略以及任何其他依赖对象。在具有数十个相互连接的视图的模式上,CASCADE是一场噩梦。

永远不要在没有准备好完整重建脚本的情况下,在生产环境的视图上使用CASCADE

CASCADE不会修改视图,它会删除它们。所有的GRANT、行级安全策略以及下游依赖都会随之消失。没有撤销操作。

因此,手动路径是:保存每个视图定义,按依赖关系的逆序删除它们,修改表,按依赖关系的顺序重新创建它们,重新应用所有权限。对于三个视图来说,这很繁琐。对于跨越多个模式的三十个视图来说,这是一个完整的迁移项目。

-- 1. 保存定义SELECTpg_get_viewdef('customer_summary',true);SELECTpg_get_viewdef('customer_orders',true);SELECTpg_get_viewdef('active_customers',true);-- 2. 按叶子优先的顺序删除DROPVIEWcustomer_summary;DROPVIEWcustomer_orders;DROPVIEWactive_customers;-- 3. 现在你可以修改表了ALTERTABLEcustomersDROPCOLUMNname;-- 4. 按正确顺序重新创建CREATEVIEWactive_customersASSELECTid,email,status,last_login_at,deleted_atFROMcustomersWHEREdeleted_atISNULLANDstatus='active'ANDlast_login_at>now()-interval'90 days';CREATEVIEWcustomer_ordersAS...;CREATEVIEWcustomer_summaryAS...;-- 5. 重新应用权限等-- ...

SELECT *的陷阱

你可能会认为SELECT *可以让你免于列级依赖。但事实并非如此。它会让情况变得更糟。

CREATEVIEWall_customersASSELECT*FROMcustomers;

这看起来很灵活。PostgreSQL 在视图创建时展开SELECT *并冻结结果。检查数据库实际存储的内容:

SELECTpg_get_viewdef('all_customers'::regclass,true);SELECTid,email,name,status,last_login_at,deleted_atFROMcustomers;

*被展开为视图创建时存在的列。现在向基表添加一列:

ALTERTABLEcustomersADDCOLUMNphoneTEXT;SELECT*FROMall_customers;

phone列不在那里。视图仍然返回原来的列。要获取新列,你必须执行CREATE OR REPLACE VIEW all_customers AS SELECT * FROM customers,这将根据当前的表定义重新展开*

删除原始展开中的某一列,迁移会以与使用显式列列表相同的方式被阻止,只不过现在依赖关系隐藏在目录中,而不是在视图主体中可见。SELECT *在制造相同刚性耦合的同时,却给人一种灵活的错觉。

这实际上是有文档记录的行为,并且遵循 SQL 标准。但几乎每个人第一次遇到时都会感到惊讶,并且它是许多"我的视图缺少列"的错误报告的根源。

始终在视图中使用显式的列列表。至少这样依赖关系是可见的,并且破坏是可预测的。

依赖关系的墙、类型 OID 的耦合、冻结的SELECT *展开:所有这些都是相同的权衡,并且一致地应用。刚性是特性,而非缺陷。

为什么这不是 PostgreSQL 的 bug

这种刚性是一种设计选择,而不是缺陷。其他数据库做出了不同的选择,但每一种都不是没有代价的。

  • Oracle将依赖的视图标记为INVALID,而不是阻塞 DDL。视图会在下次访问时自动重新编译。如果还能工作,很好;如果不能,你会在查询时得到一个错误。这听起来更好,直到你意识到它在实践中意味着什么:你可以部署一个迁移,得到一个干净的退出码,直到周一早上用户访问时才发现一个关键的报表视图已经损坏。破坏从迁移时(你在关注)转移到了运行时(你可能没在关注)。对于重视部署信心的团队来说,这可以说更糟糕。
  • SQL Serversp_refreshview,它根据当前的表定义重新编译视图的元数据。你可以修改表,然后刷新依赖的视图以获取更改。但是sp_refreshview一次只处理一个视图。没有内置的方法可以按正确顺序刷新整个依赖链。而且,如果视图引用了一个已删除的列,sp_refreshview会失败;它不会帮你移除引用。你仍然需要手动编辑并重新创建视图。这是一个便利性功能,而非解决方案。

PostgreSQL 选择了编译时安全:生产环境中没有意外的INVALID视图,也没有懒惰的重新编译掩盖结构变化而导致的静默错误结果。其代价是每次模式更改都需要手动管理依赖关系,这个代价高到足以让有经验的团队完全放弃视图。

救命稻草:事务性 DDL

在讨论变通方法之前,有一个 PostgreSQL 特性实际上改变了风险评估:DDL 是事务性的。整个删除-修改-重建-重新授权的序列可以放在一个BEGIN/COMMIT内部:

BEGIN;DROPVIEWcustomer_summary;DROPVIEWcustomer_orders;DROPVIEWactive_customers;ALTERTABLEcustomersDROPCOLUMNname;CREATEVIEWactive_customersAS...;CREATEVIEWcustomer_ordersAS...;CREATEVIEWcustomer_summaryAS...;GRANTSELECTONcustomer_summaryTOreporting;COMMIT;

如果任何语句出错(重新创建的视图中有错别字、缺少授权、意外的依赖关系),整个迁移就会回滚,数据库会恢复到原来的状态。Oracle 和 SQL Server 对大多数 DDL 都无法做到这一点;它们的CREATE VIEW会自动提交,因此部分失败会使你处于一个半迁移的模式,没有ROLLBACK可以依靠。PostgreSQL 的“你必须删除并重建所有东西”的痛苦是真实的,但其恢复机制比 Oracle 或 SQL Server 要好得多。

这并没有解决锁定问题。DROP VIEWCREATE VIEW在视图上获取AccessExclusiveLockALTER TABLE在表上获取一个。在事务期间,任何访问这些对象的操作都会被阻塞。在繁忙的系统上,一个长时间运行的视图重建事务就是一个停止世界的窗口。保持事务紧凑:在事务外部准备好新的 SQL,不要持有其他锁,并且不要在流量高峰期间运行。事务性 DDL 给你带来的是安全性,而不是并发性。

变通方法

这些方法都不能解决根本问题;它们只是使其可管理。

  • 完全避免视图。用应用程序级别的查询构建器或 ORM 作用域替换视图。你失去了共享抽象:当三个服务都需要“活跃客户”的相同定义时,每个服务都会独立实现它。定义会逐渐偏离。对于能够完全控制小型代码库的团队来说,这是一个合理的权衡。对于共享数据平台,则不然。
  • 编写脚本进行重建。使用pg_get_viewdef()提取定义,然后编写迁移脚本,在模式更改时删除并重建视图。问题在于:没有影响分析(你必须自己弄清楚依赖顺序),没有权限保留(视图定义不包括权限),并且无法扩展到几个视图之外。你的迁移框架在这里也帮不上忙。它们按顺序运行你的 SQL 文件,但弄清楚要写什么 SQL 完全取决于你。
  • 对视图进行版本管理。不要就地修改视图,而是在active_customers_v1旁边创建active_customers_v2。逐个迁移消费者。当没有东西使用时,删除旧版本。当视图被多个独立团队使用时,这很有效,因为你无法强制每个人都在同一个部署窗口内迁移。代价是命名规范和版本的激增:如果customer_orders_v1依赖于active_customers_v1,那么你现在也需要customer_orders_v2。对于面向公共的 API,基于模式的版本控制(CREATE SCHEMA api_v2; CREATE VIEW api_v2.active_customers AS ...)比使用后缀更清晰。它为你提供了一个自然的命名空间,并允许你通过SET search_path来切换版本。
  • 自己查询pg_dependPostgreSQL 在pg_depend系统目录中跟踪所有对象依赖关系。问题是:视图并不直接依赖于表。实现视图的重写规则才依赖,因此每次遍历都需要经过pg_depend -> pg_rewrite -> pg_class。直接依赖于customers的视图:
-- 一个查询直接依赖者的 SQL 示例SELECTdepns.nspnameASdependent_schema,depc.relnameASdependent_view,COALESCE((SELECTa.attnameFROMpg_attribute aWHEREa.attrelid=d.refobjidANDa.attnum=d.refobjsubid),'*')ASsource_columnFROMpg_depend dJOINpg_rewrite rONr.oid=d.objidJOINpg_class depcONdepc.oid=r.ev_classJOINpg_namespace depnsONdepns.oid=depc.relnamespaceWHEREd.refobjid='customers'::regclassANDd.classid='pg_rewrite'::regclassANDd.refclassid='pg_class'::regclassANDdepc.relkindIN('v','m')ANDdepc.oid<>'customers'::regclassORDERBYdepns.nspname,depc.relname;

这很有用,但只能跳一步。customer_summary通过两个中间视图间接依赖于customers,这个查询会遗漏。一个递归 CTE 可以遍历整个链:

-- 一个用于递归查找所有依赖视图的 SQL 示例WITHRECURSIVE view_depsAS(-- direct dependents of the target tableSELECTDISTINCTdepc.oidASview_oid,depns.nspname||'.'||depc.relnameASview_name,1ASdepthFROMpg_depend dJOINpg_rewrite rONr.oid=d.objidJOINpg_class depcONdepc.oid=r.ev_classJOINpg_namespace depnsONdepns.oid=depc.relnamespaceWHEREd.refobjid='customers'::regclassANDd.classid='pg_rewrite'::regclassANDdepc.relkindIN('v','m')UNION-- views depending on views we already foundSELECTdepc.oid,depns.nspname||'.'||depc.relname,vd.depth+1FROMview_deps vdJOINpg_depend dONd.refobjid=vd.view_oidJOINpg_rewrite rONr.oid=d.objidJOINpg_class depcONdepc.oid=r.ev_classJOINpg_namespace depnsONdepns.oid=depc.relnamespaceWHEREd.classid='pg_rewrite'::regclassANDdepc.relkindIN('v','m')ANDdepc.oid<>vd.view_oid-- skip the view's own _RETURN rule)SELECTview_name,MIN(depth)ASdepthFROMview_depsGROUPBYview_nameORDERBYdepth,view_name;

这能工作,但到了这一步,你实际上已经在编写一个依赖分析工具了。再加上拓扑排序、权限捕获、物化视图处理、RLS 策略保留,你就有了一个小产品。

  • 物化视图(Materialized Views)的情况更糟。以上所有都适用,此外,在DROP时你还会丢失缓存的结果集。重建后,REFRESH会从头开始重建:对于大型数据集,这需要几分钟到几小时,并且在存在唯一索引之前没有CONCURRENTLY选项。对于支撑仪表板的物化视图,这意味着停机时间。

pg_dump中隐藏的已有实现

手动变通方法一直在重新发明的机制——在保留视图身份、权限和依赖关系的同时重写其主体——其实已经存在于 PostgreSQL 内部。它恰好存在于pg_dump中,只在需要时使用。

大多数情况下,pg_dump会做显而易见的事情:构建一个依赖图,对其进行拓扑排序,然后按基表优先的顺序使用普通的CREATE VIEW发出每个视图。转储我们的四个视图链,你得到的就是这个。

有趣的情况发生在排序失败时。循环不常见但确实存在:一个视图主体调用了一个函数,而该函数的主体又反向引用了该视图;基表上的一个触发器通过读取该基表的视图进行读取;交叉引用的物化视图与指向视图的 RLS 策略。当pg_dump遇到这种情况时,它会回退到占位符视图技巧:提前发出其中一个视图,作为一个具有正确列列表和类型但主体为空的存根,然后在循环的其余部分存在后,使用CREATE OR REPLACE VIEW回来安装真正的定义。

-- 提前发出,作为一个占位符CREATEVIEWcustomer_summaryASSELECTNULL::integerASid,NULL::textASemail,NULL::textASname,NULL::bigintASorders_12mo,NULL::bigintASrevenue_12mo_cents;-- 稍后发出,一旦依赖存在CREATEORREPLACEVIEWcustomer_summaryASSELECTco.id,co.email,co.name,COUNT(co.order_id)ASorders_12mo,COALESCE(SUM(co.total_cents),0)ASrevenue_12mo_centsFROMcustomer_orders coGROUPBYco.id,co.email,co.name;

权限、注释和策略会附加到存根上,并在重写后保留,因为 OID 从未改变。CREATE OR REPLACE VIEW在原地修改同一个pg_class行。多年来,PostgreSQL 在每次pg_dump --schema-only时都在做这件事。这个机制是存在的;只是没有作为面向用户的 DDL 暴露出来。

在你使用视图之前

视图值得使用。抽象是真实的:共享的定义、清晰的分层、列级安全,并且在规划器可以内联它们时没有运行时成本。问题是,所有这些刚性从外部都是不可见的。视图在目录中、在\d中、在 ORM 中、在访问它的每个查询中,看起来都像表。其拆解成本只有在第一次有人试图删除一列、加宽一种类型,或者在周五下午试图用CASCADE摆脱迁移时才会显现出来。

因此,在你使用视图之前,请牢记这些权衡:

  • 视图主体中的SELECT *是一个陷阱。它在创建时冻结列列表,将依赖关系隐藏在目录中,并且仍然会像显式列列表一样阻塞相同的 DDL。始终把列写出来。
  • 每一层都会使拆解成本倍增。一个三层的视图链意味着,对于底层列的任何结构性更改,都需要按正确顺序进行三次删除、三次重建、三组权限和策略的重新应用。保持依赖树浅层,并诚实地评估抽象是否物有所值。
  • CASCADE不是一个修复方案。它会删除依赖的视图以及它们的权限、RLS 策略和下游依赖,且没有撤销操作。在没有准备好重建脚本的情况下,永远不要在生产环境中运行它
  • 当迁移最终必须发生时,有两件事可以让这个循环变得可承受。将整个删除-修改-重建-重新授权序列包装在一个事务中,这样拼写错误或遗漏的授权就能干净地回滚,并且保持该事务紧凑——AccessExclusiveLock在其持续期间会阻塞其他一切。然后,在动手之前映射依赖图:通过pg_rewrite连接pg_depend,用递归 CTE 遍历,它会告诉你真正会破坏什么。在迁移之前运行这个查询,而不是在事故之后。

这种痛苦可以追溯到一个缺失的原语。今天的ALTER VIEW可以处理重命名、所有者更改、模式更改、列默认值和security_barrier等选项,但没有任何结构性操作CREATE OR REPLACE VIEW只能在末尾追加列,仅此而已。一个真正的ALTER VIEW DROP COLUMNADD COLUMNALTER COLUMN TYPE才能使视图能够安全地演化。目录和原地重写机制已经存在,正如pg_dump的占位符技巧所证明的那样;缺少的是面向用户的 DDL。

即使没有它,视图仍然值得使用。只是不要假装它们是表

http://www.jsqmd.com/news/797844/

相关文章:

  • Google DeepMind 重大更新 Gemini API File Search:多模态、元数据过滤与页码引用齐上阵
  • 2026年4月行业内优质的双相钢管生产厂家推荐,不锈钢管/换热管/AP管/双相钢管/焊管/厚壁管,双相钢管公司找哪家 - 品牌推荐师
  • 如何快速掌握WindowResizer:终极窗口强制调整工具完整指南
  • 北京家长必看:低预算留学怎么“花小钱办大事”?朝海教育有答案 - GrowthUME
  • 可调电源设计:三种输出电压调节方案原理与实战解析
  • 本地AI代码助手Letta:私有化部署、离线可用的开发效率利器
  • Python 爬虫数据处理:爬取数据关联关系挖掘实战
  • 2026年高权威GEO公司TOP5排行榜单:按综合实力客观评测推荐,附GEO优化实战效果验证 - GrowthUME
  • 2026 洛阳家装机构实测呈现:五家本土装企服务信息与流程记录 - GrowthUME
  • 涿州老王匠全屋定制:中高端品质 工厂直供价格 - GrowthUME
  • LSLib终极指南:从游戏文件编辑到MOD制作完整教程
  • 霓虹深渊2修改器2026最新版23项功能
  • 如何通过内存注入技术解锁《原神》帧率限制
  • 解锁Perplexity Science未公开API接口:科研团队私密部署+本地化期刊索引增强方案(仅限前200位订阅者获取)
  • 用STC8A的硬件PWM驱动循迹小车:一份超详细的电机控制与传感器融合代码解析
  • 维普大更新后如何降低ai率?5款降ai率工具防坑测评 - 殷念写论文
  • 3步彻底解决MacBook电源管理的3个核心痛点:SleeperX智能睡眠控制方案
  • 别再凭感觉选电机了!手把手教你用Excel搞定丝杆和同步带的惯量计算(附模板)
  • 不止于点亮屏幕:深度解析NCS8803芯片的AUX通道与EDP通道调试,解决‘偶尔能通’的玄学问题
  • AI驱动电力系统优化:从碳排放到健康影响的内生化决策
  • SteamAutoCrack终极指南:如何免Steam启动游戏,3大核心技术深度解析
  • 前端学习打卡 Day 7: 综合实战案例 | 人气美食推荐馆网页制作
  • 别再死记CTL公式了!用UPPAAL三个实战案例,带你玩转模型验证
  • 秦皇岛特色餐饮实地探访:5 家门店客观信息实录 - GrowthUME
  • Cesium三维地形剖切与开挖:从原理到可复用组件封装
  • 别再只会Range赋值了!VBA二维数组的3种高效创建方法(含嵌套数组转换)
  • 为什么92%的AI团队在K8s上卡在vLLM部署阶段?:SITS 2026专家团复盘的4个反模式与1套可审计CI/CD流水线模板
  • 期刊推荐:International Journal of Foundations of Computer Science(ISSN: 0129-0541)
  • 3分钟学会:B站缓存视频永久保存的完整解决方案
  • 避开这些坑!MATLAB C Mex S函数调试与性能优化实战指南