SQL Server 2019入门学习教程,从入门到精通,SQL Server 2019 规则、默认值和完整性约束 —— 语法详解与实战案例(11)

SQL Server 2019 规则、默认值和完整性约束 ------ 语法详解与实战案例

⚠️ 重要提醒

  • "规则(Rule)"和"默认值(Default)"是 SQL Server 的遗留特性 ,在新项目中推荐使用 CHECK约束DEFAULT约束
  • 规则和默认值使用 sp_bindrule / sp_bindefault 绑定到列或用户定义数据类型,而约束直接定义在表结构中。
  • 本章仍需掌握,因维护旧系统时会频繁遇到。

一、规则(Rule)------ 已过时,但需了解

📌 规则(Rule) :独立于表的对象,用于限制列或用户定义数据类型的取值范围。

⚠️ SQL Server 2019 仍支持,但微软官方文档标注为"不推荐使用 ",建议用 CHECK约束 替代。


1.1 创建规则

✅ 基本语法:
sql 复制代码
CREATE RULE rule_name AS condition_expression;
  • condition_expression 必须引用 @value 作为占位符
  • 只能引用标量值,不能引用表或函数(除少数系统函数)
📌 案例代码:
sql 复制代码
USE TestDB;
GO

-- 创建性别规则:只能是 'M' 或 'F'
CREATE RULE GenderRule AS
    @value IN ('M', 'F');
GO

-- 创建年龄规则:18-100岁
CREATE RULE AgeRule AS
    @value BETWEEN 18 AND 100;
GO

-- 创建邮箱格式规则(简单验证)
CREATE RULE EmailRule AS
    @value LIKE '%@%.%';
GO

-- 创建成绩规则:0.00 到 100.00
CREATE RULE ScoreRule AS
    @value >= 0.00 AND @value <= 100.00;
GO

-- 查看已创建的规则
SELECT name, create_date, type_desc 
FROM sys.objects 
WHERE type = 'R'; -- R = Rule

1.2 把自定义规则绑定到字段

✅ 基本语法:
sql 复制代码
EXEC sp_bindrule 'rule_name', 'table_name.column_name';
-- 或绑定到用户定义数据类型
EXEC sp_bindrule 'rule_name', 'user_defined_type';
📌 案例代码:
sql 复制代码
-- 创建学生表
IF OBJECT_ID('Students', 'U') IS NOT NULL DROP TABLE Students;
GO

CREATE TABLE Students (
    StudentID INT IDENTITY(1,1) PRIMARY KEY,
    Name NVARCHAR(50) NOT NULL,
    Gender CHAR(1),      -- 将绑定GenderRule
    Age INT,             -- 将绑定AgeRule
    Email NVARCHAR(100), -- 将绑定EmailRule
    Score DECIMAL(5,2)   -- 将绑定ScoreRule
);
GO

-- 绑定规则到列
EXEC sp_bindrule 'GenderRule', 'Students.Gender';
EXEC sp_bindrule 'AgeRule', 'Students.Age';
EXEC sp_bindrule 'EmailRule', 'Students.Email';
EXEC sp_bindrule 'ScoreRule', 'Students.Score';

-- ✅ 验证绑定成功
-- 方法1:查看 sys.columns 和 sys.objects
SELECT 
    c.name AS ColumnName,
    r.name AS RuleName
FROM sys.columns c
JOIN sys.objects r ON c.rule_object_id = r.object_id
WHERE c.object_id = OBJECT_ID('Students');

-- 方法2:尝试插入合法数据(应成功)
INSERT INTO Students (Name, Gender, Age, Email, Score)
VALUES ('张三', 'M', 20, 'zhangsan@school.com', 85.5);

SELECT * FROM Students; -- 应显示插入成功

1.3 验证规则作用

📌 案例代码:
sql 复制代码
-- 尝试插入违反规则的数据(应失败并报错)

-- ❌ 违反性别规则
BEGIN TRY
    INSERT INTO Students (Name, Gender, Age, Email, Score)
    VALUES ('李四', 'X', 22, 'lisi@school.com', 90.0);
END TRY
BEGIN CATCH
    PRINT '❌ 性别规则阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ❌ 违反年龄规则
BEGIN TRY
    INSERT INTO Students (Name, Gender, Age, Email, Score)
    VALUES ('王五', 'F', 17, 'wangwu@school.com', 78.0);
END TRY
BEGIN CATCH
    PRINT '❌ 年龄规则阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ❌ 违反邮箱规则
BEGIN TRY
    INSERT INTO Students (Name, Gender, Age, Email, Score)
    VALUES ('赵六', 'M', 25, 'zhaoliu-at-school', 88.0);
END TRY
BEGIN CATCH
    PRINT '❌ 邮箱规则阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ❌ 违反成绩规则
BEGIN TRY
    INSERT INTO Students (Name, Gender, Age, Email, Score)
    VALUES ('钱七', 'F', 19, 'qianqi@school.com', 105.0);
END TRY
BEGIN CATCH
    PRINT '❌ 成绩规则阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ✅ 插入合法数据(应成功)
INSERT INTO Students (Name, Gender, Age, Email, Score)
VALUES ('孙八', 'F', 21, 'sunba@school.com', 92.0);

PRINT '✅ 合法数据插入成功,当前学生总数:' + CAST(@@ROWCOUNT AS NVARCHAR(10));
SELECT * FROM Students;

1.4 取消规则绑定

✅ 基本语法:
sql 复制代码
EXEC sp_unbindrule 'table_name.column_name';
-- 或
EXEC sp_unbindrule 'user_defined_type';
📌 案例代码:
sql 复制代码
-- 取消邮箱列的规则绑定
EXEC sp_unbindrule 'Students.Email';

-- 验证取消成功
SELECT 
    c.name AS ColumnName,
    r.name AS RuleName
FROM sys.columns c
LEFT JOIN sys.objects r ON c.rule_object_id = r.object_id
WHERE c.object_id = OBJECT_ID('Students') AND c.name = 'Email';

-- ✅ 现在可以插入不符合邮箱格式的数据(不推荐,仅测试)
INSERT INTO Students (Name, Gender, Age, Email, Score)
VALUES ('周九', 'M', 23, 'no-at-sign', 87.0);

SELECT * FROM Students WHERE Name = '周九'; -- 应插入成功

1.5 删除规则

✅ 基本语法:
sql 复制代码
DROP RULE rule_name;

⚠️ 必须先解绑所有绑定,否则删除失败!

📌 案例代码:
sql 复制代码
-- 先解绑所有剩余绑定
EXEC sp_unbindrule 'Students.Gender';
EXEC sp_unbindrule 'Students.Age';
EXEC sp_unbindrule 'Students.Score';

-- 删除所有规则
DROP RULE GenderRule;
DROP RULE AgeRule;
DROP RULE EmailRule;
DROP RULE ScoreRule;

-- 验证规则已删除
SELECT name FROM sys.objects WHERE type = 'R' AND name LIKE '%Rule%';

-- ✅ 现在可以自由插入数据(无规则限制)
INSERT INTO Students (Name, Gender, Age, Email, Score)
VALUES ('吴十', 'Z', 10, 'invalid', -10.0);

SELECT * FROM Students WHERE Name = '吴十'; -- 应插入成功

二、默认值(Default)------ 已过时,但需了解

📌 默认值(Default) :独立于表的对象,为列提供默认值。

⚠️ 同样是遗留特性,推荐使用 DEFAULT约束


2.1 创建默认值

✅ 基本语法:
sql 复制代码
CREATE DEFAULT default_name AS constant_expression;
  • constant_expression 可以是常量、系统函数(如 GETDATE())、NULL
  • 不能是子查询或用户定义函数
📌 案例代码:
sql 复制代码
-- 创建注册日期默认值(当前日期)
CREATE DEFAULT RegDateDefault AS GETDATE();
GO

-- 创建状态默认值
CREATE DEFAULT StatusDefault AS 'Active';
GO

-- 创建国家默认值
CREATE DEFAULT CountryDefault AS '中国';
GO

-- 创建折扣默认值
CREATE DEFAULT DiscountDefault AS 0.00;
GO

-- 查看默认值对象
SELECT name, create_date, type_desc 
FROM sys.objects 
WHERE type = 'D'; -- D = Default

2.2 把自定义默认值绑定到字段

✅ 基本语法:
sql 复制代码
EXEC sp_bindefault 'default_name', 'table_name.column_name';
-- 或绑定到用户定义数据类型
EXEC sp_bindefault 'default_name', 'user_defined_type';
📌 案例代码:
sql 复制代码
-- 创建客户表
IF OBJECT_ID('Customers', 'U') IS NOT NULL DROP TABLE Customers;
GO

CREATE TABLE Customers (
    CustomerID INT IDENTITY(1,1) PRIMARY KEY,
    Name NVARCHAR(100) NOT NULL,
    RegistrationDate DATE,    -- 将绑定RegDateDefault
    Status NVARCHAR(20),      -- 将绑定StatusDefault
    Country NVARCHAR(50),     -- 将绑定CountryDefault
    Discount DECIMAL(5,2)     -- 将绑定DiscountDefault
);
GO

-- 绑定默认值
EXEC sp_bindefault 'RegDateDefault', 'Customers.RegistrationDate';
EXEC sp_bindefault 'StatusDefault', 'Customers.Status';
EXEC sp_bindefault 'CountryDefault', 'Customers.Country';
EXEC sp_bindefault 'DiscountDefault', 'Customers.Discount';

-- ✅ 验证绑定成功
SELECT 
    c.name AS ColumnName,
    d.name AS DefaultName
FROM sys.columns c
JOIN sys.objects d ON c.default_object_id = d.object_id
WHERE c.object_id = OBJECT_ID('Customers');

