【数据库完整性】

数据库完整性约束

一句话总览:完整性约束 = 让数据库里的数据始终"正确、可信、符合现实规则"的一组规定。

其中按"能不能用 DDL 一句话声明出来"又分成 直接约束间接约束


目录


一、什么是完整性约束

完整性约束(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 TRIGGERASSERTION、业务 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 类即可):

  1. 跨行统计约束:一个班最多 40 人 / 一个用户最多 5 张银行卡。
  2. 动态(时序)约束:订单状态只能从"待支付→已支付",不能回退;工资只能涨不能降。
  3. 跨表关联计算约束:下单数量 ≤ 库存表当前数量;订单总额 = 明细表金额之和。
  4. 复杂条件约束:VIP 用户折扣不得低于 0.5,普通用户不得低于 0.8(取值依赖另一字段且含分支)。

要点:凡是"要看别的行 / 别的表 / 旧值 / 统计结果 / 多字段联合判断"的,基本都是间接约束。


第 8 题 一句话解释:为什么"直接/间接"和"实体/参照/用户定义/域"是两套不冲突的分类?并举一个同一条规则既能直接、又能间接实现的例子。
👉 查看参考答案

参考答案:

因为它们分类的角度不同 :四大类型按约束的**内容(约束什么)分,直接/间接按约束的实现方式(怎么保证)**分,二者正交、可以交叉组合。

例子 :"订单的 user_id 必须存在于用户表" 这条参照完整性规则------

  • 启用外键时:用 FOREIGN KEY 实现 → 直接约束;
  • 分库分表禁用外键时:在 Service 层查一次用户是否存在 → 间接约束

同一条规则,内容分类不变(参照完整性),实现分类却随手段改变(直接 ↔ 间接)。


学完自检 :看到任意一条业务规则,你能立刻说出

①它属于四大类型里的哪一类;②它该用直接约束还是间接约束实现;③为什么。

三个都能答上来,这个知识点就过关了。