数据库完整性

数据库完整性概述

在数据库系统中,不仅要保证数据的「安全性」,防止非法访问和破坏,还需要确保数据的完整性,即数据的正确性有效性相容性

完整性约束是防止不符合现实世界语义或逻辑规则的数据进入数据库的关键机制。

数据库完整性定义

数据库完整性是指数据库中存储的数据在任何时候都必须满足预定义的语义约束条件,确保数据的「正确性」和「相容性」。

  • 正确性(Correctness):指数据符合现实世界的实际情况和语义规则。例如,年龄不能为负数,性别只能是男或女。
  • 相容性(Compatibility):指数据库中同一对象在不同关系(表)中的数据是符合逻辑、相互协调的。例如,学生选课表中的学号必须在学生表中存在。

完整性 vs. 安全性

数据库的完整性与安全性是两个不同的概念,但都对数据库的可靠运行至关重要。

特性 数据库完整性 数据库安全性
目标 防止数据库中存在不符合语义不正确的数据 保护数据库,防止恶意破坏非法存取
防范对象 不合语义、不正确的数据 非法用户、非法操作
控制方式 定义完整性约束规则,进行检查和违约处理 用户身份鉴别、存取控制、加密等

完整性控制机制

关系数据库管理系统(RDBMS)通常提供一套完整的机制来实现数据的完整性,主要包括三个方面:

  1. 定义完整性约束条件:提供用于定义数据必须满足的语义规则或约束的机制。这些规则通常在创建或修改表结构时定义。
  2. 完整性检查:在用户执行 INSERTUPDATEDELETE 等操作,试图修改数据库数据时,DBMS 会自动按照预定义的约束规则进行检查,判断该操作是否会破坏数据完整性。检查时机通常是 DML 语句执行后,也可以设置为事务提交时。
  3. 违约处理:当用户的操作违背了完整性约束条件时,DBMS 会采取预定的处理方式,以保证数据的完整性。常见的处理方式包括:
    • 拒绝执行NO ACTION/RESTRICT):不执行导致违约的操作。这是大多数约束的默认处理方式。
    • 级联操作CASCADE):在参照完整性中,当对被参照表进行删除或更新时,相应地删除或更新参照表中的相关元组。
    • 设置为空值SET NULL):在参照完整性中,当对被参照表进行删除或更新时,将参照表中的相关外键列设置为空值(前提是该列允许为空)。
    • 设置为默认值SET DEFAULT):在参照完整性中,当对被参照表进行删除或更新时,将参照表中的相关外键列设置为预定义的默认值(前提是该列定义了默认值)。

完整性约束的分类

完整性约束可以从不同角度进行分类:

  • 按约束对象分类:
    1. 与表有关的约束:定义在单个表上的约束,是最常见的类型。
      • 列级约束:定义在单个列上,作为列定义的一部分。例如 NOT NULL, UNIQUE, PRIMARY KEY(单列主键), CHECK(仅涉及该列), DEFAULT
      • 表级约束:定义在表级别,可以涉及一个或多个列。例如 PRIMARY KEY(多列主键), FOREIGN KEY, UNIQUE(多列唯一), CHECK(涉及多列)。
    2. 域约束:定义在一个上的约束。域可以看作是具有特定数据类型和约束的「自定义类型」。当多个列需要共享相同的约束时,使用域可以简化定义和维护。通过 CREATE DOMAIN 定义。
    3. 断言:定义独立于任何具体表的、更全局性的约束规则,可以涉及多个表或复杂的查询条件(如聚合)。通过 CREATE ASSERTION 定义。
  • 按约束状态分类(或实现方式)
    1. 静态约束:要求数据库在任何时候都必须满足的约束。大部分通过 PRIMARY KEY, FOREIGN KEY, CHECK, NOT NULL, UNIQUE, DOMAIN, ASSERTION 定义的约束属于此类。DBMS 会在数据修改时自动检查。
    2. 动态约束:在数据库状态发生改变时需要满足的约束,或者需要执行更复杂的业务逻辑来维护数据一致性。这类约束通常通过触发器(Trigger)来实现。触发器是与表关联的、由事件(INSERT, UPDATE, DELETE)驱动的程序代码段。