2.3 插入默认值

📌 案例代码:
sql 复制代码
-- 插入数据时不指定有默认值的列
INSERT INTO Customers (Name)
VALUES ('张三公司');

-- 插入部分列,其余使用默认值
INSERT INTO Customers (Name, Country)
VALUES ('李四商店', '美国'); -- Country被显式指定,其他用默认

-- 使用 DEFAULT 关键字强制使用默认值
INSERT INTO Customers (Name, Status, RegistrationDate)
VALUES ('王五企业', DEFAULT, DEFAULT);

-- 查看结果
SELECT * FROM Customers;
/*
应显示:
- 张三公司:当前日期、'Active'、'中国'、0.00
- 李四商店:当前日期、'Active'、'美国'、0.00
- 王五企业:当前日期、'Active'、'中国'、0.00
*/

2.4 取消默认值的绑定

✅ 基本语法:
sql 复制代码
EXEC sp_unbindefault 'table_name.column_name';
📌 案例代码:
sql 复制代码
-- 取消状态列的默认值绑定
EXEC sp_unbindefault 'Customers.Status';

-- 验证取消成功
SELECT 
    c.name AS ColumnName,
    d.name AS DefaultName
FROM sys.columns c
LEFT JOIN sys.objects d ON c.default_object_id = d.object_id
WHERE c.object_id = OBJECT_ID('Customers') AND c.name = 'Status';

-- 插入数据测试(Status应为NULL)
INSERT INTO Customers (Name) VALUES ('赵六小店');
SELECT * FROM Customers WHERE Name = '赵六小店'; -- Status应为NULL

2.5 删除默认值

✅ 基本语法:
sql 复制代码
DROP DEFAULT default_name;

⚠️ 必须先解绑所有绑定,否则删除失败!

📌 案例代码:
sql 复制代码
-- 解绑剩余默认值
EXEC sp_unbindefault 'Customers.RegistrationDate';
EXEC sp_unbindefault 'Customers.Country';
EXEC sp_unbindefault 'Customers.Discount';

-- 删除默认值对象
DROP DEFAULT RegDateDefault;
DROP DEFAULT StatusDefault;
DROP DEFAULT CountryDefault;
DROP DEFAULT DiscountDefault;

-- 验证删除
SELECT name FROM sys.objects WHERE type = 'D' AND name LIKE '%Default%';

-- ✅ 现在插入数据,未指定列将为NULL
INSERT INTO Customers (Name) VALUES ('钱七工作室');
SELECT * FROM Customers WHERE Name = '钱七工作室'; -- 所有未指定列均为NULL

三、完整性约束 ------ ✅ 推荐使用方式

📌 完整性约束:直接在表定义中声明,是现代SQL的标准做法,功能强大且易于维护。


3.1 主键约束(PRIMARY KEY)

✅ 基本语法:
sql 复制代码
-- 列级约束
column_name data_type PRIMARY KEY

-- 表级约束
PRIMARY KEY (column1, column2, ...)

-- 命名约束
CONSTRAINT constraint_name PRIMARY KEY (column1, column2, ...)
📌 案例代码:
sql 复制代码
-- 创建带主键约束的表
IF OBJECT_ID('Products', 'U') IS NOT NULL DROP TABLE Products;
GO

CREATE TABLE Products (
    ProductID INT CONSTRAINT PK_Products PRIMARY KEY IDENTITY(1,1),
    ProductCode NVARCHAR(20) NOT NULL,
    ProductName NVARCHAR(100) NOT NULL,
    CategoryID INT,
    -- 复合主键示例(注释掉,仅演示)
    -- CONSTRAINT PK_ProductCode_Category UNIQUE (ProductCode, CategoryID)
);
GO

-- 插入数据测试
INSERT INTO Products (ProductCode, ProductName, CategoryID)
VALUES 
    ('P001', '笔记本电脑', 1),
    ('P002', '无线鼠标', 1),
    ('P003', '机械键盘', 1);

-- ❌ 尝试插入重复主键(应失败)
BEGIN TRY
    INSERT INTO Products (ProductID, ProductCode, ProductName, CategoryID)
    VALUES (1, 'P004', '重复ID', 2); -- 主键冲突
END TRY
BEGIN CATCH
    PRINT '❌ 主键约束阻止插入:' + ERROR_MESSAGE();
END CATCH

-- 查看约束信息
SELECT 
    t.name AS TableName,
    c.name AS ConstraintName,
    c.type_desc AS ConstraintType
FROM sys.tables t
JOIN sys.objects c ON t.object_id = c.parent_object_id
WHERE t.name = 'Products' AND c.type = 'PK';

3.2 外键约束(FOREIGN KEY)

✅ 基本语法:
sql 复制代码
-- 列级
column_name data_type REFERENCES parent_table(parent_column)

-- 表级
FOREIGN KEY (column1, column2) REFERENCES parent_table(col1, col2)

-- 命名约束 + 级联操作
CONSTRAINT fk_name FOREIGN KEY (col) REFERENCES parent(col)
    ON DELETE [CASCADE | SET NULL | SET DEFAULT | NO ACTION]
    ON UPDATE [CASCADE | SET NULL | SET DEFAULT | NO ACTION]
📌 案例代码:
sql 复制代码
-- 创建分类表
IF OBJECT_ID('Categories', 'U') IS NOT NULL DROP TABLE Categories;
GO

CREATE TABLE Categories (
    CategoryID INT PRIMARY KEY IDENTITY(1,1),
    CategoryName NVARCHAR(50) NOT NULL UNIQUE,
    Description NVARCHAR(200)
);
GO

-- 重新创建产品表(带外键)
IF OBJECT_ID('Products', 'U') IS NOT NULL DROP TABLE Products;
GO

CREATE TABLE Products (
    ProductID INT PRIMARY KEY IDENTITY(1,1),
    ProductCode NVARCHAR(20) NOT NULL UNIQUE,
    ProductName NVARCHAR(100) NOT NULL,
    CategoryID INT CONSTRAINT FK_Products_Categories 
        FOREIGN KEY REFERENCES Categories(CategoryID)
        ON DELETE SET NULL  -- 删除分类时设为NULL
        ON UPDATE CASCADE,  -- 更新分类ID时级联更新
    Price DECIMAL(10,2) CHECK (Price >= 0),
    Stock INT DEFAULT 0
);
GO

-- 插入分类数据
INSERT INTO Categories (CategoryName, Description)
VALUES 
    ('电子产品', '电脑、手机、配件等'),
    ('家具', '桌椅、沙发、床等'),
    ('图书', '各类书籍');

-- 插入产品数据
INSERT INTO Products (ProductCode, ProductName, CategoryID, Price, Stock)
VALUES 
    ('E001', '笔记本电脑', 1, 5000.00, 50),
    ('E002', '无线鼠标', 1, 150.00, 200),
    ('F001', '办公椅', 2, 1200.00, 30);

-- ❌ 尝试插入无效外键(应失败)
BEGIN TRY
    INSERT INTO Products (ProductCode, ProductName, CategoryID, Price, Stock)
    VALUES ('X001', '无效分类产品', 999, 100.00, 10);
END TRY
BEGIN CATCH
    PRINT '❌ 外键约束阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ✅ 测试级联更新
UPDATE Categories SET CategoryID = 10 WHERE CategoryID = 1;
SELECT * FROM Products WHERE CategoryID = 10; -- 应显示电子产品类别已更新

-- ✅ 测试SET NULL(删除分类)
DELETE FROM Categories WHERE CategoryID = 2;
SELECT * FROM Products WHERE ProductName = '办公椅'; -- CategoryID应为NULL

-- 查看外键约束
SELECT 
    fk.name AS ForeignKeyName,
    OBJECT_NAME(fk.parent_object_id) AS TableName,
    COL_NAME(fkc.parent_object_id, fkc.parent_column_id) AS ColumnName,
    OBJECT_NAME(fk.referenced_object_id) AS ReferencedTable,
    COL_NAME(fkc.referenced_object_id, fkc.referenced_column_id) AS ReferencedColumn,
    fk.delete_referential_action_desc AS OnDeleteAction,
    fk.update_referential_action_desc AS OnUpdateAction
FROM sys.foreign_keys fk
JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
WHERE OBJECT_NAME(fk.parent_object_id) = 'Products';

3.3 唯一性约束(UNIQUE)

✅ 基本语法:
sql 复制代码
-- 列级
column_name data_type UNIQUE

-- 表级
UNIQUE (column1, column2, ...)

-- 命名约束
CONSTRAINT constraint_name UNIQUE (column1, column2, ...)
📌 案例代码:
sql 复制代码
-- 创建员工表(带唯一约束)
IF OBJECT_ID('Employees', 'U') IS NOT NULL DROP TABLE Employees;
GO

CREATE TABLE Employees (
    EmployeeID INT PRIMARY KEY IDENTITY(1,1),
    EmployeeCode NVARCHAR(10) CONSTRAINT UQ_Employees_Code UNIQUE NOT NULL,
    Email NVARCHAR(100) CONSTRAINT UQ_Employees_Email UNIQUE,
    Phone NVARCHAR(20),
    -- 复合唯一约束:姓名+电话组合唯一
    FirstName NVARCHAR(50) NOT NULL,
    LastName NVARCHAR(50) NOT NULL,
    CONSTRAINT UQ_Employees_NamePhone UNIQUE (FirstName, LastName, Phone)
);
GO

-- 插入合法数据
INSERT INTO Employees (EmployeeCode, Email, Phone, FirstName, LastName)
VALUES 
    ('EMP001', 'zhangsan@company.com', '13800138001', '张', '三'),
    ('EMP002', 'lisi@company.com', '13900139002', '李', '四');

