【数据库系统原理】第5篇:关系的完整性约束:实体、参照与用户定义的逻辑守卫
目录一、完整性约束的哲学数据库的逻辑免疫系统二、域完整性属性取值的边界守夜人三、实体完整性确保每一行的可身份性四、参照完整性跨越关系边界的逻辑契约五、用户定义完整性业务的边界守卫六、约束的声明性与检查时机理论与实践七、结语约束即信任一、完整性约束的哲学数据库的逻辑免疫系统一个数据库在运行周期中会经历无数次数据修改——插入新记录、删除旧记录、更新字段值。每一次修改都可能引入错误将年龄填为负数将性别填为不存在的编码将订单归属到不存在的顾客名下或者留下一个主码为空的“幽灵记录”。在文件系统时代防范这些错误的责任完全落在应用程序员肩上——每个程序都要独立校验数据一个程序的疏漏就可能导致数据库的慢性腐坏。数据库系统引入了完整性约束机制将数据合法性校验从应用程序中剥离出来内嵌为数据库引擎的一项系统级职责。完整性约束的精髓在于它不是在事后检查数据而是根本不接受不合法的数据进入数据库。任何试图违反约束的插入、更新或删除操作都会在提交前被系统拦截整个事务被回滚。这种“先验式拒绝”机制使得数据库在任何时刻都处于一个逻辑自洽的状态——无论上层应用犯了什么错误数据库内部的数据结构都不会崩坏。从形式化的角度看完整性约束是施加在数据库实例上的谓词——如果数据库实例使得所有约束谓词为真则该实例是合法的否则非法。完整性约束分为三类域完整性、实体完整性和参照完整性。这三类约束分别作用于属性的取值空间、元组在关系内部的唯一可标识性、以及关系之间的逻辑纽带共同构成数据库的“逻辑免疫系统”。二、域完整性属性取值的边界守夜人域完整性Domain Integrity是最基础层面的约束它规定每个属性的取值必须落在其所基于的域之内。形式化地设关系模式R的属性A基于域D则对于R的任意合法实例r中的任意元组tt[A] ∈ D 必须恒成立。域完整性的约束来源有三。其一数据类型约束——整数字段不能接受字符串日期字段不能接受语法错误的日期。这是最底层的约束由数据库管理系统的类型系统强制执行。其二取值范围约束——即便类型正确取值也可能越界。例如年龄字段的类型是整数但-5和300虽然在整数域内却不在“合理年龄”的范围内。这类约束通过CHECK子句或自定义域来声明。其三格式约束——某些字段的取值必须符合特定的正则模式如邮箱地址、身份证号的校验规则。域完整性虽然基础却常常在实践中被草率对待。一个典型的不良实践是为了“灵活”而将所有字符串字段都设为VARCHAR(255)将数值字段都设为不限制精度的类型将本应使用专用枚举域的值用松散的自由文本存储。这种做法虽然避免了“字段长度不够”的即时报错却将数据校验的责任完全推给了应用程序数据库沦为被动存储容器丧失了作为数据质量第一道防线的作用。正确的态度是能精确就别模糊能在数据库层声明约束就尽早声明。每一个精确的域声明都是对未来某次数据污染的一次预防注射。三、实体完整性确保每一行的可身份性实体完整性Entity Integrity的表述简明而强硬在任何关系实例中主码的任何组成属性都不得取空值NULL。这条规则的前置概念已在上一篇详述——主码是设计者选定的首要标识符它的职责是无可推卸地唯一标识关系中的每一个元组。如果主码为空对应的元组就失去了最小意义上的可标识性成为一个“存在但无法被指称”的逻辑幽灵。更严重的是如果主码部分为空——即复合主码中某几个属性有值而某几个为空——那么该元组的身份就处于一种悬而未决的状态外码的引用将无法完成匹配。在SQL标准中创建表时声明PRIMARY KEY将自动隐含两重约束列值非空NOT NULL和列值唯一UNIQUE。但值得注意的是SQL允许候选码包含可空列UNIQUE约束在大多数实现中允许多个NULL值并存因为NULL不被视为等于NULL。这一点与关系模型的理论要求存在微妙偏差——理论中候选码的所有属性也必须非空否则无法充当身份标识。从实践角度建议设计者对任何可能成为被引用目标的列施加NOT NULL约束即便它在SQL语法上并非强制。实体完整性的一个延伸问题是主码的不可变性。主码值一旦分配给某个元组在元组的整个生命周期中应当保持不变。主码的修改会引发连锁反应——所有引用该主码的外码都需要同步更新在级联策略不当或并发操作干扰下这极易引发数据不一致。因此选择主码时“稳定性”是仅次于“唯一性”的第二考量优先选择那些在业务生命周期中几乎不会变化的属性如系统生成的UUID或自增序列而非业务含义可能变更的自然属性如手机号、邮箱。四、参照完整性跨越关系边界的逻辑契约参照完整性是三类约束中逻辑深度最深、工程处理最复杂的一类。上一篇我们给出了外码的形式化定义设FK是关系R₁的外码参照关系R₂的主码PK则R₁中任意元组在FK上的取值要么全为空要么在R₂中存在一个元组其PK取值与之匹配。这一规则确保了一条跨关系的指涉链条始终能够追溯到被指涉的目标实体。在具体实践中参照完整性面临两个核心难题空值的多重语义以及被参照记录被删除或更新时外码记录的命运。空值的语义困境。外码允许空值但空值在外码上的语义是模糊的。当一个学生的“导师工号”为空它可能意味着该学生尚未分配导师“未知”也可能意味着该学生明确不需要导师“不适用”还可能意味着数据暂时缺失“待补充”。这三种语义在空值中被压平为同一种表现形式。如果业务逻辑需要区分“无”和“未知”就不能仅依赖空值来实现而需要通过额外的状态字段或约定特殊标记值来显式表达。这一困境暴露了关系模型在处理缺失信息时的理论局限——科德本人后来提出了四值逻辑的设想但从未在主流产品中落地。级联操作策略。当R₂中的一个元组被删除或者其主码值被更新时R₁中所有引用了该元组的外码记录将面临逻辑上的危机。数据库管理系统提供了四种标准策略供设计者选择CASCADE级联策略删除R₂记录时自动删除R₁中所有引用该记录的外码记录更新R₂的主码时自动将R₁中外码的对应值同步更新。这一策略适用于R₁中的记录对R₂中的记录具有“存在依赖性”的场景——例如订单明细表中的记录离开订单主表就毫无意义当订单被删除时订单明细理应随之消失。SET NULL置空策略删除或更新R₂记录时将R₁中对应外码的值设为NULL。这一策略适用于关联是“可选的”场景——例如一个课程可能由一位教师授课当教师的记录被删除时该课程不应随之消失而应回到“尚未分配教师”的状态。使用此策略的前提是外码列允许空值。SET DEFAULT设默认值策略删除或更新R₂记录时将R₁中外码的值设为一个预设的默认值。例如所有员工属于某个部门当某部门被撤销时其下属员工自动划归一个名为“待分配”的默认部门。这一策略要求默认值对应的记录在R₂中真实存在否则将落入参照完整性的陷阱之中。RESTRICT / NO ACTION限制 / 无动作策略如果R₁中存在引用某R₂记录的元组则禁止删除该R₂记录或修改其主码操作直接报错。这一策略是最保守、最安全的选择——它从根本上杜绝了级联效应可能带来的大面积数据变更迫使应用程序先行处理所有引用记录再删除被引用记录。RESTRICT与NO ACTION在标准SQL中存在微妙差异主要涉及约束检查的时机是在语句执行过程中还是事务结束时在大多数数据库产品的实际表现中二者行为相近。级联策略的选择绝非纯技术决策它携带着沉重的业务语义承诺。如果将策略设为CASCADE你实际上是在声明“R₁中的这些记录没有独立的生存权它们的命运随R₂中的记录起伏。”如果这个声明与业务现实不符某天一位操作员误删了一条R₂记录级联删除将以不可逆的方式吞没R₁中的大量数据。策略选择必须建立在对业务关系的深刻理解之上不可因“方便”而轻率决定。五、用户定义完整性业务的边界守卫域完整性、实体完整性和参照完整性是关系模型内在的、普适性的约束。但真实世界的业务场景远比模型预想的丰富——一家航空公司要求行李重量不得超过32公斤一家银行要求账户余额不得低于零一个论坛要求发帖内容不能为空字符串。这些规则与数据模型无关却与业务逻辑息息相关。数据库系统通过用户定义完整性User-Defined Integrity机制来承载这一类约束。用户定义完整性最直接的实现途径是CHECK约束——在定义表结构时声明一个布尔表达式系统在每次插入或更新操作时求值该表达式表达式为假则拒绝操作。CHECK约束可以涉及本行的多个列如“结束日期必须大于开始日期”但不能跨表引用——跨表约束属于参照完整性的范畴或者需要通过触发器来实现。触发器和存储过程提供了比CHECK约束更强大的用户定义完整性表达能力但它们也模糊了“数据库层约束”与“应用程序逻辑”之间的边界。将过于复杂的业务规则下沉到数据库层可能导致数据库逻辑膨胀、可测试性下降、跨数据库迁移困难。设计者需要清醒地判断哪些规则是数据逻辑本身不可分割的一部分如“年龄不得为负”哪些规则属于上层业务逻辑的自然领地如“VIP客户在生日当天享受的折扣率”。前者应当留在数据库中作为约束声明后者更适合在应用层实现并辅以充分的测试覆盖。六、约束的声明性与检查时机理论与实践完整性约束在SQL标准中被定义为声明性的——设计者只需声明约束应该满足什么条件而不必指定系统如何去检查这些条件。数据库管理系统会保证在每一次可能违反约束的操作发生时自动执行检查。这种声明性将约束的维护成本从应用程序开发者转移到了数据库引擎是数据库系统提供的一项核心价值。约束检查的时机有两种模式立即检查与延迟检查。立即检查在每条SQL语句执行后立即校验约束违反则立即回滚延迟检查允许约束在整个事务提交时才进行校验。大多数数据库系统默认采用立即检查模式部分系统支持延迟检查通常仅限于外码约束。延迟检查在某些批处理场景中有其用武之地——例如批量导入一批相互引用的记录时中间状态可能暂时违反外码约束但只要事务提交时所有引用都已修正整体仍合法。不过延迟检查增加了事务管理的复杂性多数日常应用无需涉及。七、结语约束即信任完整性约束的最终受益者不是数据库管理员而是所有依赖数据库的应用程序和业务决策者。一个严格执行完整性约束的数据库向所有使用者承诺这里存储的数据在结构层面是可信的——没有幽灵记录没有孤儿引用没有越界的取值没有重复的身份。这种信任是数据仓库、数据分析乃至所有数据驱动决策的基础。当我们从纯粹的数学定义走向工程实践下一篇文章将转向关系代数——在完整性约束守护的逻辑框架之内我们如何高效地从关系中提取所需的数据以及这些操作的数学基础如何让查询优化成为可能。