数据库系统工程师考试里,有一个知识点年年出现、分值稳定、却总是被考生轻视:完整性约束。很多人备考时把精力全部压在范式、事务、SQL 查询上,遇到"以下哪项不属于关系模型的完整性约束"这类题目,反而在干扰项面前犹豫半天。命题人恰恰看准了这一点,把"元组完整性""完整性等于 ACID 的 I"之类的干扰项年年往卷子上放。这篇文章从概念定义出发,深入 DBMS 的约束检查机制,把三大完整性的底层原理、五种参照动作、八类高频陷阱和历年真题一次性讲透。
数据库领域对关系模型的定义有一个经典表述:关系模型由关系数据结构、关系操作集合和关系完整性约束三部分组成。数据结构指二维表,操作指选择、投影、连接等关系代数与关系演算,而完整性约束则是关系模型的第三条支柱,缺了它,前两部分就只是形式框架,无法保证数据的语义正确。教材对数据库完整性的正式定义是:数据库的完整性指数据的正确性和相容性。正确性要求数据符合现实世界语义,取值落在合法范围之内;相容性要求同一对象存储在不同关系中的数据保持一致,不出现自相矛盾。
具体到关系层面,完整性约束被划分为三个类别,这是软考的标准分类:实体完整性、参照完整性、用户定义完整性。实体完整性约束基本关系的属性取值,参照完整性约束关系之间的联系,用户定义完整性约束具体应用环境的业务语义。这三类约束分别对应三种不同的错误来源:实体完整性防止同一实体被重复记录或无法标识,参照完整性防止引用悬空,用户定义完整性防止业务上不允许的取值混入。理解这一点,就抓住了三大完整性的本质分工。
教材对这一概念还有一个更本质的表述:完整性规则是关系模型必须满足的最低语义要求,它不随具体应用的改变而改变。这句话有两层含义。其一,完整性是模型内置的规则,而不是某个企业临时附加的约定,任何建立在关系模型之上的数据库管理系统都必须提供完整性支持;其二,完整性约束规定的是数据合法性的下限,在此之上业务还可以追加更严格的约束,但下限本身不可突破。理解这个定位,就能明白为什么软考把完整性约束与关系操作、关系数据结构并列,而不是当作 SQL 语法的附属品。
正确性这一维度最容易理解。一张会员表的年龄字段出现负值,一张订单表的金额字段出现空值,这些都是数据不正确。正确性由取值范围、取值类型、非空要求共同刻画,具体落到 SQL 层面就是 NOT NULL、UNIQUE、CHECK 这三类声明。相容性这一维度则要复杂一些。典型场景是订单表与订单明细表:明细行必须挂在某个存在的订单之下,否则数据库中就出现了没有主人的明细记录,这种"孤儿记录"就是相容性被破坏的结果。相容性由外键约束来保证,其本质是维护关系之间的引用一致性。除此之外,相容性还有一层更隐蔽的含义:同一事实在冗余存储时不发生矛盾。比如一张表存了会员余额,另一张表存了消费流水,若两者对不上,也是相容性被破坏。这层含义在软考中通常与一致性概念关联考查,考生需要辨别,完整性约束是手段,数据正确相容是目的,手段与目的之间不能画等号。
需要强调的是,完整性与安全性是两个不同的概念。完整性防的是错误的数据进入数据库,无论这种错误是无意造成的还是有意制造的;安全性防的是非法用户的越权操作,手段是用户标识与鉴别、存取控制、视图、审计和加密。GRANT 与 REVOKE 语句属于安全性范畴而非完整性范畴,这个区分本身就是软考的考点。此外,事务的 ACID 特性中,I 代表隔离性而非完整性,这也是命题人最常用的干扰项之一,后文误区部分会展开。
约束不是写在数据字典里的装饰品,DBMS 在每条插入、更新、删除语句执行时都会触发约束检查引擎。以实体完整性为例,当一条 INSERT 语句试图插入一个主键值与已有元组重复的记录时,DBMS 并不会傻到全表扫描去比对主键,而是借助主键背后自动建立的唯一索引完成查重。多数商用数据库为主键建立 B+ 树索引,查重复杂度从线性降为对数级,这正是主键索引同时承担查询加速与完整性检查双重职责的原因。若查重发现冲突,语句被整体拒绝并报错,事务中该语句之前的修改是否回滚,取决于事务的提交语义。
约束检查的具体内容随语句类型不同而分工明确。INSERT 语句要过四道关:主键查重、非空检查、CHECK 表达式求值、外键到父表的引用验证,任何一道关不过,整条语句被拒绝。DELETE 语句只查一处:是否有子表行正在引用将被删除的行,若有且参照动作是拒绝类型,删除失败。UPDATE 语句最复杂,如果修改的是主键列,相当于删除旧值再插入新值,两侧检查都要执行;如果修改的是外键列,则要重新验证新值在父表中的存在性;如果修改的只是普通列,则仅执行 CHECK 与非空检查。软考题目有时会问插入子表行时,DBMS 要到父表的主键索引中查找被引用的主码值是否存在,这一步决定了外键列上建索引的重要性,否则每次插入都要在父表上做全表扫描,还会在被参照表上产生额外的共享锁竞争。删除或更新父表行时,DBMS 则要根据外键声明中指定的参照动作决定子表的联动行为。理解了这个双向检查机制,就理解了为什么滥用外键会拖慢写入性能,也理解了为什么数据仓库的事实表通常不建外键。
SQL 标准规定了约束检查的两种时机:立即执行与延迟执行。立即约束在每条语句执行完毕后就立即检查,默认的 NOT NULL、UNIQUE、CHECK 都属于此类;延迟约束则把检查推迟到事务提交时刻,由 INITIALLY DEFERRED 子句声明。延迟约束存在的意义是支持合法的一过性中间状态。设想批量导入一批订单,订单主表与明细表数据交错写入,若要求每条语句立即检查,先插入的明细行必然因父行尚未写入而报错。把外键约束声明为可延迟后,整个导入事务在提交时才做一次性检查,中间状态的暂时悬空被允许,这是教科书上经典的延迟约束应用场景。软考一般不考语法细节,但立即检查与延迟检查的存在,解释了为什么有些约束必须等到事务结束才"发作",遇到相关题目时能帮助排除干扰项。
完整性约束有两条实现路线。声明式路线是直接使用主键、外键、CHECK 等约束子句,由 DBMS 自动执行检查,代码量小、不易出错、优化器可见;过程式路线是编写触发器,在事件发生时执行一段程序化逻辑来完成检查或修正。触发器的优势在于灵活,可以跨表引用、可以携带复杂业务逻辑,比如"新增消费记录后自动更新会员余额"这种联动更新就是触发器的主场。但触发器难以调试、增加系统开销,且过深的触发器嵌套会形成级联触发风暴。SQL 标准还提供了断言机制,用于表达涉及多张表的全局约束,不过主流商用数据库对断言的实现支持度参差不齐,实务中多被触发器替代。软考的立场是:能用声明式约束解决的问题优先用约束,约束表达不了的复杂规则再交给触发器,两者不是互斥关系而是互补关系。
实体完整性的标准表述是:若属性 A 是基本关系 R 的主属性,则 A 不能取空值。所谓主属性,就是构成候选码的属性。这个定义比"主键不能为空"更严格也更准确,因为实体完整性保护的对象是所有候选码,而不仅仅是当选为主键的那一个。落实到 SQL 中,实体完整性通过 PRIMARY KEY 子句声明,其语义等价于 NOT NULL 加 UNIQUE 的组合。一个基本关系只能声明一个主键,但可以有多个唯一约束;主键不允许出现空值,唯一约束对空值的处理则因数据库产品而异。实体完整性的现实意义在于让每一行都能被唯一标识,从而为查询定位、索引建立和参照引用提供锚点。订单号、学号、身份证号这些业务上的天然标识,就是实体完整性约束的典型应用。进一步追问,当一个表存在多个候选码时,如何选择主键?工程上的经验法则是三看:看属性个数,选属性最少的,越短越好做索引;看稳定性,选取值不变的,频繁变更的列不适合做主键;看非空性,选业务上必然有值的,可空的列天然与实体完整性冲突。主键可以由多个属性共同组成,称为复合主键,此时实体完整性的要求是复合主键中的每一个属性都不能取空值,因为主属性取空值会让部分元组失去可标识性。唯一约束与主键的另一个微妙差别在于对空值的处理,多数数据库允许唯一约束列出现多个空值,因为空值在唯一性判断中视为互不相同,而个别数据库产品只允许一个空值,这种产品差异正是命题人出细节题的素材。
参照完整性的标准定义是:设 F 是基本关系 R 的一个或一组属性,但不是关系 R 的码,K 是基本关系 S 的主码,若 F 与 K 相对应
本篇完!