-- ❌ 违反唯一约束:重复员工编码
BEGIN TRY
    INSERT INTO Employees (EmployeeCode, Email, Phone, FirstName, LastName)
    VALUES ('EMP001', 'zhangsan2@company.com', '13800138003', '张', '三');
END TRY
BEGIN CATCH
    PRINT '❌ 唯一约束(员工编码)阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ❌ 违反唯一约束:重复邮箱
BEGIN TRY
    INSERT INTO Employees (EmployeeCode, Email, Phone, FirstName, LastName)
    VALUES ('EMP003', 'zhangsan@company.com', '13800138004', '王', '五');
END TRY
BEGIN CATCH
    PRINT '❌ 唯一约束(邮箱)阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ❌ 违反复合唯一约束:姓名+电话重复
BEGIN TRY
    INSERT INTO Employees (EmployeeCode, Email, Phone, FirstName, LastName)
    VALUES ('EMP004', 'wangwu@company.com', '13800138001', '张', '三');
END TRY
BEGIN CATCH
    PRINT '❌ 复合唯一约束(姓名+电话)阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ✅ 合法插入:姓名相同但电话不同
INSERT INTO Employees (EmployeeCode, Email, Phone, FirstName, LastName)
VALUES ('EMP005', 'zhangsan_new@company.com', '13800138005', '张', '三');

SELECT * FROM Employees;

3.4 CHECK约束

✅ 基本语法:
sql 复制代码
-- 列级
column_name data_type CHECK (condition)

-- 表级
CHECK (condition)

-- 命名约束
CONSTRAINT constraint_name CHECK (condition)
📌 案例代码:
sql 复制代码
-- 创建订单表(带多种CHECK约束)
IF OBJECT_ID('Orders', 'U') IS NOT NULL DROP TABLE Orders;
GO

CREATE TABLE Orders (
    OrderID INT PRIMARY KEY IDENTITY(1,1),
    OrderNumber NVARCHAR(20) NOT NULL UNIQUE,
    CustomerID INT NOT NULL,
    OrderDate DATE CONSTRAINT CK_Orders_Date 
        CHECK (OrderDate <= GETDATE()), -- 订单日期不能是未来
    RequiredDate DATE,
    -- 表级CHECK:要求交货日期 >= 订单日期
    CONSTRAINT CK_Orders_Dates CHECK (RequiredDate >= OrderDate),
    Status NVARCHAR(20) CONSTRAINT CK_Orders_Status 
        CHECK (Status IN ('Pending', 'Processing', 'Shipped', 'Delivered', 'Cancelled')),
    TotalAmount DECIMAL(12,2) CONSTRAINT CK_Orders_Amount 
        CHECK (TotalAmount >= 0),
    Discount DECIMAL(5,2) CONSTRAINT CK_Orders_Discount 
        CHECK (Discount >= 0 AND Discount <= 1.00), -- 0到1之间(0%-100%)
    -- 复杂CHECK:如果状态是Delivered,则必须有发货日期
    ShippedDate DATE,
    CONSTRAINT CK_Orders_ShippedDate 
        CHECK (
            (Status != 'Delivered' AND ShippedDate IS NULL) OR
            (Status = 'Delivered' AND ShippedDate IS NOT NULL)
        )
);
GO

-- ✅ 插入合法数据
INSERT INTO Orders (OrderNumber, CustomerID, OrderDate, RequiredDate, Status, TotalAmount, Discount)
VALUES 
    ('ORD2025001', 1, '2025-09-01', '2025-09-10', 'Pending', 1500.00, 0.10),
    ('ORD2025002', 2, GETDATE(), DATEADD(DAY, 7, GETDATE()), 'Processing', 800.00, 0.05);

-- ❌ 违反订单日期CHECK(未来日期)
BEGIN TRY
    INSERT INTO Orders (OrderNumber, CustomerID, OrderDate, RequiredDate, Status, TotalAmount, Discount)
    VALUES ('ORD2025003', 3, '2026-01-01', '2026-01-10', 'Pending', 2000.00, 0.00);
END TRY
BEGIN CATCH
    PRINT '❌ CHECK约束(订单日期)阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ❌ 违反状态CHECK
BEGIN TRY
    INSERT INTO Orders (OrderNumber, CustomerID, OrderDate, RequiredDate, Status, TotalAmount, Discount)
    VALUES ('ORD2025004', 4, GETDATE(), DATEADD(DAY, 5, GETDATE()), 'InvalidStatus', 500.00, 0.00);
END TRY
BEGIN CATCH
    PRINT '❌ CHECK约束(状态)阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ❌ 违反折扣CHECK
BEGIN TRY
    INSERT INTO Orders (OrderNumber, CustomerID, OrderDate, RequiredDate, Status, TotalAmount, Discount)
    VALUES ('ORD2025005', 5, GETDATE(), DATEADD(DAY, 3, GETDATE()), 'Pending', 1000.00, 1.50);
END TRY
BEGIN CATCH
    PRINT '❌ CHECK约束(折扣)阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ✅ 测试复杂CHECK:插入已发货订单
INSERT INTO Orders (OrderNumber, CustomerID, OrderDate, RequiredDate, Status, TotalAmount, Discount, ShippedDate)
VALUES ('ORD2025006', 6, GETDATE(), DATEADD(DAY, 7, GETDATE()), 'Delivered', 1200.00, 0.00, GETDATE());

-- ❌ 违反复杂CHECK:已发货但无发货日期
BEGIN TRY
    INSERT INTO Orders (OrderNumber, CustomerID, OrderDate, RequiredDate, Status, TotalAmount, Discount, ShippedDate)
    VALUES ('ORD2025007', 7, GETDATE(), DATEADD(DAY, 7, GETDATE()), 'Delivered', 900.00, 0.00, NULL);
END TRY
BEGIN CATCH
    PRINT '❌ CHECK约束(发货日期)阻止插入:' + ERROR_MESSAGE();
END CATCH

SELECT * FROM Orders;

3.5 DEFAULT约束

✅ 基本语法:
sql 复制代码
-- 列级
column_name data_type DEFAULT default_value

-- 命名约束
column_name data_type CONSTRAINT constraint_name DEFAULT default_value
📌 案例代码:
sql 复制代码
-- 创建日志表(带DEFAULT约束)
IF OBJECT_ID('SystemLogs', 'U') IS NOT NULL DROP TABLE SystemLogs;
GO

CREATE TABLE SystemLogs (
    LogID INT PRIMARY KEY IDENTITY(1,1),
    LogTime DATETIME CONSTRAINT DF_SystemLogs_Time DEFAULT GETDATE(),
    LogLevel NVARCHAR(20) CONSTRAINT DF_SystemLogs_Level DEFAULT 'INFO',
    Source NVARCHAR(100) CONSTRAINT DF_SystemLogs_Source DEFAULT 'System',
    Message NVARCHAR(500) NOT NULL,
    UserID INT,
    IPAddress NVARCHAR(15) CONSTRAINT DF_SystemLogs_IP DEFAULT '127.0.0.1',
    -- 计算默认值
    SessionID UNIQUEIDENTIFIER CONSTRAINT DF_SystemLogs_SessionID DEFAULT NEWID()
);
GO

-- 插入数据(只指定必要列)
INSERT INTO SystemLogs (Message)
VALUES ('系统启动成功');

-- 插入部分列
INSERT INTO SystemLogs (Message, LogLevel, UserID)
VALUES ('用户登录', 'SUCCESS', 1001);

-- 使用 DEFAULT 关键字
INSERT INTO SystemLogs (Message, LogLevel, Source)
VALUES ('数据库连接', DEFAULT, 'Database');

-- 查看结果
SELECT * FROM SystemLogs;

-- ✅ 所有DEFAULT约束列都有默认值
-- LogTime: 当前时间
-- LogLevel: 'INFO' 或指定值
-- Source: 'System' 或指定值
-- IPAddress: '127.0.0.1'
-- SessionID: 新GUID

3.6 NOT NULL约束

✅ 基本语法:
sql 复制代码
-- 列定义时指定
column_name data_type NOT NULL

-- 已存在表添加NOT NULL约束(需先确保无NULL值)
ALTER TABLE table_name ALTER COLUMN column_name data_type NOT NULL;
📌 案例代码:
sql 复制代码
-- 创建用户表(带NOT NULL约束)
IF OBJECT_ID('Users', 'U') IS NOT NULL DROP TABLE Users;
GO

CREATE TABLE Users (
    UserID INT PRIMARY KEY IDENTITY(1,1),
    Username NVARCHAR(50) NOT NULL UNIQUE,      -- 用户名不能为空
    PasswordHash NVARCHAR(255) NOT NULL,       -- 密码不能为空
    Email NVARCHAR(100) NOT NULL,              -- 邮箱不能为空
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE(), -- 创建日期不能为空
    LastLoginDate DATETIME NULL,               -- 最后登录日期可为空
    IsActive BIT NOT NULL DEFAULT 1            -- 是否激活不能为空
);
GO

-- ✅ 插入合法数据
INSERT INTO Users (Username, PasswordHash, Email)
VALUES ('zhangsan', 'hashed_password_1', 'zhangsan@email.com');

-- ❌ 违反NOT NULL约束(缺少必需列)
BEGIN TRY
    INSERT INTO Users (Username, PasswordHash) -- 缺少Email
    VALUES ('lisi', 'hashed_password_2');
END TRY
BEGIN CATCH
    PRINT '❌ NOT NULL约束阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ❌ 违反NOT NULL约束(显式插入NULL)