约束类型概览

  • 核心约束(常用):实体完整性(PRIMARY KEY)、参照完整性(FOREIGN KEY)、用户定义完整性(CHECK, NOT NULL, UNIQUE, DEFAULT)。这些通常在 CREATE TABLEALTER TABLE 中定义。
  • 可重用约束:域约束(CREATE DOMAIN)。
  • 全局/复杂约束:断言(CREATE ASSERTION)。理论上强大,但实践中支持有限且可能低效。
  • 过程式/动态约束:触发器(CREATE TRIGGER)。灵活强大,但增加复杂性且特定于 DBMS。

SQL 中定义完整性约束

SQL 标准提供了丰富的数据定义语言来支持完整性约束的定义,主要在 CREATE TABLEALTER TABLE 语句中实现。

CREATE TABLE 中的约束定义

基本语法结构:

1
2
3
4
5
CREATE TABLE [schema_name.]table_name (
column_name datatype [ DEFAULT default_expr ] [ column_constraint [ ... ] ],
[ column_name datatype [ DEFAULT default_expr ] [ column_constraint [ ... ] ], ... ]
[ table_constraint [ ... ] ]
);

其中 column_constrainttable_constraint 用于定义完整性规则。

列级约束语法片段:

1
2
3
4
5
6
7
8
[ CONSTRAINT constraint_name ]
{ NOT NULL |
UNIQUE |
PRIMARY KEY |
CHECK ( search_condition ) |
REFERENCES referenced_table [ ( referenced_column ) ]
[ ON DELETE aCtIoN ] [ ON UPDATE aCtIoN ]
}
  • 定义在列定义之后,只作用于当前列。
  • 单列主键、单列唯一、非空、针对单列的 CHECK、指向单列主键/唯一的 REFERENCES 可用列级定义。
  • aCtIoN 可选值有:
    • CASCADE:级联删除或更新。
    • RESTRICT:拒绝删除或更新。
    • SET NULL:将外键列设置为 NULL
    • …(可以参考下面)

表级约束语法片段:

1
2
3
4
5
6
7
8
[ CONSTRAINT constraint_name ]
{ UNIQUE ( column_name [, ...] ) |
PRIMARY KEY ( column_name [, ...] ) |
FOREIGN KEY ( column_name [, ...] )
REFERENCES referenced_table [ ( referenced_column [, ...] ) ]
[ ON DELETE aCtIoN ] [ ON UPDATE aCtIoN ] |
CHECK ( search_condition )
}
  • 独立于任何列定义,通常放在所有列定义之后。
  • 必须用于定义多列主键、多列唯一约束、多列外键、涉及多列的 CHECK 约束。
  • 也可用于定义单列约束(提供另一种语法形式)。

CONSTRAINT constraint_name

为约束命名是可选的,但强烈推荐。

  • 优点:便于后续通过 ALTER TABLE 语句修改或删除特定的约束。
  • 命名规范:建议采用能反映约束类型、涉及表和列的命名方式,如 pk_student_sno, fk_sc_sno, chk_student_sage
    • pk_student_sno:表示 Student 表的主键约束,涉及列 Sno
    • fk_sc_sno:表示 SC 表的外键约束,引用 Student 表的 Sno 列。
    • chk_student_sage:表示 Student 表的年龄约束,涉及列 Sage

接下来,我们将详细讨论几种核心的完整性约束。

实体完整性(Entity Integrity)

实体完整性要求关系(表)中的每个元组(行)都是可唯一标识、不可重复的实体。

实体完整性规则

主键(Primary Key)的属性(列)不能取空值NULL),且必须唯一

SQL 定义

