使用约束强制实施数据完整性

已完成

约束和序列对象是设计选择,可防止数据问题发生。 缺少外键约束意味着数据库中可能已存在孤立记录。 稍后添加序列对象以替换标识列需要对所有应用程序进行更改。 应用程序代码可以验证数据,但用户可以通过批量导入、直接查询或跳过验证的新应用程序绕过数据。

数据库约束在引擎级别强制实施规则,因此它们始终适用。 在设计过程中做出的决策(例如要在数据库中强制实施的规则以及是否使用标识列或序列)会影响应用程序生命周期的数据质量。

了解何时使用约束

数据质量问题成本高昂。 数据质量不佳导致业务决策错误、集成失败和合规性违规。 与访问同一数据库的不同应用程序之间可能不一致的应用程序级别验证不同,约束在数据库引擎级别强制实施规则,应用程序代码、即席查询、直接 SQL 脚本或批量导入无法绕过这些规则。 在数据库引擎提交更改之前,每个INSERTUPDATEDELETE操作都必须满足所有定义的约束。

应用数据库约束

约束会在数据质量问题破坏数据库之前进行预防。 下表显示了每个约束类型如何解决特定的数据完整性问题:

问题 约束 Example
孤立记录 外键 防止没有有效客户的订单
重复数据 UNIQUE 停止重复的电子邮件注册
无效数据 CHECK 拒绝负价格或未来的出生日期
缺少关键数据 NOT NULL 防止不完整的记录
引用不一致 外键 跨表维护数据完整性

假设有一家零售公司未在其客户电子邮件列上定义唯一约束。 随着时间的推移,同一客户使用相同的电子邮件地址多次注册。 营销发送促销活动时,一些客户收到了同一封电子邮件的三份副本,增加了成本和损害了客户的信任。 添加 UNIQUE (EmailAddress) 到表定义中本来可以阻止这些重复项的插入。

约束在数据库引擎级别强制实施规则,无论数据如何进入系统,确保数据质量。 可以绕过应用程序验证,因应用程序而异,难以维护。 数据库约束始终强制实施、集中,并提供一个事实来源。

约束可确保数据库级别的数据质量和一致性。

使用主键约束

主键约束 保证唯一的数据并强制实施实体完整性。 指定主键约束时,数据库引擎会自动为主键列创建唯一索引。 表只能包含一个主键约束,并且主键约束中定义的所有列必须定义为 NOT NULL

可以使用约束创建主键 PRIMARY KEY 。 下面是一个示例:

CREATE TABLE Customer (
    CustomerID INT PRIMARY KEY IDENTITY(1,1),
    EmailAddress NVARCHAR(100) NOT NULL
);

使用外键约束

外键约束 通过控制可存储在外键表中的数据来强制执行引用完整性。 如果外键约束使指向外键表中数据的链接失效,则外键约束可防止更改主键表中的数据。

您可以定义级联引用操作,例如CASCADESET NULLSET DEFAULT,以指定当用户尝试删除或更新现有外键所指向的密钥时的处理方式。 虽然不需要 手动为外键列创建索引 ,但它通常很有用,因为外键列经常用于联接条件。

可以通过使用 FOREIGN KEY 约束和 REFERENCES 子句来创建外键。 下面是一个示例:

CREATE TABLE Order (
    OrderID INT PRIMARY KEY IDENTITY,
    CustomerID INT NOT NULL,
    OrderDate DATETIME2,
    FOREIGN KEY (CustomerID) REFERENCES Customer(CustomerID)
);

使用唯一约束

唯一约束 可确保不会在不参与主键的特定列中输入重复值。 与 PRIMARY KEY 约束不同的是,UNIQUE 约束允许 NULL 值。 不过,当与参与 UNIQUE 约束的任何值一起使用时,每列只允许一个空值。 数据库引擎会自动创建唯一的非聚集索引,以强制实施唯一性要求。

可以使用关键字创建唯一约束 UNIQUE 。 下面是一个示例:

CREATE TABLE Product (
    ProductID INT PRIMARY KEY,
    SKU NVARCHAR(50) UNIQUE,
    ProductName NVARCHAR(100)
);

使用检查约束

检查约束 通过限制一个或多个列接受的值来强制实施域完整性。 可以使用任何逻辑表达式创建CHECK约束,该表达式根据逻辑运算符返回TRUEFALSE。 可以将多个 CHECK 约束应用于单个列,也可以对多个列应用单个 CHECK 约束。

由于空值的计算结果为 UNKNOWN,它们在表达式中的存在可能会覆盖约束。 例如,INT 列上的约束 MyColumn = 10 仍然允许插入 NULL,因为 NULL 的计算结果不为 FALSE

可以通过将 CHECK 关键字与逻辑表达式结合使用来创建 CHECK 约束。 下面是一个示例:

CREATE TABLE Employee (
    EmployeeID INT PRIMARY KEY,
    HireDate DATE,
    Salary DECIMAL(10,2),
    CHECK (Salary >= 20000),
    CHECK (HireDate <= GETDATE())
);

使用默认约束