BEGIN TRY
    INSERT INTO Users (Username, PasswordHash, Email, CreatedDate)
    VALUES ('wangwu', 'hashed_password_3', NULL, GETDATE()); -- Email为NULL
END TRY
BEGIN CATCH
    PRINT '❌ NOT NULL约束阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ✅ 所有必需列都提供
INSERT INTO Users (Username, PasswordHash, Email, CreatedDate, IsActive)
VALUES ('zhaoliu', 'hashed_password_4', 'zhaoliu@email.com', '2025-01-01', 0);

SELECT * FROM Users;

-- 修改现有表添加NOT NULL约束(需谨慎)
-- 先创建测试表
IF OBJECT_ID('TestTable', 'U') IS NOT NULL DROP TABLE TestTable;
GO

CREATE TABLE TestTable (
    ID INT PRIMARY KEY,
    Name NVARCHAR(50) NULL, -- 允许NULL
    Value INT NULL
);
GO

-- 插入包含NULL的数据
INSERT INTO TestTable VALUES (1, 'A', 100), (2, NULL, 200), (3, 'C', NULL);

-- ❌ 直接添加NOT NULL会失败(因为存在NULL值)
BEGIN TRY
    ALTER TABLE TestTable ALTER COLUMN Name NVARCHAR(50) NOT NULL;
END TRY
BEGIN CATCH
    PRINT '❌ 添加NOT NULL约束失败:' + ERROR_MESSAGE();
END CATCH

-- ✅ 正确步骤:先更新NULL值,再添加约束
UPDATE TestTable SET Name = 'Unknown' WHERE Name IS NULL;
UPDATE TestTable SET Value = 0 WHERE Value IS NULL;

-- 现在可以添加NOT NULL约束
ALTER TABLE TestTable ALTER COLUMN Name NVARCHAR(50) NOT NULL;
ALTER TABLE TestTable ALTER COLUMN Value INT NOT NULL;

-- 验证
SELECT * FROM TestTable;
-- 尝试插入NULL(应失败)
BEGIN TRY
    INSERT INTO TestTable VALUES (4, NULL, 400);
END TRY
BEGIN CATCH
    PRINT '✅ NOT NULL约束生效:' + ERROR_MESSAGE();
END CATCH

四、综合性实战案例


🎯 案例28:学生管理系统 ------ 完整约束体系

sql 复制代码
USE SchoolManagement;
GO

-- ========== 创建基础表结构 ==========
PRINT '🔄 创建学院表...';
IF OBJECT_ID('Departments', 'U') IS NOT NULL DROP TABLE Departments;
GO

CREATE TABLE Departments (
    DeptID INT PRIMARY KEY IDENTITY(1,1),
    DeptCode NVARCHAR(10) NOT NULL UNIQUE 
        CONSTRAINT CK_Departments_Code CHECK (LEN(DeptCode) >= 3),
    DeptName NVARCHAR(100) NOT NULL UNIQUE,
    EstablishedYear INT 
        CONSTRAINT CK_Departments_Year CHECK (EstablishedYear BETWEEN 1900 AND YEAR(GETDATE())),
    Location NVARCHAR(100) NOT NULL DEFAULT '主校区',
    Dean NVARCHAR(50),
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
    IsActive BIT NOT NULL DEFAULT 1
);
GO

PRINT '🔄 创建专业表...';
IF OBJECT_ID('Majors', 'U') IS NOT NULL DROP TABLE Majors;
GO

CREATE TABLE Majors (
    MajorID INT PRIMARY KEY IDENTITY(1,1),
    MajorCode NVARCHAR(10) NOT NULL UNIQUE,
    MajorName NVARCHAR(100) NOT NULL,
    DeptID INT NOT NULL 
        CONSTRAINT FK_Majors_Departments REFERENCES Departments(DeptID)
        ON DELETE CASCADE,
    DurationYears INT NOT NULL 
        CONSTRAINT CK_Majors_Duration CHECK (DurationYears IN (3,4,5)),
    MinCredits INT NOT NULL DEFAULT 120
        CONSTRAINT CK_Majors_Credits CHECK (MinCredits >= 100),
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
    -- 复合唯一约束
    CONSTRAINT UQ_Majors_DeptName UNIQUE (DeptID, MajorName)
);
GO

PRINT '🔄 创建学生表...';
IF OBJECT_ID('Students', 'U') IS NOT NULL DROP TABLE Students;
GO

CREATE TABLE Students (
    StudentID INT PRIMARY KEY IDENTITY(20250001,1), -- 学号从20250001开始
    StudentNumber AS ('S' + RIGHT('00000000' + CAST(StudentID AS NVARCHAR(8)), 8)) PERSISTED UNIQUE, -- 计算列
    Name NVARCHAR(50) NOT NULL,
    Gender CHAR(1) NOT NULL 
        CONSTRAINT CK_Students_Gender CHECK (Gender IN ('M', 'F', 'O')), -- O=Other
    BirthDate DATE NOT NULL
        CONSTRAINT CK_Students_BirthDate CHECK (BirthDate <= DATEADD(YEAR, -16, GETDATE())), -- 至少16岁
    EnrollmentDate DATE NOT NULL DEFAULT GETDATE()
        CONSTRAINT CK_Students_EnrollmentDate CHECK (EnrollmentDate <= GETDATE()),
    MajorID INT NOT NULL 
        CONSTRAINT FK_Students_Majors REFERENCES Majors(MajorID),
    Email NVARCHAR(100) NOT NULL UNIQUE
        CONSTRAINT CK_Students_Email CHECK (Email LIKE '%@%.%'),
    Phone NVARCHAR(20),
    Address NVARCHAR(200),
    GPA DECIMAL(3,2) 
        CONSTRAINT CK_Students_GPA CHECK (GPA BETWEEN 0.00 AND 4.00),
    Status NVARCHAR(20) NOT NULL DEFAULT 'Active'
        CONSTRAINT CK_Students_Status CHECK (Status IN ('Active', 'Graduated', 'Suspended', 'Withdrawn')),
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
    -- 复合CHECK:毕业学生必须有GPA
    CONSTRAINT CK_Students_GraduatedGPA 
        CHECK (
            (Status != 'Graduated') OR 
            (Status = 'Graduated' AND GPA IS NOT NULL)
        )
);
GO

PRINT '🔄 创建课程表...';
IF OBJECT_ID('Courses', 'U') IS NOT NULL DROP TABLE Courses;
GO

CREATE TABLE Courses (
    CourseID INT PRIMARY KEY IDENTITY(1,1),
    CourseCode NVARCHAR(10) NOT NULL UNIQUE
        CONSTRAINT CK_Courses_Code CHECK (CourseCode LIKE '[A-Z][A-Z][A-Z][0-9][0-9][0-9]'), -- 如CS101
    CourseName NVARCHAR(100) NOT NULL,
    Credits INT NOT NULL DEFAULT 3
        CONSTRAINT CK_Courses_Credits CHECK (Credits IN (1,2,3,4,5)),
    DeptID INT NOT NULL 
        CONSTRAINT FK_Courses_Departments REFERENCES Departments(DeptID),
    Level INT NOT NULL 
        CONSTRAINT CK_Courses_Level CHECK (Level BETWEEN 100 AND 500), -- 100=大一, 500=研究生
    Description NVARCHAR(500),
    IsActive BIT NOT NULL DEFAULT 1,
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE()
);
GO

PRINT '🔄 创建成绩表...';
IF OBJECT_ID('Grades', 'U') IS NOT NULL DROP TABLE Grades;
GO

CREATE TABLE Grades (
    GradeID INT PRIMARY KEY IDENTITY(1,1),
    StudentID INT NOT NULL 
        CONSTRAINT FK_Grades_Students REFERENCES Students(StudentID)
        ON DELETE CASCADE,
    CourseID INT NOT NULL 
        CONSTRAINT FK_Grades_Courses REFERENCES Courses(CourseID),
    Semester NVARCHAR(20) NOT NULL DEFAULT '2024-2025第一学期'
        CONSTRAINT CK_Grades_Semester CHECK (Semester LIKE '____-____%学期'),
    Year INT NOT NULL DEFAULT 2025
        CONSTRAINT CK_Grades_Year CHECK (Year BETWEEN 2000 AND 2030),
    Score DECIMAL(5,2) NOT NULL
        CONSTRAINT CK_Grades_Score CHECK (Score BETWEEN 0.00 AND 100.00),
    GradeLetter AS (
        CASE 
            WHEN Score >= 90 THEN 'A'
            WHEN Score >= 80 THEN 'B'
            WHEN Score >= 70 THEN 'C'
            WHEN Score >= 60 THEN 'D'
            ELSE 'F'
        END
    ) PERSISTED,
    IsPassed AS (CASE WHEN Score >= 60 THEN 1 ELSE 0 END) PERSISTED,
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
    -- 唯一约束:学生+课程+学期组合唯一
    CONSTRAINT UQ_Grades_StudentCourseSemester UNIQUE (StudentID, CourseID, Semester),
    -- CHECK:如果通过,分数必须>=60
    CONSTRAINT CK_Grades_PassedScore 
        CHECK (
            (IsPassed = 0 AND Score < 60) OR
            (IsPassed = 1 AND Score >= 60)
        )
);
GO

-- ========== 初始化数据 ==========
PRINT '🔄 初始化学院数据...';
INSERT INTO Departments (DeptCode, DeptName, EstablishedYear, Location, Dean)
VALUES 
    ('CS', '计算机科学与技术学院', 1995, '信息楼', '张教授'),
    ('EE', '电子工程学院', 1990, '工程楼', '李教授'),
    ('MATH', '数学学院', 1985, '理科楼', '王教授'),
    ('ENG', '外国语学院', 1992, '文学院', '赵教授');

