全书目录

第八篇:PostgreSQL 完全指南 —— 时间档案馆宇宙

第四章:约束 —— 档案馆的硬规矩

2 分钟 483 字 第 168 / 962 个阅读单元

只靠应用代码保证数据正确,迟早出事。

数据库里的约束,就是档案馆大门上的铁规则。

常见约束关系:

text
NOT NULL     -> 这栏不能空
CHECK        -> 这栏必须满足表达式
UNIQUE       -> 这类编号不能重复
PRIMARY KEY  -> 这份档案的主编号
FOREIGN KEY  -> 这份档案必须指向另一份真实存在的档案

比如:

sql
CREATE TABLE permits (
  permit_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  citizen_id bigint NOT NULL REFERENCES citizens(citizen_id),
  permit_type text NOT NULL,
  valid_from date NOT NULL,
  valid_to date NOT NULL,
  status text NOT NULL CHECK (status IN ('draft', 'active', 'expired', 'revoked')),
  CHECK (valid_to >= valid_from)
);

这里有几层意思:

  • PRIMARY KEY:每张通行证必须有唯一主编号
  • NOT NULL:关键字段不能为空
  • CHECK:到期日不能早于生效日
  • REFERENCES citizens(citizen_id):通行证不能挂到不存在的人头上

一个非常关键的设计习惯是:

能用约束表达的数据规则,就不要只写在代码里。

再看一个很实用的外键删除策略:

sql
CREATE TABLE permit_events (
  event_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  permit_id bigint NOT NULL REFERENCES permits(permit_id) ON DELETE CASCADE,
  event_type text NOT NULL,
  payload jsonb NOT NULL DEFAULT '{}'::jsonb
);

这表示:如果通行证删了,对应事件日志也跟着删。

但要注意两个常见坑:

  1. UNIQUE 默认允许多个 NULL
  2. 外键不会自动给“引用方”列建索引

比如:

sql
CREATE INDEX idx_permits_citizen_id ON permits(citizen_id);
CREATE INDEX idx_permit_events_permit_id ON permit_events(permit_id);

没有这些索引,大量删除或更新被引用行时,性能可能会非常差。

还有一个更隐蔽的坑:

不要用 CHECK 去引用“别的行”或“别的表”的数据。 官方文档明确提醒,这类 CHECK 看起来能跑,实际上不能可靠保证一致性,甚至可能在 dump/restore 时出问题。跨行、跨表规则,优先用 UNIQUEFOREIGN KEYEXCLUDE 或触发器。