约束:写下你以为「显然」的规矩
你脑子里有一堆「这显然不该发生」:余额不能为负、邮箱不能重复、订单必须挂在真实存在的客户上。问题是——「显然」只在你脑子里。写进约束,它才成为数据库当真执行的铁律,替你挡住应用的 bug、手滑的运维、和绕过应用的一切写入。
为什么约束要放在数据库里,而不是只放在应用里
常见的反驳是:「这些校验我应用层做了呀。」但把规矩只放在应用层,有几个致命弱点:
- 应用有 bug:漏了一处校验、并发下的竞态、某个后门接口忘了检查——数据就脏了。
- 不止一个写入方:定时任务、数据迁移脚本、临时手动
UPDATE、另一个团队的服务,都可能绕过你的应用直接写库。 - 规矩会漂移:应用代码里散落十几处校验,改一处漏一处,久了就不一致。
数据库约束是最后一道防线,也是唯一一道无论谁、从哪个入口写入都绕不过的防线。它在离数据最近的地方站岗。把核心不变量(invariant)写成约束,等于给数据上了一道谁都撬不开的锁。
五种约束,各管一摊
| 约束 | 保证什么 | 例子 |
|---|---|---|
NOT NULL | 这一列不许是「不知道」 | 书必须有标题 |
UNIQUE | 这一列(或几列组合)不许重复 | 邮箱唯一 |
CHECK | 值必须满足一个你写的条件 | balance >= 0 |
PRIMARY KEY | = UNIQUE + NOT NULL,一行的身份证 | id |
FOREIGN KEY | 这一列的值必须在另一张表里真实存在 | 订单挂在真客户上 |
下面这台真引擎有一张带全套约束的 account 表。挨个点那些「试着插入」——正常的一条会成功,五种违反(主键重复、邮箱重复、非空违反、CHECK 违反、外键悬空)会各自被当场拒绝,并给出 PostgreSQL 真实的报错文案。
注意每种报错都精确指出违反了哪条约束(account_pkey、account_email_key、account_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 是最被低估的约束。它能把相当复杂的业务规则钉进表定义:
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 重叠(&&) );
有了它,「不许重复预订」从一条你得在应用里小心翼翼、还挡不住并发的检查,变成了一条数据库层面并发安全、绝对可靠的规则。这是「把约束当真」能到达的深度。
- 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 用户眼馋的「超能力类型」——数组、范围、枚举、域,它们让一格能装下更丰富的结构。