PRINT '🔄 初始化专业数据...';
INSERT INTO Majors (MajorCode, MajorName, DeptID, DurationYears, MinCredits)
VALUES 
    ('CS01', '计算机科学与技术', 1, 4, 160),
    ('CS02', '软件工程', 1, 4, 160),
    ('EE01', '电子信息工程', 2, 4, 150),
    ('MATH01', '数学与应用数学', 3, 4, 140),
    ('ENG01', '英语', 4, 4, 130);

PRINT '🔄 初始化学生数据...';
INSERT INTO Students (Name, Gender, BirthDate, MajorID, Email, Phone, GPA, Status)
VALUES 
    ('张三', 'M', '2005-03-15', 1, 'zhangsan@university.edu', '13800138001', 3.75, 'Active'),
    ('李四', 'F', '2005-07-22', 2, 'lisi@university.edu', '13900139002', 3.90, 'Active'),
    ('王五', 'M', '2004-11-08', 3, 'wangwu@university.edu', '13700137003', 3.20, 'Graduated'),
    ('赵六', 'F', '2006-01-30', 4, 'zhaoliu@university.edu', '13600136004', NULL, 'Active'),
    ('钱七', 'O', '2005-09-12', 5, 'qianqi@university.edu', '13500135005', 3.85, 'Active');

PRINT '🔄 初始化课程数据...';
INSERT INTO Courses (CourseCode, CourseName, Credits, DeptID, Level, Description)
VALUES 
    ('CS101', '程序设计基础', 3, 1, 100, '学习编程基础'),
    ('CS201', '数据结构', 4, 1, 200, '学习数据结构与算法'),
    ('MATH101', '高等数学', 5, 3, 100, '微积分与线性代数'),
    ('ENG101', '大学英语', 3, 4, 100, '英语听说读写'),
    ('EE101', '电路分析', 4, 2, 100, '电路基础理论');

PRINT '🔄 初始化成绩数据...';
INSERT INTO Grades (StudentID, CourseID, Semester, Year, Score)
VALUES 
    (20250001, 1, '2024-2025第一学期', 2025, 85.5),
    (20250001, 3, '2024-2025第一学期', 2025, 92.0),
    (20250002, 1, '2024-2025第一学期', 2025, 88.0),
    (20250002, 4, '2024-2025第一学期', 2025, 87.5),
    (20250003, 1, '2023-2024第二学期', 2024, 78.0),
    (20250003, 2, '2023-2024第二学期', 2024, 82.5),
    (20250004, 3, '2024-2025第一学期', 2025, 95.0),
    (20250005, 4, '2024-2025第一学期', 2025, 89.0);

-- ========== 验证约束 ==========
PRINT '✅ 数据初始化完成!';
PRINT '📊 数据统计:';
SELECT 
    (SELECT COUNT(*) FROM Departments) AS 学院数,
    (SELECT COUNT(*) FROM Majors) AS 专业数,
    (SELECT COUNT(*) FROM Students) AS 学生数,
    (SELECT COUNT(*) FROM Courses) AS 课程数,
    (SELECT COUNT(*) FROM Grades) AS 成绩记录数;

-- ❌ 测试约束:插入无效数据
PRINT '🔄 测试约束...';

-- 违反CHECK约束(年龄不足)
BEGIN TRY
    INSERT INTO Students (Name, Gender, BirthDate, MajorID, Email)
    VALUES ('测试学生', 'M', '2020-01-01', 1, 'test@university.edu');
END TRY
BEGIN CATCH
    PRINT '❌ 年龄约束阻止插入:' + ERROR_MESSAGE();
END CATCH

-- 违反外键约束(无效专业ID)
BEGIN TRY
    INSERT INTO Students (Name, Gender, BirthDate, MajorID, Email)
    VALUES ('测试学生2', 'F', '2005-01-01', 999, 'test2@university.edu');
END TRY
BEGIN CATCH
    PRINT '❌ 外键约束阻止插入:' + ERROR_MESSAGE();
END CATCH

-- 违反唯一约束(重复邮箱)
BEGIN TRY
    INSERT INTO Students (Name, Gender, BirthDate, MajorID, Email)
    VALUES ('测试学生3', 'M', '2005-01-01', 1, 'zhangsan@university.edu');
END TRY
BEGIN CATCH
    PRINT '❌ 唯一约束阻止插入:' + ERROR_MESSAGE();
END CATCH

-- 违反复合CHECK(毕业学生无GPA)
BEGIN TRY
    INSERT INTO Students (Name, Gender, BirthDate, MajorID, Email, Status)
    VALUES ('测试学生4', 'F', '2000-01-01', 1, 'test4@university.edu', 'Graduated');
END TRY
BEGIN CATCH
    PRINT '❌ 复合CHECK约束阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ✅ 合法插入毕业学生(有GPA)
INSERT INTO Students (Name, Gender, BirthDate, MajorID, Email, GPA, Status)
VALUES ('测试学生5', 'M', '2000-01-01', 1, 'test5@university.edu', 3.50, 'Graduated');

PRINT '✅ 约束测试完成!';
SELECT * FROM Students WHERE Name LIKE '测试%';

🎯 案例29:电商平台订单系统 ------ 复杂业务约束

sql 复制代码
USE ECommerceDB;
GO

-- ========== 创建表结构 ==========
PRINT '🔄 创建用户表...';
IF OBJECT_ID('Users', 'U') IS NOT NULL DROP TABLE Users;
GO

CREATE TABLE Users (
    UserID INT PRIMARY KEY IDENTITY(1,1),
    Username NVARCHAR(50) NOT NULL UNIQUE
        CONSTRAINT CK_Users_Username CHECK (LEN(Username) >= 3),
    Email NVARCHAR(100) NOT NULL UNIQUE
        CONSTRAINT CK_Users_Email CHECK (Email LIKE '%@%.%'),
    Phone NVARCHAR(20) NOT NULL UNIQUE
        CONSTRAINT CK_Users_Phone CHECK (Phone LIKE '[1-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'),
    PasswordHash NVARCHAR(255) NOT NULL,
    FullName NVARCHAR(100) NOT NULL,
    Gender CHAR(1) 
        CONSTRAINT CK_Users_Gender CHECK (Gender IN ('M', 'F', 'O')),
    BirthDate DATE
        CONSTRAINT CK_Users_BirthDate CHECK (BirthDate <= DATEADD(YEAR, -13, GETDATE())), -- 至少13岁
    RegistrationDate DATETIME NOT NULL DEFAULT GETDATE(),
    LastLoginDate DATETIME,
    Status NVARCHAR(20) NOT NULL DEFAULT 'Active'
        CONSTRAINT CK_Users_Status CHECK (Status IN ('Active', 'Inactive', 'Suspended', 'Deleted')),
    Points INT NOT NULL DEFAULT 0
        CONSTRAINT CK_Users_Points CHECK (Points >= 0),
    -- 计算列:用户等级
    UserLevel AS (
        CASE 
            WHEN Points >= 10000 THEN 'VIP'
            WHEN Points >= 5000 THEN 'Gold'
            WHEN Points >= 1000 THEN 'Silver'
            ELSE 'Bronze'
        END
    ) PERSISTED,
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE()
);
GO

PRINT '🔄 创建产品类别表...';
IF OBJECT_ID('Categories', 'U') IS NOT NULL DROP TABLE Categories;
GO

CREATE TABLE Categories (
    CategoryID INT PRIMARY KEY IDENTITY(1,1),
    CategoryCode NVARCHAR(10) NOT NULL UNIQUE,
    CategoryName NVARCHAR(100) NOT NULL UNIQUE,
    ParentCategoryID INT NULL 
        CONSTRAINT FK_Categories_Parent REFERENCES Categories(CategoryID),
    Level INT NOT NULL DEFAULT 1
        CONSTRAINT CK_Categories_Level CHECK (Level BETWEEN 1 AND 3),
    Description NVARCHAR(500),
    IsActive BIT NOT NULL DEFAULT 1,
    SortOrder INT NOT NULL DEFAULT 0,
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
    -- 自引用约束:顶级分类ParentCategoryID为NULL
    CONSTRAINT CK_Categories_ParentLevel 
        CHECK (
            (ParentCategoryID IS NULL AND Level = 1) OR
            (ParentCategoryID IS NOT NULL AND Level > 1)
        )
);
GO

PRINT '🔄 创建产品表...';
IF OBJECT_ID('Products', 'U') IS NOT NULL DROP TABLE Products;
GO

CREATE TABLE Products (
    ProductID INT PRIMARY KEY IDENTITY(1,1),
    ProductCode NVARCHAR(20) NOT NULL UNIQUE
        CONSTRAINT CK_Products_Code CHECK (ProductCode LIKE 'P[0-9][0-9][0-9][0-9][0-9]'),
    ProductName NVARCHAR(200) NOT NULL,
    CategoryID INT NOT NULL 
        CONSTRAINT FK_Products_Categories REFERENCES Categories(CategoryID),
    Brand NVARCHAR(100),
    Price DECIMAL(10,2) NOT NULL
        CONSTRAINT CK_Products_Price CHECK (Price > 0),
    OriginalPrice DECIMAL(10,2)
        CONSTRAINT CK_Products_OriginalPrice CHECK (OriginalPrice >= Price),
    Stock INT NOT NULL DEFAULT 0
        CONSTRAINT CK_Products_Stock CHECK (Stock >= 0),
    ReservedStock INT NOT NULL DEFAULT 0
        CONSTRAINT CK_Products_ReservedStock CHECK (ReservedStock >= 0 AND ReservedStock <= Stock),
    Weight DECIMAL(8,3) -- 重量(kg)
        CONSTRAINT CK_Products_Weight CHECK (Weight >= 0),
    Dimensions NVARCHAR(50), -- 尺寸(长x宽x高 cm)
    Description NVARCHAR(MAX),
    Specifications NVARCHAR(MAX),
    MainImageURL NVARCHAR(500),
    IsOnSale BIT NOT NULL DEFAULT 0,
    SaleStartDate DATE,
    SaleEndDate DATE,
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
    ModifiedDate DATETIME NOT NULL DEFAULT GETDATE(),
    -- CHECK:促销日期逻辑
    CONSTRAINT CK_Products_SaleDates 
        CHECK (
            (IsOnSale = 0 AND SaleStartDate IS NULL AND SaleEndDate IS NULL) OR
            (IsOnSale = 1 AND SaleStartDate IS NOT NULL AND SaleEndDate IS NOT NULL AND SaleEndDate >= SaleStartDate)
        ),
    -- CHECK:促销价格必须小于原价
    CONSTRAINT CK_Products_SalePrice 
        CHECK (
            (IsOnSale = 0) OR
            (IsOnSale = 1 AND Price < OriginalPrice)
        )
);
GO

