主题
字段与约束设计
NULL 与 NOT NULL
NULL 表示没有值 / 未知 / 未设置,与空字符串、0 不同。
md
NULL → 没有值
'' → 有值,只是字符串为空
0 → 有值,数值为 0字段是否允许 NULL,应根据业务语义决定:
一条合法记录中,这个字段是否允许不存在?
必须存在:
md
用户名
订单金额
库存数量
创建时间通常使用:
sql
NOT NULL允许不存在:
md
头像
个人简介
发布时间(未发布时)
删除时间(未删除时)可以允许 NULL。
不要使用无意义的默认值代替真正的「不存在」,例如:
md
published_at = ''
deleted_at = 0
parent_id = 0如果业务含义就是「没有」,通常使用 NULL 更准确。
UNIQUE 唯一约束
UNIQUE 用于保证字段值不能重复。
例如:
md
username → UNIQUE
email → UNIQUE主键已经具有唯一性,因此:
sql
id PRIMARY KEY通常不需要再额外设置:
sql
id UNIQUE联合唯一
有时单个字段允许重复,但多个字段的组合不能重复。
例如项目成员:
md
project_member
- project_id
- user_id一个用户不能重复加入同一个项目:
sql
UNIQUE (project_id, user_id)允许:
md
(项目1, 用户1)
(项目1, 用户2)
(项目2, 用户1)不允许:
md
(项目1, 用户1)
(项目1, 用户1)联合唯一可以包含多个字段:
sql
UNIQUE (student_id, course_id, semester)表示同一个学生在同一学期不能重复选择同一门课程,但可以在不同学期再次选择。
核心:
UNIQUE 约束的是业务定义的「唯一组合」,不一定是单个字段。
条件唯一
有些业务唯一性只针对满足特定条件的数据。
例如软删除用户:
md
user
- id
- email
- deleted_at业务规则:
当前有效用户的 email 不能重复,但已删除用户不占用 email。
真正的规则是:
md
deleted_at IS NULL
→ email 必须唯一
deleted_at IS NOT NULL
→ 不参与唯一性限制SQLite / PostgreSQL 可以通过部分唯一索引实现:
sql
CREATE UNIQUE INDEX idx_user_email_active
ON user (email)
WHERE deleted_at IS NULL;注意这与:
md
UNIQUE(email, deleted_at)不是同一个概念。
前者是:
先根据条件过滤记录,再保证指定字段唯一。
CHECK 检查约束
CHECK 用于限制字段值必须满足指定条件。
范围限制
sql
CHECK (price >= 0)sql
CHECK (age >= 0 AND age <= 150)固定值范围
例如订单状态只能是:
md
pending
paid
cancelled可以:
sql
CHECK (status IN ('pending', 'paid', 'cancelled'))CHECK 与 NOT NULL
两者职责不同:
md
NOT NULL
→ 是否必须有值
CHECK
→ 值是否合法例如:
sql
age INTEGER CHECK (age >= 0)仍可能允许:
md
age = NULL如果要求必须填写且不能小于 0:
sql
age INTEGER NOT NULL CHECK (age >= 0)因此不要把 CHECK 当成 NOT NULL 使用。
DEFAULT 默认值
DEFAULT 表示插入记录时,如果没有提供字段,则自动使用默认值。
例如:
sql
stock INTEGER NOT NULL DEFAULT 0sql
status TEXT NOT NULL DEFAULT 'active'SQLite 当前时间:
sql
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMPDEFAULT 与 NULL
DEFAULT 只在没有提供字段时生效。
例如:
sql
status TEXT DEFAULT 'active'未提供:
sql
INSERT INTO user (username)
VALUES ('Tom');得到:
md
status = 'active'显式提供:
sql
status = NULL不会自动替换成默认值。
因此:
md
DEFAULT
→ 未提供字段时使用什么值
NOT NULL
→ 是否允许最终值为 NULL常见组合:
sql
status TEXT NOT NULL DEFAULT 'active'表示:
字段必须有值;未指定时使用
active。
PRIMARY KEY 主键
主键用于唯一标识一条记录。
例如:
md
user
- id PRIMARY KEY
- email UNIQUE虽然 id 和 email 都具有唯一性,但语义不同:
md
id
→ 记录身份标识
email
→ 业务字段,只是业务要求不能重复因此:
主键一定具有唯一性,但唯一字段不一定应该作为主键。
自然主键与代理主键
自然主键
直接使用具有业务意义的字段作为主键:
md
email PRIMARY KEY这种字段称为自然主键(Natural Key)。
代理主键
增加一个与业务内容无直接关系的字段作为主键:
md
id PRIMARY KEY
email UNIQUE这种主键称为代理主键(Surrogate Key)。
业务系统通常更倾向使用稳定的代理主键:
md
id
→ 负责稳定标识记录
email / phone / sku_code
→ 负责业务唯一性原因是业务字段可能变化。
例如:
md
email:
a@example.com
↓
b@example.com如果其他表引用的是:
md
user.id修改邮箱不会影响表之间的关系。
自然主键并非错误设计,是否使用仍取决于业务特征。
FOREIGN KEY 外键约束
外键用于保证表之间的引用完整性。
例如:
md
user
- id
article
- id
- user_id关系:
md
article.user_id → user.id外键约束保证:
article.user_id引用的用户必须真实存在。
SQLite 示例:
sql
FOREIGN KEY (user_id) REFERENCES user(id)SQLite 需要确保当前数据库连接启用了外键约束:
sql
PRAGMA foreign_keys = ON;外键不仅表达表之间存在关系,还负责防止产生无效引用。
联合外键
有时引用的不是单个字段,而是父表中必须同时成立的字段组合。
例如选课必须对应一条真实开课记录:
md
course_offering
- course_id
- semester
enrollment
- course_id
- semester父表:
sql
PRIMARY KEY (course_id, semester)子表:
sql
FOREIGN KEY (course_id, semester)
REFERENCES course_offering (course_id, semester)允许:
md
course_offering 已有 (课程1, 2026春)
enrollment (课程1, 2026春)不允许:
md
enrollment (课程1, 2026秋)(课程1, 2026秋) 这组引用不存在。
被引用字段必须共同构成主键或唯一约束。两侧字段个数相同、类型兼容,按书写顺序对应,不按字段名对应。父表唯一约束的字段顺序也要与 REFERENCES 中的顺序一致。
这与多个独立外键不同:
sql
FOREIGN KEY (course_id) REFERENCES course(id)
FOREIGN KEY (semester) REFERENCES semester(code)独立外键只分别保证课程存在、学期存在。联合外键保证的是组合本身存在。
核心:
联合外键约束的是「这一组引用必须真实存在」,不是每个字段各自存在。
默认情况下,组合里任意一个字段为 NULL,这条外键不检查。
md
(课程1, NULL)
→ 通过,即使没有对应开课记录组合必须完整时,引用字段使用 NOT NULL。
PostgreSQL 可用 MATCH FULL:要么全部为 NULL,要么全部非空且组合存在。SQLite 不执行 MATCH,始终按上述默认规则处理。
删除和更新策略作用于整组引用。ON DELETE SET NULL 会把参与该外键的字段都置空,这些字段必须允许 NULL。
单字段主键不能表达「引用必须落在同一业务范围内」时,仍然使用联合外键。
例如员工只能属于本租户的部门:
md
department
- id PRIMARY KEY
- tenant_id
employee
- tenant_id
- department_id父表需要:
sql
UNIQUE (tenant_id, id)子表:
sql
FOREIGN KEY (tenant_id, department_id)
REFERENCES department (tenant_id, id)FOREIGN KEY (department_id) REFERENCES department(id) 只能保证部门存在,不能阻止引用其他租户的部门。id 已是主键时,仍要单独声明 UNIQUE (tenant_id, id),外键才能引用这个组合。
删除与引用完整性
删除被其他数据引用的记录时,需要明确关联数据如何处理。
常见策略:
md
RESTRICT / NO ACTION
CASCADE
SET NULLRESTRICT
存在关联数据时,禁止删除父记录。
适用于:
子数据不能因为父数据被删除而随意消失,同时又要求父子关系保持有效。
例如某些情况下:
md
分类仍存在商品
→ 禁止直接删除分类
→ 必须先迁移或处理商品CASCADE
删除父记录时,同时删除依赖它的子记录。
适用于:
父记录不存在后,子记录也完全失去业务意义。
例如:
md
order
↓
order_item如果业务允许物理删除订单:
md
删除 order
→ 自动删除对应 order_itemSET NULL
删除父记录后保留子记录,但解除关联:
md
employee.department_id = NULL适用于:
子实体仍然具有独立存在的意义,只是原来的关系消失。
例如:
md
删除部门
→ 员工保留
→ department_id = NULL
→ 员工进入“未分配部门”状态使用 SET NULL 时,对应外键字段必须允许 NULL。
删除策略的判断原则
不要根据表名或关系类型机械选择删除策略。
核心问题是:
父记录消失以后,子记录在业务上应该处于什么状态?
md
子记录也应该消失
→ CASCADE
父记录不能消失,因为关联数据要求它存在
→ RESTRICT
子记录继续存在,但允许解除关系
→ SET NULL软删除
数据库中的「删除」不一定意味着物理 DELETE。
对于需要保留历史身份、业务关系、审计数据的实体,可以使用软删除。
常见设计:
md
deleted_at = NULL
→ 正常数据
deleted_at != NULL
→ 已删除数据也可以使用:
md
is_deleted但 deleted_at 还能记录删除时间,通常信息量更高。
例如用户注销:
md
user
- id
- email
- deleted_at
order
- id
- user_id注销后:
md
user 记录仍然存在
deleted_at 设置删除时间
order.user_id
→ 仍然引用原 user.id这样可以同时实现:
md
用户无法正常使用账号
历史订单继续存在
订单仍能关联原用户
外键关系保持完整软删除与物理删除的选择
删除数据前应先判断:
md
这条数据是否真的应该从数据库中消失?如果需要保留:
- 历史关系
- 交易记录
- 审计信息
- 原始身份
- 恢复能力
通常应考虑软删除,而不是直接物理删除。
数据库约束与程序校验
数据库约束和程序校验不是二选一,两者职责不同。
数据库约束
负责维护数据完整性,作为最终防线。
典型规则:
md
PRIMARY KEY
UNIQUE
NOT NULL
FOREIGN KEY
必要的 CHECK即使程序出现 Bug、并发问题或存在多个数据写入入口,也不能让数据库进入非法状态。
程序校验
更适合:
md
复杂业务规则
容易变化的规则
流程相关规则
用户体验相关校验例如:
md
密码至少 12 位且包含数字
用户每天最多发布 10 篇文章
不同地区具有不同注册年龄要求两层同时校验
部分规则适合程序提前判断,同时由数据库最终兜底。
例如 email 唯一:
md
程序:
→ 提前查询 email 是否存在
→ 返回友好的错误信息
数据库:
→ UNIQUE(email)
→ 最终保证不会产生重复数据程序层的「先查询再插入」不能替代数据库唯一约束。
并发情况下可能出现:
md
请求 A → 查询 email 不存在
请求 B → 查询 email 不存在
请求 A → INSERT
请求 B → INSERT只有数据库的唯一约束能够最终保证不会出现重复记录。
约束设计速查
md
PRIMARY KEY
→ 唯一标识一条记录
NOT NULL
→ 字段必须有值
UNIQUE
→ 字段或字段组合不能重复
联合 UNIQUE
→ 多个字段组成的业务组合不能重复
条件唯一
→ 只对满足特定条件的数据保证唯一
CHECK
→ 字段值必须满足合法范围
DEFAULT
→ 未提供字段时使用默认值
FOREIGN KEY
→ 保证引用的数据真实存在
联合 FOREIGN KEY
→ 多个字段组成的引用必须整体存在
RESTRICT
→ 存在关联数据时禁止删除
CASCADE
→ 父数据删除时一起删除依赖数据
SET NULL
→ 父数据删除后保留子数据并解除关系
软删除
→ 数据保留,通过状态表示业务上的删除约束设计原则
设计字段和约束时重点判断:
md
1. 字段是否允许没有值?
2. 哪些字段或字段组合必须唯一?
3. 唯一性是否只针对特定状态的数据?
4. 字段值是否存在明确的合法范围?
5. 未提供字段时是否存在合理默认值?
6. 哪个字段负责稳定标识记录?
7. 业务字段是否可能发生变化?
8. 外键引用的是单个字段,还是必须同时存在的字段组合?
9. 父数据删除后,关联数据应该如何处理?
10. 数据应该物理删除还是软删除?
11. 规则属于数据完整性约束还是业务流程规则?
12. 哪些规则需要程序校验,同时由数据库最终兜底?