CREATE TABLE 中定义主键:

  1. 列级约束(适用于单列主键):
    1
    2
    3
    4
    5
    CREATE TABLE Student (
    Sno CHAR(9) PRIMARY KEY, -- 列级定义
    Sname CHAR(20) NOT NULL,
    -- ... 其他列
    );
  2. 表级约束(适用于单列或多列主键,多列主键必须用表级):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    CREATE TABLE Student (
    Sno CHAR(9),
    Sname CHAR(20) NOT NULL,
    -- ... 其他列
    PRIMARY KEY (Sno) -- 表级定义单列主键
    );

    CREATE TABLE SC (
    Sno CHAR(9) NOT NULL,
    Cno CHAR(4) NOT NULL,
    Grade SMALLINT,
    PRIMARY KEY (Sno, Cno) -- 表级定义多列主键
    );

UNIQUE + NOT NULL

虽然 PRIMARY KEY 是最常用的方式,但技术上也可以通过组合 UNIQUENOT NULL 约束来达到类似效果(定义候选键)。但 PRIMARY KEY 语义更清晰,且一个表只能有一个主键。

检查与违约处理

DBMS 在执行 INSERTUPDATE 操作时,会自动检查:

  1. 主键列的值是否为 NULL
  2. 主键值是否与表中已存在的其他行的主键值重复。

如果违反实体完整性规则(主键为空或重复),DBMS 会拒绝INSERTUPDATE 操作。

性能考量

为了高效地检查主键的唯一性,DBMS 通常会在主键列上自动创建唯一索引(如 B+ 树索引)。这避免了每次插入/更新时进行全表扫描。

参照完整性(Referential Integrity)

参照完整性用于维护表与表之间引用关系的正确性,确保关联数据的一致性。

参照完整性规则

外键(Foreign Key)的值必须满足以下条件之一:

  1. 等于其引用的被参照关系(Referenced Relation)中某个元组的主键值。
  2. 空值NULL),前提是定义外键的列允许为空。
  • 参照关系/表:包含外键的表。
  • 被参照关系/表:外键所引用的主键所在的表。

SQL 定义

CREATE TABLE 中定义外键:

  1. 列级约束(适用于单列外键,且引用单列主键/唯一键):
    1
    2
    3
    4
    5
    6
    7
    8
    CREATE TABLE SC (
    Sno CHAR(9) NOT NULL REFERENCES Student(Sno) -- 列级定义外键 Sno
    ON DELETE CASCADE -- 定义删除时的违约处理
    ON UPDATE CASCADE, -- 定义更新时的违约处理
    Cno CHAR(4) NOT NULL REFERENCES Course(Cno), -- 列级定义外键 Cno(假设 Course 主键是 Cno)
    Grade SMALLINT,
    PRIMARY KEY (Sno, Cno)
    );
  2. 表级约束(适用于单列或多列外键,多列外键必须用表级):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    CREATE TABLE SC (
    Sno CHAR(9) NOT NULL,
    Cno CHAR(4) NOT NULL,
    Grade SMALLINT,
    PRIMARY KEY (Sno, Cno),
    FOREIGN KEY (Sno) REFERENCES Student(Sno) -- 表级定义外键 Sno
    ON DELETE CASCADE
    ON UPDATE CASCADE,
    FOREIGN KEY (Cno) REFERENCES Course(Cno) -- 表级定义外键 Cno
    ON DELETE NO ACTION -- 显式指定删除时拒绝(通常是默认)
    ON UPDATE CASCADE
    );

检查与违约处理