INSERT操作期间未指定任何值时,默认约束会提供默认值。 使用数据库项目时,建议使用显式名称创建约束,而不是允许系统生成的名称,这在环境中有所不同。

可以使用关键字创建 DEFAULT 约束 DEFAULT 。 下面是一个示例:

CREATE TABLE Activity (
    ActivityID INT PRIMARY KEY IDENTITY,
    Description NVARCHAR(200),
    CreatedDate DATETIME2 CONSTRAINT DF_Activity_CreatedDate DEFAULT GETUTCDATE(),
    IsActive BIT CONSTRAINT DF_Activity_IsActive DEFAULT 1
);

使用序列对象

序列对象是用户定义的架构绑定对象,根据创建序列的规范生成数值序列。 与标识列不同,序列不与特定表相关联。 应用程序引用序列对象来检索其下一个值,序列和表之间的关系由应用程序控制。

当需要为单个表自动编号时,标识列非常适用。 但是,它们仅限于该表。 不能跨多个表共享数字,在插入行之前获取下一个值,或轻松重置计数器。 序列对象通过独立于任何表生成数字来解决这些问题。

了解何时使用序列

在以下情况下将使用序列,而非标识列:

  • 共享数字系列 - 应用程序需要在多个表或表中的多个列之间共享一系列数字。
  • 循环编号系列 - 当达到指定数字时,应用程序必须重启数字系列。 例如,在分配值 1 到 10 后,应用程序再次开始分配值 1 到 10。
  • 已排序的序列值 - 应用程序要求按另一个字段对序列值进行排序。 NEXT VALUE FOR 函数可以应用OVER 子句,该子句保证按照ORDER BY 子句的顺序生成返回的值。
  • 保留多个数字 - 应用程序需要同时保留多个顺序数字。 如果其他进程同时被分配编号,请求标识值可能会导致编号不连续。 调用 sp_sequence_get_range 一次检索序列中的多个数字。
  • 可更改的规范 — 创建后需要更改序列的规范,例如增量值。

序列对象比标识列更具灵活性:

功能 / 特点 序列 身份
绑定到表 是的
在多个表或列之间共享 是的
在进行插入操作之前获取下一个值 是的
自定义最小值/最大值 是的 受限制
一次性检索多个数字 是的
在指定次数循环/重启 是的
按另一个字段对值进行排序 是的
创建后更改增量 是的

当您需要为单个表提供一个简单的自动递增主键,而且不需要在多个表之间共享相同的编号序列或者无需在插入行之前检索下一个值时,请使用标识列。

当应用程序在进行插入前需要数字、需要在多个表之间共享单个序列、在达到指定数字时必须重新启动数字系列,或者需要一次保留多个顺序数字时,请使用序列。

了解序列限制

与标识列不同,在插入表后 ,序列值不会自动受到保护 。 此外,不会自动为序列值强制实施唯一性。 如果表中的序列值必须唯一,请对列创建唯一约束。

序列号在当前事务的作用域之外生成。 无论使用序列号的事务是提交还是回滚,序列号都会被消耗。

可以使用带 CREATE SEQUENCE 可选参数的语句创建序列对象,以便开始、递增和范围。 下面是一个示例:

-- Create sequence
CREATE SEQUENCE OrderNumber 
    START WITH 1000
    INCREMENT BY 1
    MINVALUE 1000
    MAXVALUE 999999
    NO CYCLE;

-- Use sequence in INSERT with NEXT VALUE FOR function
INSERT INTO Order (OrderID, CustomerID, OrderNumber, OrderDate)
VALUES (1, 100, NEXT VALUE FOR OrderNumber, GETDATE());

-- Get next value before INSERT
DECLARE @NextOrderNum INT = NEXT VALUE FOR OrderNumber;
SELECT @NextOrderNum;

-- Get multiple sequence numbers at once for batch processing
DECLARE @FirstSeq INT, @LastSeq INT;
EXEC sp_sequence_get_range 
    @sequence_name = N'OrderNumber',
    @range_size = 100,
    @range_first_value = @FirstSeq OUTPUT,
    @range_last_value = @LastSeq OUTPUT;

-- Reset sequence
ALTER SEQUENCE OrderNumber RESTART WITH 1000;

此示例创建了一个名为 OrderNumber 的序列,从1000开始,以1递增,并在不循环的情况下于到达999999前停止。 当 NEXT VALUE FOR 函数检索下一个可用数字时,可以在INSERT语句中内联使用,也可以在插入之前将其分配给变量,具体取决于应用程序是否需要首先引用该值。 对于一次性需要多个顺序数的批处理作, sp_sequence_get_range 保留一个 100 个数字块,返回范围中的第一个值和最后一个值。 该 ALTER SEQUENCE 语句在需要时将序列重置回 1000。

约束是体系结构中的决策,能够在问题发生之前加以预防。 缺少 CHECK 约束允许无效数据以无提示方式损坏数据库。 当需要跨表编号时选择标识列,会迫使在应用程序级别采取变通方法。 无论哪个应用程序、工具或脚本访问数据库,在数据库级别定义的约束都会保护数据质量。 这些决策可塑造应用程序生命周期的数据完整性保证。