PRINT '🔄 创建订单表...';
IF OBJECT_ID('Orders', 'U') IS NOT NULL DROP TABLE Orders;
GO

CREATE TABLE Orders (
    OrderID INT PRIMARY KEY IDENTITY(1,1),
    OrderNumber AS ('ORD' + FORMAT(GETDATE(), 'yyyyMMdd') + RIGHT('000000' + CAST(OrderID AS NVARCHAR(6)), 6)) PERSISTED UNIQUE,
    UserID INT NOT NULL 
        CONSTRAINT FK_Orders_Users REFERENCES Users(UserID),
    OrderDate DATETIME NOT NULL DEFAULT GETDATE(),
    RequiredDate DATE,
    ShipDate DATE,
    Status NVARCHAR(20) NOT NULL DEFAULT 'Pending'
        CONSTRAINT CK_Orders_Status CHECK (Status IN ('Pending', 'Confirmed', 'Processing', 'Shipped', 'Delivered', 'Cancelled', 'Returned')),
    Subtotal DECIMAL(12,2) NOT NULL DEFAULT 0
        CONSTRAINT CK_Orders_Subtotal CHECK (Subtotal >= 0),
    DiscountAmount DECIMAL(12,2) NOT NULL DEFAULT 0
        CONSTRAINT CK_Orders_Discount CHECK (DiscountAmount >= 0),
    ShippingFee DECIMAL(12,2) NOT NULL DEFAULT 0
        CONSTRAINT CK_Orders_Shipping CHECK (ShippingFee >= 0),
    TaxAmount DECIMAL(12,2) NOT NULL DEFAULT 0
        CONSTRAINT CK_Orders_Tax CHECK (TaxAmount >= 0),
    TotalAmount AS (Subtotal - DiscountAmount + ShippingFee + TaxAmount) PERSISTED
        CONSTRAINT CK_Orders_Total CHECK (TotalAmount >= 0),
    PaymentMethod NVARCHAR(50)
        CONSTRAINT CK_Orders_Payment CHECK (PaymentMethod IN ('CreditCard', 'DebitCard', 'PayPal', 'BankTransfer', 'CashOnDelivery')),
    PaymentStatus NVARCHAR(20) NOT NULL DEFAULT 'Unpaid'
        CONSTRAINT CK_Orders_PaymentStatus CHECK (PaymentStatus IN ('Unpaid', 'Paid', 'Refunded')),
    ShippingAddress NVARCHAR(500) NOT NULL,
    ShippingCity NVARCHAR(100) NOT NULL,
    ShippingRegion NVARCHAR(100),
    ShippingPostalCode NVARCHAR(20),
    ShippingCountry NVARCHAR(50) NOT NULL DEFAULT '中国',
    TrackingNumber NVARCHAR(100),
    Notes NVARCHAR(500),
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
    ModifiedDate DATETIME NOT NULL DEFAULT GETDATE(),
    -- 业务规则:已发货订单必须有发货日期
    CONSTRAINT CK_Orders_ShippedDate 
        CHECK (
            (Status NOT IN ('Shipped', 'Delivered') AND ShipDate IS NULL) OR
            (Status IN ('Shipped', 'Delivered') AND ShipDate IS NOT NULL)
        ),
    -- 业务规则:已支付订单不能取消
    CONSTRAINT CK_Orders_CancelPaid 
        CHECK (
            (Status != 'Cancelled') OR
            (Status = 'Cancelled' AND PaymentStatus = 'Unpaid')
        ),
    -- 业务规则:总金额必须等于各项之和
    CONSTRAINT CK_Orders_AmountCalculation 
        CHECK (ABS(TotalAmount - (Subtotal - DiscountAmount + ShippingFee + TaxAmount)) < 0.01)
);
GO

PRINT '🔄 创建订单明细表...';
IF OBJECT_ID('OrderDetails', 'U') IS NOT NULL DROP TABLE OrderDetails;
GO

CREATE TABLE OrderDetails (
    OrderDetailID INT PRIMARY KEY IDENTITY(1,1),
    OrderID INT NOT NULL 
        CONSTRAINT FK_OrderDetails_Orders REFERENCES Orders(OrderID) ON DELETE CASCADE,
    ProductID INT NOT NULL 
        CONSTRAINT FK_OrderDetails_Products REFERENCES Products(ProductID),
    Quantity INT NOT NULL
        CONSTRAINT CK_OrderDetails_Quantity CHECK (Quantity > 0),
    UnitPrice DECIMAL(10,2) NOT NULL
        CONSTRAINT CK_OrderDetails_UnitPrice CHECK (UnitPrice >= 0),
    Discount DECIMAL(5,2) NOT NULL DEFAULT 0.00
        CONSTRAINT CK_OrderDetails_Discount CHECK (Discount >= 0 AND Discount <= 1.00),
    LineTotal AS (Quantity * UnitPrice * (1 - Discount)) PERSISTED,
    Status NVARCHAR(20) NOT NULL DEFAULT 'Active'
        CONSTRAINT CK_OrderDetails_Status CHECK (Status IN ('Active', 'Cancelled', 'Returned')),
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
    ModifiedDate DATETIME NOT NULL DEFAULT GETDATE(),
    -- 唯一约束:订单+产品组合唯一
    CONSTRAINT UQ_OrderDetails_OrderProduct UNIQUE (OrderID, ProductID),
    -- CHECK:已取消的明细,数量必须为0(逻辑删除)
    CONSTRAINT CK_OrderDetails_CancelledQuantity 
        CHECK (
            (Status != 'Cancelled') OR
            (Status = 'Cancelled' AND Quantity = 0)
        )
);
GO

-- ========== 创建触发器维护数据一致性 ==========
PRINT '🔄 创建触发器:更新订单总额...';

CREATE TRIGGER trg_UpdateOrderTotal
ON OrderDetails
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
    SET NOCOUNT ON;
    
    -- 获取受影响的订单ID
    DECLARE @OrderID INT;
    
    -- 处理INSERT和UPDATE
    IF EXISTS(SELECT 1 FROM inserted)
    BEGIN
        SELECT DISTINCT @OrderID = OrderID FROM inserted;
    END
    ELSE
    BEGIN
        SELECT DISTINCT @OrderID = OrderID FROM deleted;
    END
    
    -- 重新计算订单总额
    UPDATE o
    SET 
        Subtotal = ISNULL((SELECT SUM(LineTotal) FROM OrderDetails WHERE OrderID = o.OrderID), 0),
        ModifiedDate = GETDATE()
    FROM Orders o
    WHERE o.OrderID = @OrderID;
END
GO

-- ========== 初始化数据 ==========
PRINT '🔄 初始化类别数据...';
-- 插入顶级分类
INSERT INTO Categories (CategoryCode, CategoryName, ParentCategoryID, Level)
VALUES 
    ('ELEC', '电子产品', NULL, 1),
    ('CLOT', '服装', NULL, 1),
    ('BOOK', '图书', NULL, 1);

-- 插入二级分类
INSERT INTO Categories (CategoryCode, CategoryName, ParentCategoryID, Level)
SELECT 'PHONE', '手机', CategoryID, 2 FROM Categories WHERE CategoryCode = 'ELEC'
UNION ALL
SELECT 'LAPTOP', '笔记本电脑', CategoryID, 2 FROM Categories WHERE CategoryCode = 'ELEC'
UNION ALL
SELECT 'MEN', '男装', CategoryID, 2 FROM Categories WHERE CategoryCode = 'CLOT'
UNION ALL
SELECT 'WOMEN', '女装', CategoryID, 2 FROM Categories WHERE CategoryCode = 'CLOT'
UNION ALL
SELECT 'FICTION', '小说', CategoryID, 2 FROM Categories WHERE CategoryCode = 'BOOK'
UNION ALL
SELECT 'TECH', '科技', CategoryID, 2 FROM Categories WHERE CategoryCode = 'BOOK';

PRINT '🔄 初始化用户数据...';
INSERT INTO Users (Username, Email, Phone, PasswordHash, FullName, Gender, BirthDate, Points)
VALUES 
    ('user001', 'user001@email.com', '13800138001', 'hash1', '张三', 'M', '1990-01-15', 15000),
    ('user002', 'user002@email.com', '13900139002', 'hash2', '李四', 'F', '1992-03-20', 8000),
    ('user003', 'user003@email.com', '13700137003', 'hash3', '王五', 'M', '1988-07-10', 3000);