参照完整性检查发生在以下情况:

  1. 对参照表(如 SC)进行 INSERTUPDATE 操作:检查外键值(如 SC.Sno)是否在被参照表(如 Student)的主键列(Student.Sno)中存在,或者是否为 NULL(如果允许)。若不满足,则拒绝操作。
  2. 对被参照表(如 Student)进行 DELETEUPDATE 操作(修改主键值):检查该操作是否会导致参照表(如 SC)中的外键失去引用。若会,则根据定义的违约处理规则执行相应动作:
    • NO ACTION/RESTRICT拒绝在被参照表上的删除或更新操作。这是很多 DBMS 的默认行为。
      • RESTRICT 通常表示立即检查
      • NO ACTION 可能表示语句结束或事务提交时检查
      • 具体行为依赖于 DBMS 和 SQL 标准版本
    • CASCADE(级联):
      • ON DELETE CASCADE:删除被参照表的元组时,同时删除参照表中所有引用该元组的元组。
      • ON UPDATE CASCADE:更新被参照表的主键值时,同时更新参照表中所有引用该旧值的元组的外键值(更新为新主键值)。
    • SET NULL(设置为空):
      • 要求外键列必须允许为 NULL
      • ON DELETE SET NULL:删除被参照表的元组时,将参照表中所有引用该元组的外键列设置为空值NULL)。
      • ON UPDATE SET NULL:更新被参照表的主键值时,将参照表中所有引用该旧值的外键列设置为空值NULL)。
    • SET DEFAULT(设置为默认值):
      • 要求外键列必须定义了 DEFAULT 值。
      • ON DELETE SET DEFAULT:删除被参照表的元组时,将参照表中所有引用该元组的外键列设置为其默认值
      • ON UPDATE SET DEFAULT:更新被参照表的主键值时,将参照表中所有引用该旧值的外键列设置为其默认值

参照完整性违约处理示例

假设有 Student 表和 SC 表,SC.Sno 是参照 Student.Sno 的外键。

  • 如果定义为 ON DELETE RESTRICT,试图删除一个已被 SC 表引用的 Student 记录时,操作会被拒绝。
  • 如果定义为 ON DELETE CASCADE,删除一个 Student 记录时,所有该学生在 SC 表中的选课记录也会被自动删除。
  • 如果定义为 ON DELETE SET NULL (且 SC.Sno 允许 NULL),删除一个 Student 记录时,SC 表中对应记录的 Sno 列会被置为 NULL

外键列是否允许 NULL

定义参照完整性时,需要考虑外键列是否允许为 NULL

  • 如果外键是主键的一部分(如 SC 表中的 SnoCno),则根据实体完整性,它们不能为 NULL
  • 如果外键不是主键的一部分,是否允许 NULL 取决于业务逻辑
  • SET NULL 策略要求外键列必须允许为 NULL

用户定义完整性(User-Defined Integrity)

用户定义完整性是指针对特定应用场景和业务规则的数据约束,它补充了实体完整性和参照完整性未能覆盖的语义要求。

SQL 定义

主要通过以下约束实现:

  1. NOT NULL:约束列的值不能为空。
    1
    2
    3
    4
    5
    6
    CREATE TABLE Student (
    Sno CHAR(9) PRIMARY KEY, -- 主键隐含 NOT NULL
    Sname CHAR(20) NOT NULL, -- 姓名不允许为空
    Sage SMALLINT,
    Ssex CHAR(2)
    );
  2. UNIQUE:约束列(或列组)的值必须唯一。允许存在 NULL 值,但 NULL 值本身的处理方式可能因 DBMS 而异(通常允许多个 NULL,因为 NULL 不等于 NULL)。
    1
    2
    3
    4
    5
    CREATE TABLE Department (
    DeptNo NUMERIC(2) PRIMARY KEY,
    DeptName CHAR(20) UNIQUE NOT NULL, -- 部门名称唯一且非空
    Location CHAR(10)
    );
  3. CHECK:定义一个布尔表达式,表中所有行都必须满足该表达式(结果为 TRUEUNKNOWN,不能为 FALSE)。
    • 列级 CHECK:表达式仅涉及当前列。
      1
      2
      3
      4
      5
      CREATE TABLE Student (
      -- ...
      Ssex CHAR(2) CHECK (Ssex IN ('男', '女')), -- 性别只能是「男」或「女」,不能是「娚」
      Sage SMALLINT CHECK (Sage >= 15 AND Sage < 50) -- 年龄范围约束
      );
    • 表级 CHECK:表达式可以涉及同一行中的多个列。
      1
      2
      3
      4
      5
      6
      7
      CREATE TABLE Student (
      -- ...
      Ssex CHAR(2),
      Sname CHAR(20) NOT NULL,
      -- ...
      CHECK (Ssex = '女' OR Sname NOT LIKE 'Ms.%') -- 如果性别是男,名字不能以 Ms. 开头
      );
  4. DEFAULT:为列指定默认值。当 INSERT 语句未提供该列的值时,将使用默认值。这不是严格意义上的约束,但与数据有效性相关。
    1
    2
    3
    4
    5
    CREATE TABLE Orders (
    OrderID INT PRIMARY KEY,
    OrderDate DATE DEFAULT CURRENT_DATE, -- 订单日期默认为当前日期
    Status VARCHAR(10) DEFAULT 'Pending'
    );

