PostgreSQL码农集散地

PostgreSQL 社区罕见妥协

本期播客

PostgreSQL 19 妥协了, CHECK 约束可以“开关”了

PostgreSQL 19 推出重磅特性:CHECK 约束终于可以“开关”了!数据迁移告别痛苦。但这也是罕见妥协功能,按PG的哲学,怎么能允许这种“不靠谱”的功能呢?

约束的本质,是为了保证数据一致性,而不是成为 DBA 的枷锁。

你是否经历过这样的场景:需要将一批历史数据导入一张生产表,但表上有一个 CHECK 约束(例如 price > 0),而历史数据中包含少量不满足条件的脏数据。你陷入两难——要么花几个小时清洗数据,要么删除约束、导入数据、再重建约束。更痛苦的是,重建约束时必须记住原来的定义,稍有不慎就会出错。

这种折磨,在 PostgreSQL 19 中彻底终结。Andrew Dunstan 提交的 342051d 补丁,为 CHECK 约束带来了 ALTER CONSTRAINT ... [NOT] ENFORCED 功能。从此,CHECK 约束也可以像外键一样灵活“开关”了。

PostgreSQL 2026 年度大戏来了, 扫海报中的二维码报名, 选择早鸟或通票(都含午餐和周边礼品), 可私信我要优惠码, 数量有限先到先得!

图片

第一性原理:为什么 CHECK 约束需要“可开关”?

让我们回到约束的本质。CHECK 约束是保证数据完整性的重要工具,它确保表中的每一行都满足指定的条件。但在现实运维中,我们有时需要临时放宽这种保证——例如批量数据加载、数据清洗、应用升级等场景。

在 PostgreSQL 19 之前,只有外键约束支持 ALTER CONSTRAINT ... [NOT] ENFORCED,可以临时禁用外键检查。而 CHECK 约束却没有这个能力。如果你需要临时禁用 CHECK 约束,唯一的选择是:

-- 删除约束  
ALTERTABLE products DROPCONSTRAINT price_check;  

-- 导入数据...  

-- 重建约束(必须记住原始定义)  
ALTERTABLE products ADDCONSTRAINT price_check CHECK (price > 0);  

这个方案有三个致命缺陷:

  1. 约束定义丢失风险:你必须精确记住原来的 CHECK 条件,一旦写错,可能引入逻辑错误。
  2. 锁表时间更长:DROP CONSTRAINT 和 ADD CONSTRAINT 都需要对表加锁,而且 ADD CONSTRAINT 会全表扫描验证现有数据(除非指定 NOT VALID)。
  3. 依赖关系破坏:如果有视图或函数依赖这个约束,删除可能导致它们失效。

第一性原理告诉我们:约束的“开关”和“定义”应该是正交的。删除并重建约束,是将两个操作强行耦合在一起,违背了关注点分离的原则。

破局者:ALTER CONSTRAINT ... [NOT] ENFORCED for CHECK

这个补丁为 CHECK 约束带来了与现有外键约束一致的语法:

-- 临时禁用 CHECK 约束(不检查新数据,也不验证已有数据)  
ALTERTABLE products ALTERCONSTRAINT price_check NOTENFORCED;  

-- 批量导入数据(约束不检查,性能提升)  
COPY products FROM 'historical_data.csv';  

-- 重新启用约束,并自动验证所有数据  
ALTERTABLE products ALTERCONSTRAINT price_check ENFORCED;  

当从 NOT ENFORCED 切换到 ENFORCED 时,PostgreSQL 会执行全表扫描,验证表中所有现有行是否满足约束条件。如果发现违规行,操作将失败并回滚,确保约束被完整满足。

关键设计:

  • 对于分区表和继承表,操作会递归到所有子表。当设置为 ENFORCED 时,每个子表都会被扫描验证。
  • 当设置为 NOT ENFORCED 时,即使父表已经处于 NOT ENFORCED 状态,操作也会递归到所有子表,确保子表也切换到禁用状态。

权威数据:这个特性能为 DBA 省多少时间?

让我们分析一个典型的数据仓库维护场景:

某电商公司的 orders 表有 5 亿行数据,包含一个 CHECK 约束 order_amount > 0。每个月需要导入一批历史订单数据(约 5000 万行),这些历史数据来自旧系统,其中约有 0.1% 的订单金额为 0(历史系统允许)。

旧方案:删除并重建约束

  1. 记录约束定义:pg_constraint 中查询定义,耗时 1 分钟。
  2. 删除约束:ALTER TABLE ... DROP CONSTRAINT,耗时 0.1 秒,但需要获取排他锁。
  3. 导入数据:COPY 导入 5000 万行,耗时 30 分钟。
  4. 重建约束:ALTER TABLE ... ADD CONSTRAINT,全表扫描验证 5 亿行 + 5000 万新行,耗时 60 分钟。
  5. 处理失败:如果验证失败(因为有 0 金额的行),操作回滚,前功尽弃。你需要先清洗数据或标记异常行,再重试。

总耗时:约 91 分钟,且面临回滚风险。