PRINT '🔄 初始化产品数据...';
INSERT INTO Products (ProductCode, ProductName, CategoryID, Brand, Price, OriginalPrice, Stock, Weight, IsOnSale, SaleStartDate, SaleEndDate)
SELECT 
    'P00001', 'iPhone 15', c.CategoryID, 'Apple', 6999.00, 7999.00, 100, 0.200, 1, '2025-09-01', '2025-10-01'
FROM Categories c WHERE c.CategoryCode = 'PHONE'
UNION ALL
SELECT 'P00002', 'MacBook Pro', c.CategoryID, 'Apple', 12999.00, 14999.00, 50, 1.400, 1, '2025-09-01', '2025-10-01'
FROM Categories c WHERE c.CategoryCode = 'LAPTOP'
UNION ALL
SELECT 'P00003', '男士T恤', c.CategoryID, 'Nike', 199.00, 299.00, 200, 0.300, 0, NULL, NULL
FROM Categories c WHERE c.CategoryCode = 'MEN'
UNION ALL
SELECT 'P00004', '女士连衣裙', c.CategoryID, 'Zara', 399.00, 599.00, 150, 0.500, 0, NULL, NULL
FROM Categories c WHERE c.CategoryCode = 'WOMEN'
UNION ALL
SELECT 'P00005', 'SQL Server 2019指南', c.CategoryID, 'Microsoft Press', 89.00, 89.00, 300, 0.800, 0, NULL, NULL
FROM Categories c WHERE c.CategoryCode = 'TECH';

PRINT '🔄 初始化订单数据...';
-- 插入订单(OrderDetails触发器会自动更新总额)
INSERT INTO Orders (UserID, RequiredDate, ShippingAddress, ShippingCity, PaymentMethod, ShippingCountry)
SELECT 
    u.UserID, 
    DATEADD(DAY, 7, GETDATE()), 
    '北京市朝阳区某某街道123号', 
    '北京', 
    'CreditCard', 
    '中国'
FROM Users u WHERE u.Username = 'user001';

-- 获取刚插入的订单ID
DECLARE @OrderID1 INT = SCOPE_IDENTITY();

-- 插入订单明细
INSERT INTO OrderDetails (OrderID, ProductID, Quantity, UnitPrice, Discount)
SELECT @OrderID1, p.ProductID, 1, p.Price, 0.10
FROM Products p WHERE p.ProductCode = 'P00001'
UNION ALL
SELECT @OrderID1, p.ProductID, 1, p.Price, 0.00
FROM Products p WHERE p.ProductCode = 'P00003';

-- 插入第二个订单
INSERT INTO Orders (UserID, RequiredDate, ShippingAddress, ShippingCity, PaymentMethod, ShippingCountry, Status)
SELECT 
    u.UserID, 
    DATEADD(DAY, 5, GETDATE()), 
    '上海市浦东新区某某路456号', 
    '上海', 
    'PayPal', 
    '中国',
    'Shipped'
FROM Users u WHERE u.Username = 'user002';

DECLARE @OrderID2 INT = SCOPE_IDENTITY();

-- 插入订单明细
INSERT INTO OrderDetails (OrderID, ProductID, Quantity, UnitPrice, Discount)
SELECT @OrderID2, p.ProductID, 2, p.Price, 0.05
FROM Products p WHERE p.ProductCode = 'P00004';

-- 更新第二个订单的发货日期(因为状态是Shipped)
UPDATE Orders SET ShipDate = GETDATE() WHERE OrderID = @OrderID2;

-- ========== 验证数据和约束 ==========
PRINT '✅ 数据初始化完成!';
PRINT '📊 数据统计:';
SELECT 
    (SELECT COUNT(*) FROM Users) AS 用户数,
    (SELECT COUNT(*) FROM Categories) AS 分类数,
    (SELECT COUNT(*) FROM Products) AS 产品数,
    (SELECT COUNT(*) FROM Orders) AS 订单数,
    (SELECT COUNT(*) FROM OrderDetails) AS 订单明细数;

-- 查看订单总额是否正确计算
SELECT 
    o.OrderNumber,
    o.Subtotal,
    o.TotalAmount,
    (SELECT SUM(od.LineTotal) FROM OrderDetails od WHERE od.OrderID = o.OrderID) AS 计算总额,
    CASE 
        WHEN ABS(o.Subtotal - (SELECT SUM(od.LineTotal) FROM OrderDetails od WHERE od.OrderID = o.OrderID)) < 0.01 
        THEN '✓ 正确' 
        ELSE '✗ 错误' 
    END AS 验证结果
FROM Orders o;

-- 查看用户等级
SELECT Username, Points, UserLevel FROM Users;

-- ❌ 测试约束:插入无效数据
PRINT '🔄 测试约束...';

-- 违反促销价格约束
BEGIN TRY
    INSERT INTO Products (ProductCode, ProductName, CategoryID, Price, OriginalPrice, Stock, IsOnSale, SaleStartDate, SaleEndDate)
    SELECT 'P00006', '测试产品', c.CategoryID, 100.00, 90.00, 10, 1, '2025-09-01', '2025-10-01' -- Price > OriginalPrice
    FROM Categories c WHERE c.CategoryCode = 'ELEC';
END TRY
BEGIN CATCH
    PRINT '❌ 促销价格约束阻止插入:' + ERROR_MESSAGE();
END CATCH

-- 违反订单状态约束(已支付订单尝试取消)
BEGIN TRY
    -- 先创建一个已支付订单
    INSERT INTO Orders (UserID, ShippingAddress, ShippingCity, PaymentMethod, PaymentStatus, Status)
    SELECT u.UserID, '测试地址', '测试城市', 'CreditCard', 'Paid', 'Pending'
    FROM Users u WHERE u.Username = 'user003';
    
    DECLARE @TestOrderID INT = SCOPE_IDENTITY();
    
    -- 尝试将已支付订单改为取消状态(应失败)
    UPDATE Orders SET Status = 'Cancelled' WHERE OrderID = @TestOrderID;
END TRY
BEGIN CATCH
    PRINT '❌ 订单状态约束阻止更新:' + ERROR_MESSAGE();
END CATCH

-- ✅ 测试成功案例:插入合法促销产品
INSERT INTO Products (ProductCode, ProductName, CategoryID, Brand, Price, OriginalPrice, Stock, IsOnSale, SaleStartDate, SaleEndDate)
SELECT 'P00006', '合法促销产品', c.CategoryID, 'BrandX', 90.00, 100.00, 50, 1, '2025-09-01', '2025-10-01'
FROM Categories c WHERE c.CategoryCode = 'ELEC';

PRINT '✅ 约束测试完成!';
SELECT ProductCode, ProductName, Price, OriginalPrice, IsOnSale FROM Products WHERE ProductCode = 'P00006';

🎯 案例30:数据库重构 ------ 从规则/默认值迁移到约束

sql 复制代码
USE LegacyToModernDB;
GO

-- ========== 第一阶段:创建旧系统(使用规则和默认值) ==========
PRINT '🔄 创建旧系统表结构...';

-- 创建规则
CREATE RULE LegacyGenderRule AS @value IN ('M', 'F');
CREATE RULE LegacyAgeRule AS @value BETWEEN 1 AND 150;
CREATE RULE LegacyEmailRule AS @value LIKE '%@%.%';
CREATE RULE LegacyStatusRule AS @value IN ('Active', 'Inactive');

-- 创建默认值
CREATE DEFAULT LegacyDateDefault AS GETDATE();
CREATE DEFAULT LegacyStatusDefault AS 'Active';
CREATE DEFAULT LegacyCountryDefault AS '中国';

-- 创建旧表
IF OBJECT_ID('LegacyCustomers', 'U') IS NOT NULL DROP TABLE LegacyCustomers;
GO

CREATE TABLE LegacyCustomers (
    CustomerID INT IDENTITY(1,1) PRIMARY KEY,
    Name NVARCHAR(100) NOT NULL,
    Gender CHAR(1),
    Age INT,
    Email NVARCHAR(100),
    RegistrationDate DATE,
    Status NVARCHAR(20),
    Country NVARCHAR(50),
    LastModified DATE
);
GO

-- 绑定规则和默认值
EXEC sp_bindrule 'LegacyGenderRule', 'LegacyCustomers.Gender';
EXEC sp_bindrule 'LegacyAgeRule', 'LegacyCustomers.Age';
EXEC sp_bindrule 'LegacyEmailRule', 'LegacyCustomers.Email';
EXEC sp_bindrule 'LegacyStatusRule', 'LegacyCustomers.Status';

EXEC sp_bindefault 'LegacyDateDefault', 'LegacyCustomers.RegistrationDate';
EXEC sp_bindefault 'LegacyStatusDefault', 'LegacyCustomers.Status';
EXEC sp_bindefault 'LegacyCountryDefault', 'LegacyCustomers.Country';
EXEC sp_bindefault 'LegacyDateDefault', 'LegacyCustomers.LastModified';

-- 插入测试数据
INSERT INTO LegacyCustomers (Name, Gender, Age, Email)
VALUES 
    ('张三', 'M', 30, 'zhangsan@company.com'),
    ('李四', 'F', 25, 'lisi@company.com'),
    ('王五', 'M', 35, 'wangwu@company.com');

SELECT '旧系统', * FROM LegacyCustomers;
GO

-- ========== 第二阶段:创建新系统(使用约束) ==========
PRINT '🔄 创建新系统表结构...';

IF OBJECT_ID('ModernCustomers', 'U') IS NOT NULL DROP TABLE ModernCustomers;
GO