检查与违约处理

  • NOT NULL:在 INSERTUPDATE 时检查,若试图插入 NULL 或将值更新为 NULL,则拒绝操作。
  • UNIQUE:在 INSERTUPDATE 时检查,若试图插入或更新的值与已有值重复(非 NULL 值),则拒绝操作。
  • CHECK:在 INSERTUPDATE 时检查,若操作后的行不满足 CHECK 条件(表达式为 FALSE),则拒绝操作。
  • DEFAULT:在 INSERT 时,若未指定列值,则自动填入默认值

约束的命名与修改

为约束命名(使用 CONSTRAINT constraint_name 子句)非常重要,它使得后续管理约束(如修改或删除)更加方便。

命名约束

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE Student (
Sno NUMERIC(6)
CONSTRAINT pk_student PRIMARY KEY, -- 命名主键约束
Sname CHAR(20)
CONSTRAINT nn_student_sname NOT NULL, -- 命名非空约束
Sage NUMERIC(3)
CONSTRAINT chk_student_sage CHECK (Sage < 30), -- 命名 CHECK 约束
Ssex CHAR(2)
CONSTRAINT chk_student_ssex CHECK (Ssex IN ('男', '女')),
Deptno NUMERIC(2)
CONSTRAINT fk_student_deptno REFERENCES Department(DeptNo) -- 命名外键约束
);