新方案:ALTER ENFORCED

  1. 临时禁用约束:ALTER TABLE ... ALTER CONSTRAINT ... NOT ENFORCED,耗时 0.1 秒,锁表时间极短。
  2. 导入数据:COPY 导入 5000 万行,耗时 30 分钟(与之前相同)。
  3. 重新启用并验证:ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED,全表扫描验证 5 亿行 + 5000 万新行,耗时 60 分钟。如果有违规行,操作失败,但约束仍处于禁用状态,表可正常访问。你可以查询违规行(例如使用 EXCEPT 查询),清洗后再次尝试启用。

总耗时:约 90 分钟,与旧方案相近。但关键优势在于:

  • 约束定义保留:无需手动重建,消除了定义错误的风险。
  • 失败可恢复:验证失败后,表仍可正常使用(约束未强制),你可以逐步修复数据。
  • 锁表时间短:禁用和启用操作本身只短暂锁表,大部分时间表是可读写的(除了验证阶段的共享锁,但旧方案重建时也需要共享锁验证)。

分区表场景的放大效应

假设 orders 是按月分区的分区表,包含 60 个分区。旧方案需要逐个分区处理约束,脚本复杂且易出错。新方案一次 ALTER CONSTRAINT 递归到所有分区,统一管理,操作原子性保证所有分区要么同时启用,要么全部失败。

对于 60 个分区,手动编写脚本处理每个分区的约束,需要约 2 小时。而新方案只需一条 SQL,节省 100 倍的时间。

第一性原理的边界:什么时候这个功能会失效?

任何功能都有前提。ALTER CONSTRAINT ENFORCED 的核心代价是:全表扫描验证。当这个代价超出业务容忍度时,我们需要寻找替代方案。

崩塌场景 1:超大规模表(TB 级)

对于数十 TB 的表,全表扫描可能需要数小时甚至数天,无法在维护窗口内完成。

应对策略:

  • 分批验证:通过逻辑复制,先在新表上启用约束,然后逐步切换流量。
  • 使用 NOT VALID 选项:如果只是希望新数据满足约束,旧数据可以暂时容忍,可以使用 ADD CONSTRAINT ... NOT VALID 后跟 VALIDATE CONSTRAINT。但 ALTER CONSTRAINT 目前没有直接等价物,你可以通过先删除重建约束来实现类似效果。
  • 考虑数据归档:将历史数据移出主表,只对热数据保持严格约束。

崩塌场景 2:约束验证需要复杂逻辑

如果你的 CHECK 约束包含调用用户自定义函数,而该函数在执行期间有副作用或需要特殊权限,全表扫描时可能遇到问题。

应对策略:确保 CHECK 约束中的函数是 IMMUTABLE 的,且没有副作用。在启用约束前,先在测试环境验证。

崩塌场景 3:频繁切换约束状态

如果业务需要频繁地禁用/启用约束(例如每小时一次的批量导入),每次启用时的全表扫描将无法承受。

应对策略:考虑使用临时表或外部表,在外部完成数据清洗后再导入。或者,在业务低峰期一次性导入,并利用 NOT ENFORCED 模式持续多天导入,最后在周末维护窗口统一启用验证。

崩塌场景 4:触发器或规则依赖

如果存在依赖约束的触发器或规则,禁用约束可能导致它们逻辑失效。

应对策略:检查所有依赖关系(pg_depend),确保理解禁用约束的影响。在测试环境充分验证。

DBA 的行动指南

1. 识别可优化的维护任务

审查你的数据加载和迁移脚本,查找那些需要临时绕过 CHECK 约束的场景。如果看到 DROP CONSTRAINT 后跟 ADD CONSTRAINT,就可以用 ALTER CONSTRAINT 替换。

2. 评估全表扫描成本

对于大表,在执行 ALTER CONSTRAINT ... ENFORCED 前,先估算全表扫描时间:

-- 估算扫描时间(假设顺序读速度为 500 MB/s)  
SELECT pg_size_pretty(pg_total_relation_size('your_table')) assize,  
       pg_size_pretty(pg_total_relation_size('your_table')) / 500 * 1000as scan_time_ms;  

如果时间在维护窗口内,可以放心使用。如果过长,考虑分批策略。

3. 监控验证进度

PostgreSQL 提供了进度视图,可以监控 ALTER CONSTRAINT VALIDATE 的进度(如果实现的话,需确认)。目前可能通过 pg_stat_progress_cluster 或类似视图查看全表扫描进度。

4. 利用分区表的递归特性

如果你有分区表,一条 ALTER CONSTRAINT 语句将自动应用到所有分区。但注意,如果某个分区表没有定义该约束(例如新加的分区),操作可能会失败。确保所有子表都有同名的 CHECK 约束(通常自动继承)。

未来展望:更精细的约束控制

这个补丁打开了约束管理的新维度。未来我们可能看到:

  • 延迟验证:ALTER CONSTRAINT ... ENFORCED 提供 DEFERRABLE 选项,在事务提交时才验证。
  • 部分启用:允许对新数据启用约束,而旧数据豁免(类似 NOT VALID)。
  • 约束继承的精细控制:允许分区表上独立控制子表的约束状态。

结语

PostgreSQL 19 为 CHECK 约束带来的 ALTER CONSTRAINT ... ENFORCED 功能,看似是语法上的小改进,实则是数据管理灵活性的重大飞跃。它让 DBA 在面对数据迁移、批量加载等场景时,多了一把精细操作的手术刀,而不是只能挥舞删除重建的大锤。

从今天起,CHECK 约束不再是数据迁移路上的绊脚石,而是可以随时启用的守护者。