只靠应用代码保证数据正确,迟早出事。
数据库里的约束,就是档案馆大门上的铁规则。
常见约束关系:
NOT NULL -> 这栏不能空
CHECK -> 这栏必须满足表达式
UNIQUE -> 这类编号不能重复
PRIMARY KEY -> 这份档案的主编号
FOREIGN KEY -> 这份档案必须指向另一份真实存在的档案比如:
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):通行证不能挂到不存在的人头上
一个非常关键的设计习惯是:
能用约束表达的数据规则,就不要只写在代码里。
再看一个很实用的外键删除策略:
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
);这表示:如果通行证删了,对应事件日志也跟着删。
但要注意两个常见坑:
UNIQUE默认允许多个NULL- 外键不会自动给“引用方”列建索引
比如:
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 时出问题。跨行、跨表规则,优先用 UNIQUE、FOREIGN KEY、EXCLUDE 或触发器。