修改约束(ALTER TABLE

可以使用 ALTER TABLE 语句添加或删除已命名的约束。修改约束通常通过先删除旧约束再添加新约束的方式实现。

  • 添加约束
    1
    2
    3
    -- 添加一个新的 CHECK 约束
    ALTER TABLE Student
    ADD CONSTRAINT chk_student_sage_upper CHECK (Sage < 40);
  • 删除约束
    1
    2
    3
    -- 删除之前定义的年龄约束
    ALTER TABLE Student
    DROP CONSTRAINT chk_student_sage;

修改约束示例

Student 表的学号(Sno)范围从 90000-99999 改为 900000-999999,年龄限制从 < 30 改为 < 40。假设原约束名为 C1C3

1
2
3
4
5
6
7
8
9
– 1. 删除旧的学号范围约束 C1
ALTER TABLE Student DROP CONSTRAINT C1;
– 2. 添加新的学号范围约束 C1(可以重用名字,或用新名字)
ALTER TABLE Student ADD CONSTRAINT C1 CHECK (Sno BETWEEN 900000 AND 999999);

– 3. 删除旧的年龄约束 C3
ALTER TABLE Student DROP CONSTRAINT C3;
– 4. 添加新的年龄约束 C3
ALTER TABLE Student ADD CONSTRAINT C3 CHECK (Sage < 40);

域约束(Domain Constraint)

(Domain)是用户定义的、带有一组属性(数据类型、默认值、约束)的命名对象。它可以看作是一种自定义的数据类型,用于强制多个列遵守相同的规则。

定义域(CREATE DOMAIN

1
2
3
4
5
6
7
8
9
10
-- 定义一个性别域,包含数据类型和 CHECK 约束
CREATE DOMAIN GenderDomain CHAR(2)
CONSTRAINT gd_chk CHECK (VALUE IN ('男', '女')); -- VALUE 代表使用该域的列的值

-- 定义一个非负整数域,包含数据类型和 CHECK 约束
CREATE DOMAIN NonNegativeInt INT
CHECK (VALUE >= 0);

-- 定义一个部门编号域,包含数据类型和非空约束
CREATE DOMAIN DeptNumber CHAR(4) NOT NULL;

使用域

CREATE TABLE 时,可以直接使用域名代替数据类型。该列将自动继承域上定义的所有约束和默认值。

1
2
3
4
5
6
7
CREATE TABLE Employee (
EmpID INT PRIMARY KEY,
EmpName VARCHAR(50) NOT NULL,
Gender GenderDomain, -- 使用 GenderDomain 域
Salary NonNegativeInt, -- 使用 NonNegativeInt 域
DeptID DeptNumber -- 使用 DeptNumber 域
);

优点

  • 可重用性:一次定义,多处使用。
  • 一致性:确保使用相同域的列具有一致的数据类型和约束。
  • 易维护:修改域定义(如 ALTER DOMAIN ... ADD/DROP CONSTRAINT)会自动影响所有使用该域的列。
1
2
3
4
-- 修改 GenderDomain 的约束,例如改为使用 'M'/'F'
ALTER DOMAIN GenderDomain DROP CONSTRAINT gd_chk;
ALTER DOMAIN GenderDomain ADD CONSTRAINT gd_chk_mf CHECK (VALUE IN ('M', 'F'));
-- 所有使用 GenderDomain 的列(如 Employee.Gender)现在都将遵循新的约束

断言(Assertion)

断言是一种用于定义更复杂、更全局性约束的 SQL 机制。它独立于任何特定的表定义,可以跨越多个表,或者包含聚合函数等复杂条件。

定义断言(CREATE ASSERTION

CREATE ASSERTION assertion_name CHECK ( search_condition );

search_condition 是一个必须始终为真(或 UNKNOWN)的谓词。

示例

限制特定课程的选课人数:

1
2
3
4
5
6
-- 限制「数据库」课程最多只能有 60 名学生选修
CREATE ASSERTION ASSE_SC_DB_NUM CHECK (
60 >= (SELECT COUNT(*)
FROM Course, SC
WHERE SC.Cno = Course.Cno AND Course.Cname = '数据库')
);

限制所有课程的选课人数:

1
2
3
4
5
6
7
8
9
-- 限制每一门课程最多只能有 60 名学生选修
CREATE ASSERTION ASSE_SC_CNUM1 CHECK (
NOT EXISTS (SELECT Cno
FROM SC
GROUP BY Cno
HAVING COUNT(*) > 60)
);
-- 或者使用 ALL(语法可能因 DBMS 而异)
CHECK ( 60 >= ALL (SELECT COUNT(*) FROM SC GROUP BY Cno) )

检查与违约处理

断言创建后,DBMS 理论上会在任何可能影响断言真值的操作(涉及断言中表的 INSERT, UPDATE, DELETE)之后检查断言条件。如果任何操作导致断言的 CHECK 条件变为 FALSE,该操作将被拒绝

断言的实用性问题

尽管断言在理论上非常强大,能够表达复杂的全局约束,但在实际的 RDBMS 产品中,对断言的支持非常有限

  • 支持不完整:很多主流 DBMS(如 MySQL, PostgreSQL, SQL Server, Oracle 的较新版本)要么不支持 CREATE ASSERTION 语法,要么支持的功能受限。
  • 性能开销高:即使支持,检查复杂断言(尤其是涉及多表连接或聚合的)的成本可能非常高,严重影响数据库性能。
  • 替代方案:对于需要复杂约束或跨表检查的场景,触发器通常是更实用、更受支持的替代方案,尽管它们是过程式的,可能增加应用逻辑的复杂性。

因此,在实际开发中,应谨慎使用或避免使用断言,除非你明确知道目标 DBMS 对其有良好且高效的支持。

触发器(Trigger)

触发器是一种特殊的存储过程,它与特定的表相关联,并在该表上发生特定事件(INSERT, UPDATE, DELETE)时自动执行。触发器提供了实现复杂数据约束、业务规则、审计、级联操作等功能的强大机制。

定义触发器 (CREATE TRIGGER)

触发器的语法在不同 RDBMS 中差异较大,以下是 SQL 标准风格的一般结构:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TRIGGER trigger_name
{ BEFORE | AFTER } -- 触发时机
{ INSERT | DELETE | UPDATE [ OF column_name [, ...] ] } -- 触发事件
ON table_name -- 触发器关联的表
[ REFERENCING {
OLD [ROW] [AS] old_alias |
NEW [ROW] [AS] new_alias |
OLD TABLE [AS] old_table_alias |
NEW TABLE [AS] new_table_alias } ] -- 引用旧值/新值
[ FOR EACH { ROW | STATEMENT } ] -- 触发粒度
[ WHEN (condition) ] -- 触发条件
trigger_body -- 触发器执行的动作(通常是一段过程式 SQL 代码块)
  • 触发时机BEFORE/AFTER):指定触发器是在触发事件之前还是之后执行。
    • BEFORE 触发器可以用于检查或修改将要插入/更新的数据;
    • AFTER 触发器通常用于在数据更改后执行后续操作(如审计、级联更新)。
  • 触发事件INSERT/DELETE/UPDATE):指定哪些 DML 操作会激活触发器。
    • UPDATE 可以进一步指定只在特定列被更新时触发(UPDATE OF col1, col2)。
  • 触发粒度FOR EACH ROW/FOR EACH STATEMENT):
    • FOR EACH ROW(行级触发器):触发事件影响的每一行都会执行一次触发器动作体。可以访问该行的旧值OLD)和新值NEW)。
    • FOR EACH STATEMENT(语句级触发器):无论触发事件影响了多少行,触发器动作体都只执行一次。通常不能直接访问单行的 OLD/NEW 值,但可能可以访问受影响行的集合(如通过 OLD TABLE/NEW TABLE,但这部分标准支持和语法差异更大)。
  • REFERENCING 子句:定义别名,用于在触发器动作体中引用被修改行的旧值(OLD)或新值(NEW),或者受影响行的集合(OLD TABLE, NEW TABLE)。
  • WHEN 子句(可选):提供一个额外的条件,只有当此条件为真时,触发器动作体才会被执行。
  • 触发器动作体trigger_body):包含触发器要执行的 SQL 语句(可能是 SELECT, INSERT, UPDATE, DELETE,或者过程式控制语句如 IF, THEN, ELSE 等,具体语法依赖于 DBMS 的过程式 SQL 方言)。