CREATE TABLE ModernCustomers (
    CustomerID INT IDENTITY(1,1) PRIMARY KEY,
    CustomerCode AS ('CUST' + RIGHT('000000' + CAST(CustomerID AS NVARCHAR(6)), 6)) PERSISTED UNIQUE,
    Name NVARCHAR(100) NOT NULL,
    Gender CHAR(1) NOT NULL 
        CONSTRAINT CK_ModernCustomers_Gender CHECK (Gender IN ('M', 'F', 'O')),
    Age INT NOT NULL 
        CONSTRAINT CK_ModernCustomers_Age CHECK (Age BETWEEN 1 AND 150),
    Email NVARCHAR(100) NOT NULL 
        CONSTRAINT CK_ModernCustomers_Email CHECK (Email LIKE '%@%.%'),
    RegistrationDate DATE NOT NULL DEFAULT GETDATE(),
    Status NVARCHAR(20) NOT NULL DEFAULT 'Active'
        CONSTRAINT CK_ModernCustomers_Status CHECK (Status IN ('Active', 'Inactive', 'Suspended', 'Archived')),
    Country NVARCHAR(50) NOT NULL DEFAULT '中国',
    LastModified DATE NOT NULL DEFAULT GETDATE(),
    CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
    -- 新增字段
    PhoneNumber NVARCHAR(20),
    TotalOrders INT NOT NULL DEFAULT 0
        CONSTRAINT CK_ModernCustomers_TotalOrders CHECK (TotalOrders >= 0),
    TotalAmount DECIMAL(15,2) NOT NULL DEFAULT 0.00
        CONSTRAINT CK_ModernCustomers_TotalAmount CHECK (TotalAmount >= 0),
    -- 计算客户价值等级
    CustomerTier AS (
        CASE 
            WHEN TotalAmount >= 100000 THEN 'Platinum'
            WHEN TotalAmount >= 50000 THEN 'Gold'
            WHEN TotalAmount >= 10000 THEN 'Silver'
            ELSE 'Bronze'
        END
    ) PERSISTED
);
GO

-- ========== 第三阶段:数据迁移 ==========
PRINT '🔄 开始数据迁移...';

BEGIN TRY
    BEGIN TRANSACTION;
    
    -- 步骤1:将旧数据迁移到新表(数据转换)
    INSERT INTO ModernCustomers (
        Name, Gender, Age, Email, RegistrationDate, Status, Country, LastModified
    )
    SELECT 
        Name,
        ISNULL(Gender, 'O'), -- 旧系统允许NULL,新系统不允许
        ISNULL(Age, 18),     -- 旧系统允许NULL,新系统不允许
        Email,
        ISNULL(RegistrationDate, GETDATE()), -- 旧系统有默认值
        ISNULL(Status, 'Active'),            -- 旧系统有默认值
        ISNULL(Country, '中国'),             -- 旧系统有默认值
        ISNULL(LastModified, GETDATE())      -- 旧系统有默认值
    FROM LegacyCustomers;
    
    PRINT '✅ 数据迁移完成,共迁移 ' + CAST(@@ROWCOUNT AS NVARCHAR(10)) + ' 条记录';
    
    -- 步骤2:验证数据一致性
    IF (SELECT COUNT(*) FROM LegacyCustomers) = (SELECT COUNT(*) FROM ModernCustomers)
    BEGIN
        PRINT '✅ 数据一致性验证通过!';
    END
    ELSE
    BEGIN
        RAISERROR('数据迁移数量不一致!', 16, 1);
    END
    
    -- 步骤3:创建映射表(用于关联新旧ID)
    IF OBJECT_ID('CustomerMigrationMap', 'U') IS NOT NULL DROP TABLE CustomerMigrationMap;
    CREATE TABLE CustomerMigrationMap (
        OldCustomerID INT PRIMARY KEY,
        NewCustomerID INT NOT NULL,
        MigrationDate DATETIME NOT NULL DEFAULT GETDATE()
    );
    
    INSERT INTO CustomerMigrationMap (OldCustomerID, NewCustomerID)
    SELECT 
        lc.CustomerID,
        mc.CustomerID
    FROM LegacyCustomers lc
    JOIN ModernCustomers mc ON lc.Name = mc.Name AND lc.Email = mc.Email;
    
    PRINT '✅ 创建映射表,共 ' + CAST(@@ROWCOUNT AS NVARCHAR(10)) + ' 条映射记录';
    
    COMMIT TRANSACTION;
    PRINT '✅ 数据迁移全部完成!';
    
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION;
    PRINT '❌ 数据迁移失败:' + ERROR_MESSAGE();
END CATCH
GO

-- ========== 第四阶段:清理旧系统 ==========
PRINT '🔄 清理旧系统...';

-- 解绑规则和默认值
EXEC sp_unbindrule 'LegacyCustomers.Gender';
EXEC sp_unbindrule 'LegacyCustomers.Age';
EXEC sp_unbindrule 'LegacyCustomers.Email';
EXEC sp_unbindrule 'LegacyCustomers.Status';

EXEC sp_unbindefault 'LegacyCustomers.RegistrationDate';
EXEC sp_unbindefault 'LegacyCustomers.Status';
EXEC sp_unbindefault 'LegacyCustomers.Country';
EXEC sp_unbindefault 'LegacyCustomers.LastModified';

-- 删除规则和默认值
DROP RULE LegacyGenderRule;
DROP RULE LegacyAgeRule;
DROP RULE LegacyEmailRule;
DROP RULE LegacyStatusRule;

DROP DEFAULT LegacyDateDefault;
DROP DEFAULT LegacyStatusDefault;
DROP DEFAULT LegacyCountryDefault;

-- 重命名旧表(归档)
EXEC sp_rename 'LegacyCustomers', 'LegacyCustomers_Archived_20250913';

PRINT '✅ 旧系统清理完成!';
GO

-- ========== 第五阶段:验证新系统 ==========
PRINT '📊 新系统数据验证:';

SELECT 
    CustomerCode,
    Name,
    Gender,
    Age,
    Email,
    Status,
    Country,
    CustomerTier
FROM ModernCustomers
ORDER BY CustomerID;

-- ❌ 测试新约束:插入无效数据
PRINT '🔄 测试新约束...';

-- 违反性别CHECK约束
BEGIN TRY
    INSERT INTO ModernCustomers (Name, Gender, Age, Email)
    VALUES ('测试用户', 'X', 25, 'test@company.com');
END TRY
BEGIN CATCH
    PRINT '❌ 性别约束阻止插入:' + ERROR_MESSAGE();
END CATCH

-- 违反邮箱CHECK约束
BEGIN TRY
    INSERT INTO ModernCustomers (Name, Gender, Age, Email)
    VALUES ('测试用户2', 'M', 30, 'invalid-email');
END TRY
BEGIN CATCH
    PRINT '❌ 邮箱约束阻止插入:' + ERROR_MESSAGE();
END CATCH

-- ✅ 插入合法数据
INSERT INTO ModernCustomers (Name, Gender, Age, Email, TotalAmount)
VALUES ('测试用户3', 'O', 28, 'test3@company.com', 60000.00);

SELECT '新插入用户', CustomerCode, Name, CustomerTier FROM ModernCustomers WHERE Name = '测试用户3';

PRINT '✅ 重构项目完成!新系统已上线,旧系统已归档。';

✅ 本章核心知识点速查表

类型 推荐度 语法示例 适用场景
规则(Rule) ⚠️ 不推荐 CREATE RULE... sp_bindrule 维护旧系统
默认值(Default) ⚠️ 不推荐 CREATE DEFAULT... sp_bindefault 维护旧系统
主键约束 ✅ 推荐 PRIMARY KEY 唯一标识记录
外键约束 ✅ 推荐 FOREIGN KEY REFERENCES 维护表间关系
唯一约束 ✅ 推荐 UNIQUE 确保列值唯一
CHECK约束 ✅ 推荐 CHECK (condition) 数据有效性验证
DEFAULT约束 ✅ 推荐 DEFAULT value 提供默认值
NOT NULL约束 ✅ 推荐 NOT NULL 确保必填字段

🛡️ 最佳实践与建议

  1. 新项目一律使用约束,避免使用规则和默认值对象
  2. 为约束命名CONSTRAINT CK_Table_Column 格式便于管理
  3. 复杂业务规则:约束 + 触发器 + 存储过程组合使用
  4. 数据迁移:先备份,再迁移,最后验证
  5. 性能考虑:过多CHECK约束可能影响插入/更新性能
  6. 文档化:记录每个约束的业务含义

📘 学习建议

  • 在开发环境反复练习约束的创建和测试
  • 理解每种约束的业务场景
  • 掌握从旧系统(规则/默认值)迁移到新系统(约束)的方法
  • 学习使用系统视图查询约束信息:sys.check_constraints, sys.default_constraints

✅ 本章是数据库设计的核心,掌握后可构建健壮、可靠的数据模型!

相关推荐
长而不宰1 小时前
一个偏爱确定性的人如何面对家庭压力
sql
此刻觐神2 小时前
Windows学习笔记-18(MFC项目-制作快捷方式管理工具)
windows·笔记·学习·mfc
last_zhiyin2 小时前
Oracle sql tuning guide 翻译 Part 4-2 --- 连接方法(Join Methods)
数据库·sql
java干货2 小时前
Slave 的 SQL 线程为什么追不上 Master?
数据库·sql
科技林总2 小时前
【系统分析师】7.8 软件形式化方法
学习
weixin_458872613 小时前
东华复试OJ每日3题打卡·复盘76~78
学习
好奇龙猫3 小时前
日语学习-日语知识点小记-日本語体系構造-JLPT-N2前期阶段-第一阶段(12):単語文法
学习
babe小鑫3 小时前
大专市场调查与统计分析专业学习指南
大数据·学习·信息可视化·数据挖掘
weixin_458872614 小时前
东华复试OJ每日3题打卡·复盘79~81
学习