数据库完整性约束
一句话总览:完整性约束 = 让数据库里的数据始终"正确、可信、符合现实规则"的一组规定。
其中按"能不能用 DDL 一句话声明出来"又分成 直接约束 和 间接约束。
目录
- 一、什么是完整性约束
- 二、完整性约束的四大类型(基础地图)
- [三、直接约束 vs 间接约束(本文重点)](#三、直接约束 vs 间接约束(本文重点))
- 四、完整的对照例子(一套订单系统)
- 五、速记口诀与判断流程
- 六、练习题(含答案与解析)
一、什么是完整性约束
完整性约束(Integrity Constraint) 是数据库为了保证数据的正确性(correct) 、有效性(valid) 和 相容性(consistent) 而设置的规则。
数据库不只是"存数据的仓库",它还要保证存进去的数据不胡来:
| 没有约束会发生什么 | 约束的作用 |
|---|---|
| 同一个用户身份证号出现两次 | 唯一性保证 |
| 订单指向一个根本不存在的用户 | 引用关系保证 |
年龄填了 -5、状态填了 99 |
取值范围保证 |
| 转账后两个账户金额对不上 | 业务规则保证 |
💡 核心理解 :约束不是"建议",而是数据库强制执行的"法律"。违反约束的写入操作会被直接拒绝(报错/回滚),数据进不来。
二、完整性约束的四大类型(基础地图)
在讲"直接/间接"之前,先建立坐标系。经典关系数据库把完整性分为四类:
1️⃣ 实体完整性(Entity Integrity)
主键不能为空,且唯一。 保证每一行都能被唯一识别。
sql
CREATE TABLE t_user (
id BIGINT PRIMARY KEY, -- 主键:非空 + 唯一
name VARCHAR(50)
);
2️⃣ 参照完整性(Referential Integrity)
外键的值必须在被引用表里真实存在(或为 NULL)。保证表与表的引用关系不"悬空"。
sql
CREATE TABLE t_order (
id BIGINT PRIMARY KEY,
user_id BIGINT,
FOREIGN KEY (user_id) REFERENCES t_user(id) -- user_id 必须是真实存在的用户
);
3️⃣ 用户定义完整性(User-defined Integrity)
业务自定义的规则。如"年龄 0~150""状态只能是 0/1/2"。
sql
age INT CHECK (age BETWEEN 0 AND 150),
status TINYINT CHECK (status IN (0, 1, 2))
4️⃣ 域完整性(Domain Integrity)
字段的取值类型、范围、是否可空、默认值。是最基础的一层。
sql
email VARCHAR(100) NOT NULL,
balance DECIMAL(10,2) DEFAULT 0.00
📌 注意 :四大类型是按"约束的内容/对象 "分类。
而 直接约束 / 间接约束 是按"怎么实现 "分类 ------ 这是另一个维度,两套分类会交叉。
三、直接约束 vs 间接约束(本文重点)
定义
| 直接约束(Declarative / Direct) | 间接约束(Procedural / Indirect) | |
|---|---|---|
| 本质 | 能用 DDL 直接声明出来的约束 | DDL 表达不了,需要靠额外机制实现的约束 |
| 靠谁执行 | 数据库引擎自动保证 | 触发器 / 断言 / 存储过程 / 应用程序代码 |
| 典型手段 | PRIMARY KEY FOREIGN KEY UNIQUE NOT NULL CHECK DEFAULT |
TRIGGER、ASSERTION、业务 Service 层校验 |
| 特点 | 简单、声明式、写一次永久生效、性能好 | 灵活、能表达复杂规则、但要自己维护、易出 bug |
✅ 一句话区分 :
能在
CREATE TABLE里写出来的,就是直接约束;写不出来、得另外编程实现的,就是间接约束。
直接约束(Direct Constraint)
关键词:声明式、数据库内建、自动强制。
例子:用户表
sql
CREATE TABLE t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT, -- ① 实体完整性(直接)
email VARCHAR(100) NOT NULL UNIQUE, -- ② 非空 + 唯一(直接)
age INT CHECK (age BETWEEN 0 AND 150), -- ③ 取值范围(直接)
status TINYINT DEFAULT 0, -- ④ 默认值(直接)
dept_id BIGINT,
FOREIGN KEY (dept_id) REFERENCES t_dept(id) -- ⑤ 参照完整性(直接)
);
这五条都是直接约束 :你只是"声明"了规则,剩下的检查全部由数据库自动完成 。
插入 age = 200 → 数据库直接报错;插入一个不存在的 dept_id → 数据库直接拒绝。你一行校验代码都不用写。
间接约束(Indirect Constraint)
关键词:DDL 表达不了、要靠触发器/程序、需要自己保证。
什么样的规则 DDL 写不出来?通常是 跨行、跨表、带计算、带时序 的复杂业务规则:
例子 1:跨表统计约束 ------ "一个部门最多 50 人"
CHECK 只能看当前行,看不到"整个部门现在有多少人",所以这是间接约束,需要触发器:
sql
CREATE TRIGGER trg_dept_limit
BEFORE INSERT ON t_user
FOR EACH ROW
BEGIN
DECLARE cnt INT;
SELECT COUNT(*) INTO cnt FROM t_user WHERE dept_id = NEW.dept_id;
IF cnt >= 50 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '该部门人数已满50人';
END IF;
END;
例子 2:动态约束(带"前后状态")------ "工资只能涨不能降"
涉及"旧值 vs 新值"的比较,DDL 静态约束做不到,需要 UPDATE 触发器:
sql
CREATE TRIGGER trg_salary_no_down
BEFORE UPDATE ON t_employee
FOR EACH ROW
BEGIN
IF NEW.salary < OLD.salary THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '工资不允许下调';
END IF;
END;
例子 3:业务规则放在应用层 ------ "下单时库存必须充足"
很多团队不写触发器,而是在 Service 层代码 里保证(本项目就是这种风格):
java
// member-biz 里的业务校验,本质也是"间接约束"
if (stock.getQuantity() < order.getQuantity()) {
throw new BizException(MemberErrorCode.STOCK_NOT_ENOUGH);
}
⚠️ 间接约束的代价 :数据库不替你兜底 。只要有一条没走你的代码的写入路径(比如别人直接连库改数据、另一个服务绕过校验),约束就会被破坏。所以原则是:能用直接约束就别用间接约束。
一图理解二者关系
完整性约束(总目标:数据正确可信)
│
┌─────────────────────┴─────────────────────┐
按"内容"分 按"实现方式"分
(四大类型) (本文重点)
│ │
实体 / 参照 / ┌────────────┴────────────┐
用户定义 / 域 直接约束 间接约束
(DDL声明, DB自动) (触发器/断言/程序)
PK/FK/UNIQUE/ 复杂业务规则
CHECK/NOT NULL 跨表/跨行/动态
同一个"参照完整性"规则:
- 用
FOREIGN KEY实现 → 它就是直接约束- 数据库不支持外键(如分库分表场景),改用程序检查 → 同样的规则就变成了间接约束
👉 所以"直接/间接"描述的是实现手段,不是规则本身。
四、完整的对照例子(一套订单系统)
业务需求清单,逐条判断属于直接还是间接:
| # | 业务规则 | 类型 | 实现方式 |
|---|---|---|---|
| 1 | 每个订单有唯一订单号 | 🟢 直接 | PRIMARY KEY / UNIQUE |
| 2 | 订单必须属于一个真实存在的用户 | 🟢 直接 | FOREIGN KEY |
| 3 | 订单金额不能为负 | 🟢 直接 | CHECK (amount >= 0) |
| 4 | 订单状态只能是 待支付/已支付/已取消 | 🟢 直接 | CHECK (status IN (0,1,2)) |
| 5 | 下单数量不能超过当前库存 | 🔴 间接 | 触发器 / Service 层校验(要查另一张库存表) |
| 6 | 同一用户一天最多下 100 单 | 🔴 间接 | 触发器(COUNT 当天订单) / 程序 |
| 7 | 订单一旦"已支付"就不能改回"待支付" | 🔴 间接 | UPDATE 触发器(比较 OLD/NEW,动态约束) |
| 8 | 删除用户时,其订单也要自动删除 | 🟢 直接 | FOREIGN KEY ... ON DELETE CASCADE |
规律总结:
- 只看"这一行自己的值" → 基本能用直接约束(
CHECK/NOT NULL/ 默认值)。- 要看"别的行、别的表、旧的值、统计结果" → 通常是间接约束。
五、速记口诀与判断流程
🎯 三步判断法
拿到一条规则,问自己:
Step 1 这条规则只跟"当前这一行的字段"有关吗?
是 → 大概率【直接约束】(NOT NULL / CHECK / DEFAULT)
否 → 进入 Step 2
Step 2 它是"这张表引用另一张表的主键"这种关系吗?
是 → 【直接约束】(FOREIGN KEY)
否 → 进入 Step 3
Step 3 涉及跨行统计、跨表计算、新旧值比较、时序?
是 → 【间接约束】(触发器 / 断言 / 程序)
📝 口诀
"一行能定的,DDL 直接管;要算要比要联表,触发程序间接办。"
六、练习题(含答案与解析)
建议先自己作答,再展开看答案。共 8 题(4 选择 + 2 判断 + 2 简答)。
选择题
第 1 题 下列哪一项不属于直接约束(声明式约束)?
- A.
PRIMARY KEY - B.
FOREIGN KEY - C.
CHECK (age > 0) - D. 用触发器实现"每个班级最多 40 名学生"
👉 查看答案
答案:D
A、B、C 都能在 CREATE TABLE 里直接声明,由数据库自动强制,属于直接约束 。
D 涉及"统计同一班级当前人数"(跨行),DDL 表达不了,必须用触发器/程序实现,属于间接约束。
第 2 题 "主键不能为 NULL 且必须唯一",这描述的是哪一类完整性?
- A. 实体完整性
- B. 参照完整性
- C. 用户定义完整性
- D. 域完整性
👉 查看答案
答案:A 实体完整性
实体完整性的核心就是"主键非空且唯一",保证每行可被唯一标识。注意它同时也是一种直接约束 (用 PRIMARY KEY 声明)------内容分类与实现分类是两个维度。
第 3 题 某公司用了分库分表,数据库未启用外键,改为在 Java 代码里检查"订单的 user_id 是否存在"。此时这条"引用必须存在"的规则属于:
- A. 直接约束
- B. 间接约束
- C. 既不是约束也不是规则
- D. 实体完整性
👉 查看答案
答案:B 间接约束
规则本身是"参照完整性",但因为改用程序代码实现 ,所以它在实现维度上是间接约束 。这正说明"直接/间接"取决于实现手段,而非规则种类。
第 4 题 下列规则中,最适合 用直接约束(CHECK)实现的是:
- A. 用户余额扣减后必须 ≥ 0,且要同步更新流水表
- B. 商品折扣率必须在 0 到 1 之间
- C. 同一手机号一分钟内最多发 1 次验证码
- D. 员工的新工资不得低于原工资
👉 查看答案
答案:B
B 只看当前行的一个字段范围 discount BETWEEN 0 AND 1,天然适合 CHECK(直接约束)。
A 跨表更新、C 涉及时间窗口统计、D 涉及新旧值比较 ------ 都是间接约束。
判断题
第 5 题 判断对错:FOREIGN KEY 属于参照完整性,所以它是间接约束。
👉 查看答案
答案:错 ❌
混淆了两个维度。FOREIGN KEY 是参照完整性(内容维度),同时它能用 DDL 直接声明、由数据库自动强制,因此是直接约束(实现维度)。
第 6 题 判断对错:间接约束比直接约束更"高级",所以业务规则应优先用触发器/程序实现。
👉 查看答案
答案:错 ❌
原则恰恰相反:能用直接约束就优先用直接约束。直接约束由数据库统一兜底、无法被绕过、性能好;间接约束只有在 DDL 表达不了时才用,且需自己保证所有写入路径都经过校验,否则容易出现脏数据。
简答题
第 7 题 请说出 3 种"DDL 无法直接表达、只能用间接约束实现"的规则,并各给一个例子。
👉 查看参考答案
参考答案(任意 3 类即可):
- 跨行统计约束:一个班最多 40 人 / 一个用户最多 5 张银行卡。
- 动态(时序)约束:订单状态只能从"待支付→已支付",不能回退;工资只能涨不能降。
- 跨表关联计算约束:下单数量 ≤ 库存表当前数量;订单总额 = 明细表金额之和。
- 复杂条件约束:VIP 用户折扣不得低于 0.5,普通用户不得低于 0.8(取值依赖另一字段且含分支)。
要点:凡是"要看别的行 / 别的表 / 旧值 / 统计结果 / 多字段联合判断"的,基本都是间接约束。
第 8 题 一句话解释:为什么"直接/间接"和"实体/参照/用户定义/域"是两套不冲突的分类?并举一个同一条规则既能直接、又能间接实现的例子。
👉 查看参考答案
参考答案:
因为它们分类的角度不同 :四大类型按约束的**内容(约束什么)分,直接/间接按约束的实现方式(怎么保证)**分,二者正交、可以交叉组合。
例子 :"订单的 user_id 必须存在于用户表" 这条参照完整性规则------
- 启用外键时:用
FOREIGN KEY实现 → 直接约束; - 分库分表禁用外键时:在 Service 层查一次用户是否存在 → 间接约束。
同一条规则,内容分类不变(参照完整性),实现分类却随手段改变(直接 ↔ 间接)。
✅ 学完自检 :看到任意一条业务规则,你能立刻说出
①它属于四大类型里的哪一类;②它该用直接约束还是间接约束实现;③为什么。
三个都能答上来,这个知识点就过关了。