示例

审计:记录重要的成绩修改(行级 AFTER UPDATE

1
2
3
4
5
6
7
8
9
10
-- 当 SC 表的 Grade 列被更新,且新成绩比旧成绩高出 10% 时,记录到 SC_U 表
CREATE TRIGGER SC_T
AFTER UPDATE OF Grade ON SC
REFERENCING OLD ROW AS OldTuple, NEW ROW AS NewTuple
FOR EACH ROW
WHEN (NewTuple.Grade >= 1.1 * OldTuple.Grade)
BEGIN -- 动作体开始(语法可能变化)
INSERT INTO SC_U (Sno, Cno, OldGrade, NewGrade, ChangeTime)
VALUES (OldTuple.Sno, OldTuple.Cno, OldTuple.Grade, NewTuple.Grade, CURRENT_TIMESTAMP);
END; -- 动作体结束

强制约束:教授工资不低于 4000(行级 BEFORE INSERT OR UPDATE

1
2
3
4
5
6
7
8
9
10
-- 在插入或更新 Teacher 表之前,检查是否为教授且工资低于 4000,若是则自动改为 4000
CREATE TRIGGER Insert_Or_Update_Sal
BEFORE INSERT OR UPDATE ON Teacher
REFERENCING NEW ROW AS newTuple
FOR EACH ROW
BEGIN
IF (newTuple.Job = '教授' AND newTuple.Sal < 4000) THEN
SET newTuple.Sal = 4000; -- 修改将要插入/更新的值(需要 DBMS 支持)
END IF;
END;

统计:记录每次插入的学生人数(语句级 AFTER INSERT

1
2
3
4
5
6
7
8
9
-- 每次向 Student 表插入数据后,将插入的学生数量记录到 StudentInsertLog 表
CREATE TRIGGER Student_Count
AFTER INSERT ON Student
REFERENCING NEW TABLE AS InsertedStudents -- 引用插入的行集合(语法可能变化)
FOR EACH STATEMENT
BEGIN
INSERT INTO StudentInsertLog (Numbers, LogTime)
SELECT COUNT(*), CURRENT_TIMESTAMP FROM InsertedStudents;
END;

触发器的执行顺序

如果一个表上定义了多个触发器响应同一事件(如多个 AFTER INSERT 触发器),它们的执行顺序通常遵循:

  1. 执行所有 BEFORE 触发器。
  2. 执行激活触发器的 SQL 语句本身。
  3. 执行所有 AFTER 触发器。

同一类别(BEFOREAFTER)的多个触发器的执行顺序可能依赖于 DBMS(例如,按创建顺序或名称顺序),不应过度依赖特定顺序。

删除触发器(DROP TRIGGER

1
2
DROP TRIGGER trigger_name ON table_name;
-- 语法可能因 DBMS 而异,有时不需要 ON table_name

触发器的优缺点

  • 优点
    • 强大灵活:可以实现非常复杂的约束和业务逻辑。
    • 自动化:自动执行,保证规则实施。
    • 集中管理:将业务规则封装在数据库层面。
  • 缺点
    • 复杂性:可能使数据库逻辑难以理解和调试。
    • 性能影响:设计不当的触发器可能严重影响 DML 操作性能。
    • 可移植性差:触发器的语法和功能在不同 DBMS 之间差异很大。
    • 隐蔽性:触发器的执行是隐式的,可能导致意外行为。

断言 vs. 触发器总结

特性 断言(Assertion) 触发器(Trigger)
类型 声明式 过程式
目标 定义全局、静态的完整性约束 响应事件,执行动作(约束、业务逻辑、审计等)
检查时机 任何可能影响其真值的操作后(理论上) 由事件(INSERT/UPDATE/DELETE)激活,在 BEFORE/AFTER 执行
范围 可跨多个表,检查数据库全局状态 绑定到单个表,响应特定事件
动作 隐含动作:若为 FALSE 则拒绝操作 可执行复杂的 SQL 代码块,包括数据修改、流程控制
复杂性 表达能力强,但实现复杂,检查开销大 实现灵活,但可能增加代码复杂度和维护难度
DBMS 支持 非常有限,性能问题多,不推荐实际使用 广泛支持,但语法不统一,是实现复杂规则的实用方法
跟踪能力 无法跟踪具体更改 可通过 OLD/NEW 访问更改前后的数据,进行跟踪和审计

小结

  • 数据库完整性是保证数据库中数据正确性相容性的关键。
  • RDBMS 提供了一套机制来实现完整性:定义约束检查约束违约处理
  • 主要的完整性约束类型包括:
    • 实体完整性PRIMARY KEY):确保行的唯一标识。
    • 参照完整性FOREIGN KEY):维护表间引用关系。
    • 用户定义完整性NOT NULL, UNIQUE, CHECK, DEFAULT):满足特定业务规则。
    • 域约束DOMAIN):重用列级约束。
  • 断言ASSERTION)是理论上用于定义全局约束的机制,但实践中支持不佳且效率低。
  • 触发器TRIGGER是响应 DML 事件的程序段,是实现复杂约束、业务逻辑和审计的强大而实用的工具,但需注意其复杂性和可移植性问题。
  • 合理运用这些完整性机制,对于构建可靠、高质量的数据库应用至关重要。