4.4 用约束表达不变量
约束把一句业务命题变成所有写入口共享、并发下原子执行的失败条件。它的价值不止是“挡脏数据”:命名、类型、检查时点、支持索引与 SQLSTATE 共同组成可审计合同。本节从常用约束走到排他与延迟检查,同时严格说明每种机制不能做什么。
4.4.1 主键、唯一、外键与检查约束
选择约束先看规则的作用域:
| 不变量 | PostgreSQL 机制 | 自动支持结构 |
|---|---|---|
| 一行有唯一且非空身份 | PRIMARY KEY | unique B-tree + NOT NULL |
| 一个/一组值在表中唯一 | UNIQUE | unique B-tree |
| 引用必须存在 | FOREIGN KEY | 被引用端必须有合格 unique;引用端不自动建索引 |
| 当前行布尔命题成立 | CHECK | 无索引 |
| 列值存在 | NOT NULL | 专用高效检查 |
| 任意两行不能满足一组冲突运算 | EXCLUDE | 指定 access method 的索引 |
命名约束就是命名失败
v1 不依赖 PostgreSQL 自动生成名:
CONSTRAINT product_price_minor_bounds
CHECK (current_unit_price_minor BETWEEN 0 AND 1000000000000)调用方可以按 SQLSTATE 类别处理,又能记录 constraint name 定位具体不变量:
| SQLSTATE | condition | 本章例子 |
|---|---|---|
23505 | unique_violation | 重复 order_no |
23503 | foreign_key_violation | 不存在的引用 |
23514 | check_violation | 非法币种/状态时间 |
23502 | not_null_violation | 必填列为空 |
23P01 | exclusion_violation | 时间段重叠 |
不要依赖英文 error message 文本,它会随版本、locale 和上下文变化。也不要假设多项同时违反时必定先报告某一个:约束检查顺序不是 API 合同。本章反例每次只制造一个目标错误,并验证 constraint name。
pg_constraint.conname 不是数据库全局唯一;定位时至少带 relation/schema。目录查询:
SELECT
c.conrelid::regclass AS relation,
c.conname,
c.contype,
c.convalidated,
c.condeferrable,
c.condeferred,
pg_catalog.pg_get_constraintdef(c.oid, true) AS definition
FROM pg_catalog.pg_constraint AS c
WHERE c.conrelid IN (
'shop.sales_order'::regclass,
'shop.sales_order_item'::regclass,
'shop.payment'::regclass
)
ORDER BY relation::text, c.conname;PK、unique 与 identity 不是同义词
identity 生成候选值,PK 维护唯一/非空并声明主要行身份。一个表只能有一个 PK,却可以有多个业务 unique:
sales_order_pkey (order_id)
sales_order_order_no_key (order_no)
sales_order_request_key (customer_id, request_key)
sales_order_order_currency_key (order_id, currency_code)最后一个复合 unique 看似冗余,因为 order_id 已唯一;它是复合 FK 的合法目标,使 line/payment 必须携带与 order 相同的 currency。这个语义换来一个额外 unique index,成本在 4.5 明确记账。
默认情况下,unique constraint 把多个 NULL 视为互不相等,所以 nullable unique column 仍可有多个 NULL。PG15+ 提供 NULLS NOT DISTINCT,但本书 PG14–18 共同行为不能无条件使用。最简单的业务键通常应 NOT NULL。
FK 既是存在性,也是生命周期
本章延续:
- order→customer:
ON DELETE RESTRICT; - line→product:
ON DELETE RESTRICT,历史快照仍保留引用; - line→order:
ON DELETE CASCADE,line 是 order 的组成部分; - payment→order:
ON DELETE RESTRICT,支付记录有独立保留要求。
FK 自动要求被引用列可唯一查找,却不会自动为 child referencing columns 建索引。删除/更新 parent 时 PostgreSQL 需要在 child 找引用;数据大时通常要为 child FK 设计索引,但索引列序和其他查询可以合并考虑,不能由 FK 机械生成器盲加。
CHECK 只维护当前行的稳定命题
PostgreSQL 明确不支持把其他行/表查询塞进 CHECK 并承诺持续一致。下面不是合法方向:
CHECK (
amount_minor <= (
SELECT sum(line_total_minor)
FROM shop.sales_order_item
WHERE order_id = payment.order_id
)
)其他行后来变化时不会自动重检,dump/restore 也可能破坏假设。跨行“互不重叠”可用 EXCLUDE,引用用 FK,唯一用 UNIQUE;真正跨表业务命令留给事务/trigger/constraint trigger 等受验证实现。
CHECK 还把 NULL 结果视为通过,所以必填列另加 NOT NULL。v1 的 money bounds、ASCII 格式和状态时间都是只引用当前行、对同一输入稳定的表达式,符合边界。
4.4.2 排他约束与 btree_gist 的适用条件
UNIQUE 只能表达“这些键不能相等”。许多业务冲突是“时间段不能重叠”“圆不能相交”“同笼不能出现不同动物”。EXCLUDE 接受一组 operator,要求任意两行比较时,至少有一个 operator 结果为 false 或 NULL。
单资源预约:
CREATE TEMP TABLE booking_window (
booking_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
slot tstzrange NOT NULL,
CONSTRAINT booking_window_nonempty
CHECK (NOT isempty(slot)),
CONSTRAINT booking_window_no_overlap
EXCLUDE USING gist (slot WITH &&)
);&& 为 range overlap。排他约束自动建立 GiST index,两个重叠 slot 触发 23P01。把边界统一为 [) 很重要:09:00–10:00 与 10:00–11:00 不重叠,避免相邻时段同时包含 10:00。
多资源为什么需要 btree_gist
若每个 room 分别不能重叠:
EXCLUDE USING gist (
room_id WITH =,
slot WITH &&
)GiST 原生理解 range overlap,但普通 bigint/text equality 需要相应 GiST operator class。btree_gist 为常见标量类型提供类似 B-tree 的 GiST operator classes,适合这种“标量相等 + 空间/范围运算”的多列 GiST。
它不是“更快的 B-tree”:
- 官方文档明确说通常不会优于标准 B-tree;
- 它不能像 B-tree unique index 那样维护普通唯一性;
- 它的价值是让不同运算共存于 GiST/EXCLUDE。
在 Pigsty L1 先观察:
SELECT
name,
default_version,
installed_version
FROM pg_catalog.pg_available_extensions
WHERE name = 'btree_gist';本章实测 btree_gist_available=true,但单列 slot 实验无需创建 extension,因此不改变数据库 extension 状态。若真实模式需要它,再由配置/迁移明确:
CREATE EXTENSION btree_gist;Pigsty 负责把所需软件包/内核扩展交付到节点;CREATE EXTENSION 仍是目标 database 中的 DDL,要纳入 owner、schema、版本和备份恢复合同。
并发正确性
EXCLUDE 的优势不是语法短,而是把冲突交给 access method 与约束在并发写入中仲裁,避免应用“先查没有重叠,再插入”之间的竞态。应用仍可预查给友好提示,但最终以数据库 exclusion_violation 为准。
选择前确认:
- 冲突关系可由可索引 operator 精确表达;
- NULL/空 range 是否允许;
- 边界是
[)还是其他形式; - 是否需要按资源、租户等额外等值维度隔离;
- 索引/锁竞争在目标写入量下可接受。
4.4.3 仅对支持类型使用 DEFERRABLE,并说明事务末校验代价
DEFERRABLE 不是“让所有约束最后再查”的通用开关。PostgreSQL 只允许:
- UNIQUE;
- PRIMARY KEY;
- EXCLUDE;
- REFERENCES / FOREIGN KEY。
NOT NULL 与 CHECK 不可延迟。当前文档若写出 CHECK (...) DEFERRABLE,DDL 本身就是错的。
两个维度
DEFERRABLE / NOT DEFERRABLE
能否由事务改变检查时点
INITIALLY IMMEDIATE / INITIALLY DEFERRED
每个事务开始时的默认检查时点NOT DEFERRABLE 是默认,不能用 SET CONSTRAINTS 推迟。deferrable 且 initially immediate 默认在每条语句后检查,可以在事务中改为 deferred;initially deferred 默认到提交时检查。
实验中有两个唯一 slot:
('A', 1), ('B', 2)要在一条/一组操作中交换为 A=2、B=1,中间状态可能碰到唯一值。定义:
CONSTRAINT display_slot_slot_key
UNIQUE (slot_no)
DEFERRABLE INITIALLY IMMEDIATE事务内:
SET CONSTRAINTS display_slot_slot_key DEFERRED;
UPDATE display_slot
SET slot_no = CASE slot_no WHEN 1 THEN 2 WHEN 2 THEN 1 END;
SET CONSTRAINTS display_slot_slot_key IMMEDIATE;最后一条会立即检查当前状态;若仍冲突,就在此处失败,不必等 COMMIT。这既是实验验收,也是长事务中提早暴露错误的方法。
代价与限制
延迟检查会把失败推到更远位置,事务可能做了大量工作后才回滚,并在事务期间保留用于待检查状态的资源。官方还指出:
- deferrable constraint 不能作为
INSERT ... ON CONFLICT的 conflict arbiter; - deferrable uniqueness 可能显著慢于 immediate uniqueness;
- FK 被引用的 unique/PK 必须是 non-deferrable 合格键。
所以“以后批量导入方便”不足以把所有键改成 deferrable。先有一个需要事务内暂时不一致、提交时恢复的具体流程,再付成本。
v1 所有业务约束保持 immediate。状态转换也在每次 UPDATE 时立即拒绝;订单 command 没有证明需要暂时进入图外状态。隔离的 constraint-lab.sql只演示正确适用点,不改变核心模式。
目录验收:
SELECT
conrelid::regclass,
conname,
contype,
condeferrable,
condeferred,
convalidated
FROM pg_catalog.pg_constraint
WHERE conrelid = 'display_slot'::regclass;临时 unique 应为 deferrable=true、deferred-by-default=false;临时 CHECK 不应出现 deferrable=true。
本节验收
- 每条不变量根据行内、唯一、引用或跨行冲突选对约束;
- error handling 使用 SQLSTATE + constraint identity,不匹配英文全文;
- 知道 unique/PK/EXCLUDE 会建索引,FK child 不自动建;
- 排他实验真实拒绝 overlap,且没有无谓安装 extension;
- 能列出四类可延迟约束与两类不可延迟约束;
- 能解释延迟检查对 ON CONFLICT、失败时点和性能的影响;
- 核心 v1 没有为假想需求滥用 DEFERRABLE。
参考资料
- PostgreSQL 18:约束
- PostgreSQL 18:CREATE TABLE 与 DEFERRABLE
- PostgreSQL 18:SET CONSTRAINTS
- PostgreSQL 18:range 与 exclusion constraint
- PostgreSQL 18:
btree_gist - PostgreSQL 18:
pg_constraint
上一节:NULL、默认值与生成值 · 返回本章目录 · 下一节:类型与约束的物理代价 · 查看全书目录 · 查看索引中心