卷 III · 类型CH 12深度 12/24

约束:写下你以为「显然」的规矩

你脑子里有一堆「这显然不该发生」:余额不能为负、邮箱不能重复、订单必须挂在真实存在的客户上。问题是——「显然」只在你脑子里。写进约束,它才成为数据库当真执行的铁律,替你挡住应用的 bug、手滑的运维、和绕过应用的一切写入。

NOT NULLUNIQUECHECK外键

为什么约束要放在数据库里,而不是只放在应用里

常见的反驳是:「这些校验我应用层做了呀。」但把规矩放在应用层,有几个致命弱点:

  • 应用有 bug:漏了一处校验、并发下的竞态、某个后门接口忘了检查——数据就脏了。
  • 不止一个写入方:定时任务、数据迁移脚本、临时手动 UPDATE、另一个团队的服务,都可能绕过你的应用直接写库。
  • 规矩会漂移:应用代码里散落十几处校验,改一处漏一处,久了就不一致。

数据库约束是最后一道防线,也是唯一一道无论谁、从哪个入口写入都绕不过的防线。它在离数据最近的地方站岗。把核心不变量(invariant)写成约束,等于给数据上了一道谁都撬不开的锁。

五种约束,各管一摊

约束保证什么例子
NOT NULL这一列不许是「不知道」书必须有标题
UNIQUE这一列(或几列组合)不许重复邮箱唯一
CHECK值必须满足一个你写的条件balance >= 0
PRIMARY KEY= UNIQUE + NOT NULL,一行的身份证id
FOREIGN KEY这一列的值必须在另一张表里真实存在订单挂在真客户上
▶ 动手 · 约束校验器

下面这台真引擎有一张带全套约束的 account 表。挨个点那些「试着插入」——正常的一条会成功,五种违反(主键重复、邮箱重复、非空违反、CHECK 违反、外键悬空)会各自被当场拒绝,并给出 PostgreSQL 真实的报错文案。

注意每种报错都精确指出违反了哪条约束account_pkeyaccount_email_keyaccount_balance_check……)——这些名字是自动生成的,也可以自己命名。清晰的约束名,能让你线上看到报错时一眼知道哪条规矩被撞了。

外键:不止「值要存在」,还有「删了怎么办」

外键保证 orders.customer_id 必须对应一个真实的 customers.id。但它还管一件事:当被指向的行被删除或修改时,指向它的行怎么办?这由 ON DELETE / ON UPDATE 决定:

customer_id integer NOT NULL
  REFERENCES customers(id)
  ON DELETE RESTRICT    -- 还有订单挂着,就不许删这个客户(默认,最安全)
  -- ON DELETE CASCADE  -- 删客户,连带删掉他的所有订单(危险,想清楚再用)
  -- ON DELETE SET NULL -- 删客户,把订单的 customer_id 置空(列得允许 NULL)

RESTRICT(默认行为,和 NO ACTION 类似)最安全:有引用就不让删,逼你先处理干净。CASCADE 很方便但要慎用——一个 DELETE 可能雪崩式删掉一大片数据。选哪个,取决于业务语义,但无论选哪个,「订单不能挂在幽灵客户上」这条底线,数据库都替你守住了

◆ CHECK:把业务规则写进 schema

CHECK 是最被低估的约束。它能把相当复杂的业务规则钉进表定义:

CHECK (price >= 0)                          -- 价格非负
CHECK (discount_price <= price)             -- 折后价不高于原价(跨列)
CHECK (status IN ('pending','paid','shipped'))  -- 状态只能是这几个
CHECK (ended_at IS NULL OR ended_at > started_at)  -- 结束晚于开始

这些规矩写在这里,比写在应用代码、写在文档、写在 code review 意见里都可靠——因为它是数据库当真执行的,任何违反它的写入都进不来。schema 因此成了「这张表的数据一定满足什么」的可执行文档

EXCLUDE:PostgreSQL 的独门约束

有一类规矩,UNIQUE 表达不了:「同一个会议室,两个预订的时间段不许重叠。」这不是「相等不许重复」,是「重叠不许」。PostgreSQL 有个别家几乎没有的排他约束(EXCLUDE),配合范围类型(下一章)能直接表达它:

CREATE TABLE booking (
  room int,
  during tsrange,   -- 时间范围类型
  EXCLUDE USING gist (room WITH =, during WITH &&)
  -- 不许存在两行:room 相等 且 during 重叠(&&)
);

有了它,「不许重复预订」从一条你得在应用里小心翼翼、还挡不住并发的检查,变成了一条数据库层面并发安全、绝对可靠的规则。这是「把约束当真」能到达的深度。

⇄ 对照 MySQL
  • CHECK 曾经是摆设:这是历史 MySQL 最有名的「不当真」——8.0.16 之前,MySQL 会解析你的 CHECK 约束、然后完全忽略它,让违反的数据照进不误。无数人以为自己加了 CHECK 保护,其实裸奔。PostgreSQL 从来当真执行 CHECK。
  • 外键要看引擎:MySQL 的外键只有 InnoDB 支持,建成 MyISAM 表外键会被无声忽略。PostgreSQL 只有一种存储,外键永远有效。
  • 没有 EXCLUDE:排他约束是 PostgreSQL 独有,MySQL 没有对应能力,「防重叠」只能在应用层费力实现。
  • 可延迟约束:PostgreSQL 支持 DEFERRABLE 约束(把检查推迟到事务提交时),便于处理循环外键、批量调整;MySQL 不支持外键的延迟检查。

这一章的一句话

约束是最后一道防线:把「显然不该发生」写成 NOT NULL / UNIQUE / CHECK / 外键 / EXCLUDE,数据库就当真执行,任何入口的违规写入都进不来——schema 因此成了可执行的数据契约。

类型和约束管住了「值」的合法性。下一章,我们看 PostgreSQL 那些让 MySQL 用户眼馋的「超能力类型」——数组、范围、枚举、域,它们让一格能装下更丰富的结构。