Chris van Eijk:PostgreSQL 中的 CREATE POLICY IF NOT EXISTS:可行方案

Chris van Eijk:PostgreSQL 中的 CREATE POLICY IF NOT EXISTS:可行方案

💡 原文英文,约1900词,阅读约需7分钟。
📝

内容提要

PostgreSQL 17/18 不支持 CREATE POLICY IF NOT EXISTS 或 CREATE OR REPLACE POLICY,可重复迁移策略有三种:一是在同一事务中先 DROP POLICY IF EXISTS 再 CREATE POLICY,能确保定义正确,但 DROP 会取 ACCESS EXCLUSIVE 锁,事务外执行会出现无策略空窗,FORCE 下所有租户查不到数据;二是用 DO 块查 pg_policies 再创建,适合只建不改的策略,并发部署需加 advisory 锁;三是仅改表达式或角色时用 ALTER POLICY,但同样取 ACCESS EXCLUSIVE 锁且策略不存在时会失败。建议配合 lock_timeout。

🔎

延伸解读

为什么没有 IF NOT EXISTS:定义冲突才是难点

PostgreSQL 未提供 CREATE POLICY IF NOT EXISTS,并非遗漏。Álvaro Herrera 指出,若策略已存在但定义不同,IF NOT EXISTS 只会静默跳过,无法解决迁移真正要处理的“旧定义”问题。因此,三种方案的核心差异在于:是否保证最终策略与迁移描述一致。DO 块只检查名称,不检查定义,适合只建不改的场景;若策略可能已存在且定义过时,必须用 DROP 后重建。

事务内 DROP+CREATE 的代价:锁与等待

将 DROP POLICY IF EXISTS 与 CREATE POLICY 放在同一事务中,能避免无策略空窗,但 DROP 会取 ACCESS EXCLUSIVE 锁并持有到提交。实测中,应用查询需等待整个迁移事务结束,其他新查询也会排队。因此该方案必须配合 lock_timeout 和重试,否则一个慢查询就可能让迁移失败或阻塞所有租户。事务外执行则会出现空窗,FORCE 下所有租户查不到数据。

并发部署下的 DO 块竞态与 advisory 锁

DO 块检查 pg_policies 再创建,看似接近 IF NOT EXISTS,但两个部署同时运行时存在竞态:双方都查不到策略,先创建者成功,后者的 CREATE POLICY 会等待并最终因策略已存在而报错,导致整个事务回滚。文章实测表明,在 DO 块开头加 PERFORM pg_advisory_xact_lock(hashtext('policy:invoices')) 可让第二个会话等待锁后重新检查并跳过,两个事务均提交。并发部署时应加此锁。

ALTER POLICY 的适用边界:省空窗不省锁

ALTER POLICY 可原地修改 USING、WITH CHECK、TO 角色和重命名,但无法改变命令类型或 permissive/restrictive 属性,这些仍需 DROP 后重建。它同样取 ACCESS EXCLUSIVE 锁,只避免空窗,不避免阻塞;且策略不存在时会失败,单独使用不可重复。因此仅适用于迁移明确知道策略已存在、且只改表达式或角色的场景,并同样建议设置 lock_timeout。

❓

Q&A

PostgreSQL 支持 CREATE POLICY IF NOT EXISTS 吗?

不支持。在 PostgreSQL 17 和 18 上,CREATE POLICY IF NOT EXISTS 和 CREATE OR REPLACE POLICY 都会报语法错误(SQLSTATE 42601),19 的候选版本和开发版文档中也没有这两个语法。只有 DROP POLICY 支持 IF EXISTS 子句。

在同一个事务里先 DROP POLICY IF EXISTS 再 CREATE POLICY 有什么风险?

该模式能确保策略最终定义正确,但 DROP POLICY 会取 ACCESS EXCLUSIVE 锁并持有到提交,期间所有查询排队等待。若不在同一事务中执行,两条语句之间会出现无策略空窗:在 FORCE ROW LEVEL SECURITY 下所有租户都查不到数据(默认拒绝),插入也会失败;没有 FORCE 时所有者会看到全部数据。建议配合 lock_timeout 使用。

用 DO 块检查 pg_policies 再创建策略,能替代 IF NOT EXISTS 吗?

这是最接近 IF NOT EXISTS 的做法,但它只检查策略名是否存在,不检查定义。若同名策略已存在但 USING 子句不同,DO 块会静默跳过,不会更新。适合只创建、从不修改的策略。并发部署时两个会话可能同时通过检查,后一个 CREATE POLICY 会因策略已存在而失败并中止事务,可在块首加 pg_advisory_xact_lock 避免。

ALTER POLICY 能修改策略的哪些部分?会加锁吗?

ALTER POLICY 可以就地修改 USING 表达式、WITH CHECK 表达式、适用角色(TO)和策略名(RENAME TO),但不能修改命令类型(如 FOR SELECT)或 permissive/restrictive 属性,这些必须删除重建。它同样取 ACCESS EXCLUSIVE 锁,只是避免了空窗;且策略不存在时会失败,单独使用不可重复执行。

策略变更时 ACCESS EXCLUSIVE 锁会阻塞查询多久?

ACCESS EXCLUSIVE 与所有操作冲突,包括普通 SELECT。实测中,一个运行 5 秒的读查询会让随后启动的 CREATE POLICY 等待 4.0 秒;设置 lock_timeout = '1s' 后会在 1 秒后报锁超时(SQLSTATE 55P03)。等待期间,表上所有新查询都会排队,一个慢报表可能拖住所有租户的查询,因此迁移应设置 lock_timeout 并重试。

迁移中启用行级安全(ENABLE/FORCE ROW LEVEL SECURITY)和创建策略是原子的吗?

是。在同一事务中执行 ENABLE ROW LEVEL SECURITY、FORCE ROW LEVEL SECURITY 和 CREATE POLICY 后 ROLLBACK,relrowsecurity 和 relforcerowsecurity 均恢复为 false,表上也没有策略。因此顺序只在事务外才重要:先提交 ENABLE 而策略未建会隐藏所有行;先提交策略再 ENABLE 则策略暂不生效。

可重复执行的策略迁移应该选哪种方案?

策略只创建一次且不变:用 DO 块检查 pg_policies,若可能并发部署则加 advisory 锁。策略可能已存在但定义不同:在同一事务中 DROP POLICY IF EXISTS 再 CREATE POLICY,并设置 lock_timeout,这是唯一能保证最终定义正确的方案,同时需检查表上其他策略。仅表达式或角色变化:用 ALTER POLICY,但前提是策略已存在。无论哪种,都要确认迁移工具如何划分事务。

🏷️

标签

➡️

继续阅读