SQL Server 2019 触发器 --- 语法知识点及使用方法详解
一、触发器概述
1.1 什么是触发器
触发器(Trigger) 是一种特殊的存储过程,它在指定的表或视图上发生数据操作语言(DML)事件(INSERT、UPDATE、DELETE)或数据定义语言(DDL)事件(CREATE、ALTER、DROP)时自动执行。触发器不能被显式调用,只能由事件触发。
📌 核心特点:
- 自动执行,无需手动调用
- 与特定表/视图或数据库/服务器事件绑定
- 可用于强制业务规则、审计、数据一致性等
- 使用
INSERTED和DELETED临时表访问变更数据
1.2 触发器的作用
✅ 主要作用:
- 强制业务规则和数据完整性:在数据变更前/后验证数据。
- 审计和日志记录:记录谁在何时修改了什么数据。
- 数据同步:自动更新相关表或缓存。
- 复杂计算:在数据变更时自动计算派生数据。
- 安全控制:限制特定操作或记录访问行为。
- 级联操作:替代外键级联,实现更复杂的级联逻辑。
1.3 触发器分类
| 分类依据 | 类型 | 说明 |
|---|---|---|
| 触发事件 | DML 触发器 | 响应 INSERT、UPDATE、DELETE 操作 |
| DDL 触发器 | 响应 CREATE、ALTER、DROP 等结构变更 | |
| LOGON 触发器 | 响应用户登录事件(本章不详述) | |
| 触发时机 | AFTER 触发器 | 在操作成功执行后触发(默认) |
| INSTEAD OF 触发器 | 替代原操作执行(可用于视图) | |
| 作用域 | 表/视图触发器 | 作用于特定表或视图 |
| 数据库作用域DDL触发器 | 作用于当前数据库的DDL事件 | |
| 服务器作用域DDL触发器 | 作用于整个服务器的DDL事件 |
二、创建 DML 触发器
基本语法:
sql
CREATE [OR ALTER] TRIGGER [schema_name.]trigger_name
ON { table_or_view_name }
[ WITH <dml_trigger_option> [ ,...n ] ]
{ FOR | AFTER | INSTEAD OF }
{ [ INSERT ] [ , ] [ UPDATE ] [ , ] [ DELETE ] }
[ WITH APPEND ] -- 已弃用,不推荐使用
[ NOT FOR REPLICATION ]
AS
{ sql_statement [ ; ] [ ...n ] | EXTERNAL NAME <method specifier> }
关键选项说明:
AFTER:操作成功完成后触发(默认,不能用于视图)。INSTEAD OF:替代原操作执行(可用于表和视图)。NOT FOR REPLICATION:复制过程中不触发。WITH <dml_trigger_option>:ENCRYPTION:加密触发器文本。EXECUTE AS:指定执行上下文。
📌 临时表:
INSERTED:存放 INSERT 或 UPDATE 后的新数据。DELETED:存放 DELETE 或 UPDATE 前的旧数据。
2.1 INSERT 触发器
在 INSERT 操作后(AFTER)或替代(INSTEAD OF)执行。
案例1:审计新员工插入(AFTER INSERT)
sql
-- 创建员工审计表
CREATE TABLE dbo.EmployeeAudit (
AuditID INT IDENTITY(1,1) PRIMARY KEY,
EmployeeID INT,
Action NVARCHAR(10), -- 'INSERT'
ActionDate DATETIME DEFAULT GETDATE(),
ActionBy NVARCHAR(128) DEFAULT SUSER_SNAME(),
FirstName NVARCHAR(50),
LastName NVARCHAR(50),
Salary DECIMAL(10,2)
);
GO
-- 创建AFTER INSERT触发器
CREATE OR ALTER TRIGGER trg_AfterInsert_Employee
ON dbo.Employees
AFTER INSERT -- 在插入后触发
AS
BEGIN
SET NOCOUNT ON; -- 避免返回影响行数
-- 将新插入的员工记录到审计表
INSERT INTO dbo.EmployeeAudit (EmployeeID, Action, FirstName, LastName, Salary)
SELECT
i.EmployeeID,
'INSERT',
i.FirstName,
i.LastName,
i.Salary
FROM inserted i; -- inserted表包含新插入的行
PRINT '新员工已插入并记录到审计表!';
END
GO
-- 测试插入
INSERT INTO dbo.Employees (FirstName, LastName, Salary, DeptID, Performance)
VALUES ('孙', '八', 8000, 2, 4.0);
-- 查看审计记录
SELECT * FROM dbo.EmployeeAudit;
GO
注释:
AFTER INSERT在插入成功后执行。inserted表包含所有新插入的行。- 审计信息包括操作类型、时间、操作者。
2.2 DELETE 触发器
在 DELETE 操作后(AFTER)或替代(INSTEAD OF)执行。
案例2:防止删除高绩效员工(INSTEAD OF DELETE)
sql
-- 创建INSTEAD OF DELETE触发器:阻止删除绩效>=4.5的员工
CREATE OR ALTER TRIGGER trg_InsteadOfDelete_Employee
ON dbo.Employees
INSTEAD OF DELETE -- 替代删除操作
AS
BEGIN
SET NOCOUNT ON;
-- 检查是否有高绩效员工被删除
IF EXISTS (
SELECT 1
FROM deleted d
WHERE d.Performance >= 4.5
)
BEGIN
RAISERROR('不能删除绩效评分>=4.5的员工!', 16, 1);
RETURN; -- 阻止删除
END
-- 删除非高绩效员工
DELETE e
FROM dbo.Employees e
INNER JOIN deleted d ON e.EmployeeID = d.EmployeeID
WHERE d.Performance < 4.5;
PRINT '符合条件的员工已删除。';
END
GO
-- 测试删除(假设员工1绩效=4.5)
-- DELETE FROM dbo.Employees WHERE EmployeeID = 1; -- 会报错!
-- 测试删除低绩效员工(假设员工2绩效=3.8)
DELETE FROM dbo.Employees WHERE EmployeeID = 2; -- 成功
-- 验证
SELECT * FROM dbo.Employees WHERE EmployeeID = 2; -- 应无记录
GO
注释:
INSTEAD OF DELETE完全替代原删除操作。deleted表包含待删除的行。- 使用
RAISERROR抛出错误阻止操作。
2.3 UPDATE 触发器
在 UPDATE 操作后(AFTER)或替代(INSTEAD OF)执行。
案例3:记录薪资变更历史(AFTER UPDATE)
sql
-- 创建薪资变更历史表
CREATE TABLE dbo.SalaryHistory (
HistoryID INT IDENTITY(1,1) PRIMARY KEY,
EmployeeID INT,
OldSalary DECIMAL(10,2),
NewSalary DECIMAL(10,2),
ChangeDate DATETIME DEFAULT GETDATE(),
ChangedBy NVARCHAR(128) DEFAULT SUSER_SNAME()
);
GO
-- 创建AFTER UPDATE触发器:仅当Salary列被更新时触发
CREATE OR ALTER TRIGGER trg_AfterUpdate_EmployeeSalary
ON dbo.Employees
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- 检查Salary列是否被更新
IF UPDATE(Salary) -- 检查特定列是否在SET子句中
BEGIN
-- 记录薪资变更
INSERT INTO dbo.SalaryHistory (EmployeeID, OldSalary, NewSalary)
SELECT
d.EmployeeID,
d.Salary AS OldSalary,
i.Salary AS NewSalary
FROM deleted d
INNER JOIN inserted i ON d.EmployeeID = i.EmployeeID
WHERE d.Salary <> i.Salary; -- 确保值确实改变
PRINT '薪资变更已记录到历史表。';
END
END
GO
-- 测试更新薪资
UPDATE dbo.Employees
SET Salary = 9500
WHERE EmployeeID = 1; -- 假设原薪资8000
-- 查看历史记录
SELECT * FROM dbo.SalaryHistory;
GO
注释:
UPDATE(Salary)检查 Salary 列是否在 UPDATE 语句的 SET 子句中(即使值未变也会触发)。- 使用
deleted(旧值)和inserted(新值)比较实际变更。- 仅当值真正改变时才记录历史。
2.4 替代触发器(INSTEAD OF)
替代原操作执行,常用于视图或复杂业务逻辑。
案例4:通过视图插入订单(INSTEAD OF INSERT)--- 复用第15章案例
sql
-- 复用第15章的vw_OrderDetailsFull视图
-- 为视图创建INSTEAD OF INSERT触发器(已在第15章实现,此处简化)
CREATE OR ALTER TRIGGER trg_InsteadOfInsert_OrderView
ON dbo.vw_OrderDetailsFull
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @OrderID INT, @ProductID INT;
-- 获取或创建订单
SELECT @OrderID = OrderID FROM dbo.Orders WHERE CustomerName = (SELECT CustomerName FROM inserted);
IF @OrderID IS NULL
BEGIN
INSERT INTO dbo.Orders (CustomerName) SELECT CustomerName FROM inserted;
SET @OrderID = SCOPE_IDENTITY();
END
-- 获取产品ID
SELECT @ProductID = ProductID FROM dbo.Products WHERE ProductName = (SELECT ProductName FROM inserted);
IF @ProductID IS NULL
BEGIN
RAISERROR('产品不存在!', 16, 1);
RETURN;
END
-- 插入订单明细
INSERT INTO dbo.OrderDetails (OrderID, ProductID, Quantity, UnitPrice)
SELECT @OrderID, @ProductID, Quantity, UnitPrice FROM inserted;
PRINT '订单已通过视图成功创建!';
END
GO
-- 测试(假设产品和客户存在)
INSERT INTO dbo.vw_OrderDetailsFull (CustomerName, ProductName, Quantity, UnitPrice)
VALUES ('陈九', '鼠标', 3, 100.00);
GO
注释:
INSTEAD OF触发器替代了直接对视图的插入。- 在触发器中实现复杂逻辑:创建订单、查找产品、插入明细。
- 使视图支持原本不支持的插入操作。
2.5 允许使用嵌套触发器
嵌套触发器:一个触发器执行过程中引发另一个触发器(或同一触发器再次触发)。
SQL Server 默认允许嵌套(最大32层),可通过配置控制:
sql
-- 查看当前嵌套设置
EXEC sp_configure 'nested triggers';
-- 启用嵌套触发器(默认已启用)
EXEC sp_configure 'nested triggers', 1;
RECONFIGURE;
-- 禁用嵌套触发器
EXEC sp_configure 'nested triggers', 0;
RECONFIGURE;
案例5:嵌套触发器示例 --- 插入员工时自动创建用户账户
sql
-- 创建用户账户表
CREATE TABLE dbo.UserAccounts (
AccountID INT IDENTITY(1,1) PRIMARY KEY,
EmployeeID INT UNIQUE, -- 一对一关联
Username NVARCHAR(50),
CreatedDate DATETIME DEFAULT GETDATE()
);
GO
-- 触发器1:插入员工后自动创建用户账户
CREATE OR ALTER TRIGGER trg_AfterInsert_CreateUserAccount
ON dbo.Employees
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO dbo.UserAccounts (EmployeeID, Username)
SELECT
i.EmployeeID,
LOWER(i.FirstName + '.' + i.LastName) + '@company.com' -- 生成邮箱作为用户名
FROM inserted i;
PRINT '用户账户已自动创建。';
END
GO
-- 触发器2:插入用户账户后记录到审计表(嵌套触发)
CREATE OR ALTER TRIGGER trg_AfterInsert_UserAccountAudit
ON dbo.UserAccounts
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO dbo.EmployeeAudit (EmployeeID, Action, ActionDate, ActionBy)
SELECT
i.EmployeeID,
'CREATE_ACCOUNT',
GETDATE(),
SUSER_SNAME()
FROM inserted i;
PRINT '用户账户创建已审计。';
END
GO
-- 测试:插入新员工,将触发两个触发器
INSERT INTO dbo.Employees (FirstName, LastName, Salary, DeptID, Performance)
VALUES ('周', '十', 7000, 1, 3.5);
-- 查看结果
SELECT * FROM dbo.Employees WHERE FirstName = '周';
SELECT * FROM dbo.UserAccounts WHERE EmployeeID = SCOPE_IDENTITY();
SELECT * FROM dbo.EmployeeAudit WHERE Action = 'CREATE_ACCOUNT';
GO
注释:
- 插入员工 → 触发
trg_AfterInsert_CreateUserAccount→ 插入账户 → 触发trg_AfterInsert_UserAccountAudit。- 展示了触发器的嵌套执行。
- 嵌套深度受服务器配置限制(默认32层)。
2.6 递归触发器
递归触发器:触发器直接或间接调用自身。
默认情况下,直接递归被禁用,间接递归受嵌套设置控制。
sql
-- 启用直接递归触发器(数据库级别)
ALTER DATABASE YourDatabaseName SET RECURSIVE_TRIGGERS ON;
-- 禁用直接递归触发器(默认)
ALTER DATABASE YourDatabaseName SET RECURSIVE_TRIGGERS OFF;
案例6:递归触发器 --- 自动更新部门员工数
sql
-- 假设Departments表有EmployeeCount字段
IF COL_LENGTH('dbo.Departments', 'EmployeeCount') IS NULL
BEGIN
ALTER TABLE dbo.Departments ADD EmployeeCount INT DEFAULT 0;
-- 初始化数据
UPDATE d
SET EmployeeCount = (SELECT COUNT(*) FROM dbo.Employees e WHERE e.DeptID = d.DeptID)
FROM dbo.Departments d;
END
GO
-- 创建触发器:当员工部门变更时,更新新旧部门的员工数
CREATE OR ALTER TRIGGER trg_AfterUpdate_EmployeeDept
ON dbo.Employees
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- 检查DeptID是否被更新
IF UPDATE(DeptID)
BEGIN
-- 减少旧部门计数
UPDATE d
SET EmployeeCount = EmployeeCount - 1
FROM dbo.Departments d
INNER JOIN deleted del ON d.DeptID = del.DeptID;
-- 增加新部门计数
UPDATE d
SET EmployeeCount = EmployeeCount + 1
FROM dbo.Departments d
INNER JOIN inserted ins ON d.DeptID = ins.DeptID;
END
END
GO
-- 创建触发器:当插入员工时,增加部门计数(可能递归?)
CREATE OR ALTER TRIGGER trg_AfterInsert_UpdateDeptCount
ON dbo.Employees
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
UPDATE d
SET EmployeeCount = EmployeeCount + 1
FROM dbo.Departments d
INNER JOIN inserted i ON d.DeptID = i.DeptID;
END
GO
-- 创建触发器:当删除员工时,减少部门计数
CREATE OR ALTER TRIGGER trg_AfterDelete_UpdateDeptCount
ON dbo.Employees
AFTER DELETE
AS
BEGIN
SET NOCOUNT ON;
UPDATE d
SET EmployeeCount = EmployeeCount - 1
FROM dbo.Departments d
INNER JOIN deleted del ON d.DeptID = del.DeptID;
END
GO
-- 测试:插入新员工
INSERT INTO dbo.Employees (FirstName, LastName, Salary, DeptID, Performance)
VALUES ('吴', '十一', 6500, 1, 4.0); -- 部门1计数+1
-- 测试:更新员工部门
UPDATE dbo.Employees
SET DeptID = 2
WHERE EmployeeID = (SELECT SCOPE_IDENTITY()); -- 部门1计数-1,部门2计数+1
-- 查看部门员工数
SELECT DeptName, EmployeeCount FROM dbo.Departments;
GO
注释:
- 此例中触发器之间是间接递归(插入→更新计数;更新→更新计数),非直接递归。
- 直接递归示例:触发器内执行导致自身再次触发的操作(如更新同一表的同一行)。
- 生产环境应谨慎使用递归,避免无限循环。
三、创建 DDL 触发器
DDL 触发器响应数据定义语言事件(CREATE, ALTER, DROP, GRANT, DENY, REVOKE 等)。
3.1 创建 DDL 触发器的语法
sql
CREATE [OR ALTER] TRIGGER trigger_name
ON { DATABASE | ALL SERVER }
[ WITH <ddl_trigger_option> [ ,...n ] ]
{ FOR | AFTER } { event_type [ ,...n ] | DDL_DATABASE_LEVEL_EVENTS }
AS
{ sql_statement [ ; ] [ ...n ] | EXTERNAL NAME <method specifier> }
关键点:
ON DATABASE:当前数据库作用域。ON ALL SERVER:服务器作用域。event_type:如CREATE_TABLE,ALTER_PROCEDURE,DROP_VIEW等。DDL_DATABASE_LEVEL_EVENTS:数据库级所有DDL事件。
📌 事件函数:
EVENTDATA():返回XML格式的事件信息(包含事件类型、对象名、T-SQL语句等)。
案例7:审计数据库对象变更(DATABASE 作用域)
sql
-- 创建DDL审计表
CREATE TABLE dbo.DDLAudit (
AuditID INT IDENTITY(1,1) PRIMARY KEY,
EventTime DATETIME DEFAULT GETDATE(),
EventType NVARCHAR(100),
ObjectName NVARCHAR(256),
ObjectType NVARCHAR(100),
TSQLCommand NVARCHAR(MAX),
LoginName NVARCHAR(128) DEFAULT SUSER_SNAME()
);
GO
-- 创建数据库级DDL触发器:记录所有DDL操作
CREATE OR ALTER TRIGGER trg_DDL_AuditDatabase
ON DATABASE -- 当前数据库
FOR DDL_DATABASE_LEVEL_EVENTS -- 所有数据库级DDL事件
AS
BEGIN
SET NOCOUNT ON;
DECLARE @EventData XML = EVENTDATA(); -- 获取事件数据
INSERT INTO dbo.DDLAudit (EventType, ObjectName, ObjectType, TSQLCommand)
VALUES (
@EventData.value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)'),
@EventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(256)'),
@EventData.value('(/EVENT_INSTANCE/ObjectType)[1]', 'NVARCHAR(100)'),
@EventData.value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]', 'NVARCHAR(MAX)')
);
PRINT 'DDL操作已记录到审计表。';
END
GO
-- 测试DDL操作
CREATE TABLE dbo.TestTable (ID INT);
ALTER TABLE dbo.TestTable ADD Name NVARCHAR(50);
DROP TABLE dbo.TestTable;
-- 查看审计记录
SELECT * FROM dbo.DDLAudit;
GO
注释:
EVENTDATA()返回XML,使用.value()方法提取数据。- 记录了事件类型、对象名、完整T-SQL命令。
- 适用于数据库变更审计。
3.2 创建服务器作用域的 DDL 触发器
监控整个SQL Server实例的所有数据库的DDL事件。
案例8:防止在非工作时间修改数据库结构
sql
-- 创建服务器级DDL触发器:阻止非工作时间(18:00-8:00)的DDL操作
CREATE OR ALTER TRIGGER trg_ServerDDL_BlockAfterHours
ON ALL SERVER -- 整个服务器
FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE, -- 指定事件
CREATE_VIEW, ALTER_VIEW, DROP_VIEW,
CREATE_PROCEDURE, ALTER_PROCEDURE, DROP_PROCEDURE
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Hour INT = DATEPART(HOUR, GETDATE());
DECLARE @EventType NVARCHAR(100) = EVENTDATA().value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)');
DECLARE @LoginName NVARCHAR(128) = SUSER_SNAME();
-- 非工作时间:18:00 - 次日8:00
IF @Hour >= 18 OR @Hour < 8
BEGIN
ROLLBACK; -- 回滚DDL操作
DECLARE @Msg NVARCHAR(500) =
'DDL操作被阻止!非工作时间(18:00-8:00)禁止修改数据库结构。当前用户:' + @LoginName + ',操作:' + @EventType;
RAISERROR(@Msg, 16, 1);
END
END
GO
-- 测试(在非工作时间执行会失败)
-- CREATE TABLE dbo.TestAfterHours (ID INT); -- 会报错并回滚
-- 查看服务器触发器
SELECT * FROM sys.server_triggers WHERE name = 'trg_ServerDDL_BlockAfterHours';
GO
-- ⚠️ 注意:生产环境谨慎使用,可能影响维护!测试后请禁用或删除。
-- DROP TRIGGER trg_ServerDDL_BlockAfterHours ON ALL SERVER;
GO
注释:
ON ALL SERVER作用于整个实例。- 使用
ROLLBACK取消DDL操作。- 通过
RAISERROR通知用户。- 适用于严格的安全控制场景。
四、管理触发器
4.1 查看触发器
方法1:使用系统存储过程
sql
-- 查看表上的触发器
EXEC sp_helptrigger 'dbo.Employees';
-- 查看触发器定义
EXEC sp_helptext 'trg_AfterInsert_Employee';
方法2:查询系统视图
sql
-- 查看所有DML触发器
SELECT
t.name AS TriggerName,
OBJECT_NAME(t.parent_id) AS TableName,
t.create_date,
t.modify_date,
t.is_disabled,
t.is_instead_of_trigger
FROM sys.triggers t
WHERE t.parent_class = 1; -- 1=对象或列(表/视图)
-- 查看所有DDL触发器(数据库级)
SELECT
name AS TriggerName,
create_date,
modify_date,
is_disabled
FROM sys.triggers
WHERE parent_class = 0; -- 0=数据库
-- 查看服务器级DDL触发器
SELECT
name AS TriggerName,
create_date,
modify_date,
is_disabled
FROM sys.server_triggers;
GO
方法3:查看触发器定义和事件
sql
-- 查看DML触发器详细信息
SELECT
t.name AS TriggerName,
OBJECT_NAME(t.parent_id) AS TableName,
CASE
WHEN t.is_instead_of_trigger = 1 THEN 'INSTEAD OF'
ELSE 'AFTER'
END AS TriggerType,
CASE WHEN t.is_disabled = 1 THEN 'Disabled' ELSE 'Enabled' END AS Status,
OBJECT_DEFINITION(t.object_id) AS Definition
FROM sys.triggers t
WHERE t.parent_class = 1;
-- 查看DDL触发器事件
SELECT
te.trigger_id,
t.name AS TriggerName,
te.type_desc AS EventType
FROM sys.trigger_events te
INNER JOIN sys.triggers t ON te.object_id = t.object_id
WHERE t.parent_class = 0; -- 数据库级
GO
4.2 修改触发器
使用 ALTER TRIGGER 或 CREATE OR ALTER TRIGGER。
案例9:修改触发器 --- 增强审计信息
sql
-- 修改之前的薪资变更触发器,增加旧薪资和新薪资到审计消息
CREATE OR ALTER TRIGGER trg_AfterUpdate_EmployeeSalary
ON dbo.Employees
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF UPDATE(Salary)
BEGIN
DECLARE @Msg NVARCHAR(500);
-- 构建审计消息
SELECT @Msg =
'员工 ' + i.FirstName + ' ' + i.LastName +
' 薪资从 ' + CAST(d.Salary AS NVARCHAR(20)) +
' 变更为 ' + CAST(i.Salary AS NVARCHAR(20))
FROM deleted d
INNER JOIN inserted i ON d.EmployeeID = i.EmployeeID
WHERE d.Salary <> i.Salary;
IF @Msg IS NOT NULL
BEGIN
-- 记录到历史表
INSERT INTO dbo.SalaryHistory (EmployeeID, OldSalary, NewSalary)
SELECT d.EmployeeID, d.Salary, i.Salary
FROM deleted d
INNER JOIN inserted i ON d.EmployeeID = i.EmployeeID
WHERE d.Salary <> i.Salary;
PRINT @Msg; -- 输出消息
END
END
END
GO
-- 测试更新
UPDATE dbo.Employees
SET Salary = 10000
WHERE EmployeeID = 1;
GO
注释:
- 使用
CREATE OR ALTER更安全(避免对象不存在错误)。- 增强了用户反馈信息。
4.3 删除触发器
sql
-- 删除DML触发器
DROP TRIGGER IF EXISTS trg_AfterInsert_Employee;
-- 删除数据库级DDL触发器
DROP TRIGGER IF EXISTS trg_DDL_AuditDatabase ON DATABASE;
-- 删除服务器级DDL触发器
DROP TRIGGER IF EXISTS trg_ServerDDL_BlockAfterHours ON ALL SERVER;
-- 传统方法(兼容旧版本)
IF EXISTS (SELECT * FROM sys.triggers WHERE name = 'trg_AfterInsert_Employee')
DROP TRIGGER trg_AfterInsert_Employee;
GO
4.4 启用和禁用触发器
有时需要临时禁用触发器(如批量导入数据)。
sql
-- 禁用单个触发器
DISABLE TRIGGER trg_AfterInsert_Employee ON dbo.Employees;
-- 启用单个触发器
ENABLE TRIGGER trg_AfterInsert_Employee ON dbo.Employees;
-- 禁用表上所有触发器
DISABLE TRIGGER ALL ON dbo.Employees;
-- 启用表上所有触发器
ENABLE TRIGGER ALL ON dbo.Employees;
-- 禁用数据库级DDL触发器
DISABLE TRIGGER trg_DDL_AuditDatabase ON DATABASE;
-- 启用数据库级DDL触发器
ENABLE TRIGGER trg_DDL_AuditDatabase ON DATABASE;
-- 禁用服务器级DDL触发器
DISABLE TRIGGER trg_ServerDDL_BlockAfterHours ON ALL SERVER;
-- 启用服务器级DDL触发器
ENABLE TRIGGER trg_ServerDDL_BlockAfterHours ON ALL SERVER;
GO
-- 查看触发器状态
SELECT
name,
is_disabled
FROM sys.triggers
WHERE name IN ('trg_AfterInsert_Employee', 'trg_DDL_AuditDatabase');
GO
注释:
DISABLE TRIGGER不删除触发器,只是临时停用。- 批量操作前禁用,操作后启用,可提升性能并避免不必要的触发。
五、综合性案例
综合案例1:完整的员工管理系统 --- DML触发器综合应用
sql
-- 场景:构建员工管理系统的审计、约束、同步机制
-- 1. 清理旧触发器(避免冲突)
DROP TRIGGER IF EXISTS trg_AfterInsert_Employee;
DROP TRIGGER IF EXISTS trg_InsteadOfDelete_Employee;
DROP TRIGGER IF EXISTS trg_AfterUpdate_EmployeeSalary;
DROP TRIGGER IF EXISTS trg_AfterUpdate_EmployeeDept;
DROP TRIGGER IF EXISTS trg_AfterInsert_UpdateDeptCount;
DROP TRIGGER IF EXISTS trg_AfterDelete_UpdateDeptCount;
GO
-- 2. 确保表结构
IF COL_LENGTH('dbo.Departments', 'EmployeeCount') IS NULL
ALTER TABLE dbo.Departments ADD EmployeeCount INT DEFAULT 0;
GO
-- 3. 初始化部门员工数
UPDATE d
SET EmployeeCount = (SELECT COUNT(*) FROM dbo.Employees e WHERE e.DeptID = d.DeptID)
FROM dbo.Departments d;
GO
-- 4. 创建综合触发器
-- 触发器A:插入员工时审计 + 更新部门计数
CREATE OR ALTER TRIGGER trg_Comprehensive_InsertEmployee
ON dbo.Employees
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- 审计插入
INSERT INTO dbo.EmployeeAudit (EmployeeID, Action, FirstName, LastName, Salary)
SELECT EmployeeID, 'INSERT', FirstName, LastName, Salary FROM inserted;
-- 更新部门计数
UPDATE d
SET EmployeeCount = EmployeeCount + 1
FROM dbo.Departments d
INNER JOIN inserted i ON d.DeptID = i.DeptID;
COMMIT TRANSACTION;
PRINT '员工插入:审计和部门计数已更新。';
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
THROW;
END CATCH
END
GO
-- 触发器B:更新员工时审计薪资变更 + 检查部门变更更新计数
CREATE OR ALTER TRIGGER trg_Comprehensive_UpdateEmployee
ON dbo.Employees
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- 审计薪资变更
IF UPDATE(Salary)
BEGIN
INSERT INTO dbo.SalaryHistory (EmployeeID, OldSalary, NewSalary)
SELECT d.EmployeeID, d.Salary, i.Salary
FROM deleted d
INNER JOIN inserted i ON d.EmployeeID = i.EmployeeID
WHERE d.Salary <> i.Salary;
PRINT '薪资变更已记录。';
END
-- 处理部门变更
IF UPDATE(DeptID)
BEGIN
-- 减少旧部门计数
UPDATE d
SET EmployeeCount = EmployeeCount - 1
FROM dbo.Departments d
INNER JOIN deleted del ON d.DeptID = del.DeptID;
-- 增加新部门计数
UPDATE d
SET EmployeeCount = EmployeeCount + 1
FROM dbo.Departments d
INNER JOIN inserted ins ON d.DeptID = ins.DeptID;
PRINT '部门变更:部门计数已更新。';
END
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
THROW;
END CATCH
END
GO
-- 触发器C:删除员工时审计 + 检查绩效 + 更新部门计数
CREATE OR ALTER TRIGGER trg_Comprehensive_DeleteEmployee
ON dbo.Employees
INSTEAD OF DELETE -- 使用INSTEAD OF进行前置检查
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- 检查高绩效员工
IF EXISTS (SELECT 1 FROM deleted WHERE Performance >= 4.5)
BEGIN
RAISERROR('不能删除高绩效员工(绩效>=4.5)!', 16, 1);
RETURN;
END
-- 审计删除
INSERT INTO dbo.EmployeeAudit (EmployeeID, Action, FirstName, LastName, Salary)
SELECT EmployeeID, 'DELETE', FirstName, LastName, Salary FROM deleted;
-- 更新部门计数
UPDATE d
SET EmployeeCount = EmployeeCount - 1
FROM dbo.Departments d
INNER JOIN deleted del ON d.DeptID = del.DeptID;
-- 执行删除
DELETE e
FROM dbo.Employees e
INNER JOIN deleted d ON e.EmployeeID = d.EmployeeID;
COMMIT TRANSACTION;
PRINT '员工已删除:审计和部门计数已更新。';
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
THROW;
END CATCH
END
GO
-- 5. 测试综合功能
-- (1) 插入新员工
PRINT '=== 插入员工 ===';
INSERT INTO dbo.Employees (FirstName, LastName, Salary, DeptID, Performance)
VALUES ('郑', '十二', 7200, 1, 3.9);
-- (2) 更新薪资
PRINT '=== 更新薪资 ===';
UPDATE dbo.Employees
SET Salary = 7800
WHERE FirstName = '郑' AND LastName = '十二';
-- (3) 更新部门
PRINT '=== 更新部门 ===';
UPDATE dbo.Employees
SET DeptID = 2
WHERE FirstName = '郑' AND LastName = '十二';
-- (4) 尝试删除高绩效员工(假设员工1绩效>=4.5)
PRINT '=== 尝试删除高绩效员工 ===';
-- DELETE FROM dbo.Employees WHERE EmployeeID = 1; -- 应失败
-- (5) 删除低绩效员工
PRINT '=== 删除员工 ===';
DELETE FROM dbo.Employees WHERE FirstName = '郑' AND LastName = '十二';
-- 6. 验证结果
PRINT '=== 审计记录 ===';
SELECT * FROM dbo.EmployeeAudit WHERE FirstName = '郑';
PRINT '=== 薪资历史 ===';
SELECT * FROM dbo.SalaryHistory WHERE EmployeeID = (SELECT EmployeeID FROM dbo.Employees WHERE FirstName = '郑');
PRINT '=== 部门员工数 ===';
SELECT DeptName, EmployeeCount FROM dbo.Departments;
GO
注释:
- 演示了 INSERT、UPDATE、DELETE 触发器的综合应用。
- 每个触发器包含事务控制,确保数据一致性。
- 结合了审计、业务规则检查、数据同步功能。
- 使用
INSTEAD OF DELETE实现删除前检查。
综合案例2:数据库安全与合规 --- DDL触发器综合应用
sql
-- 场景:确保数据库结构变更符合安全规范
-- 1. 创建详细的DDL审计表
CREATE TABLE dbo.DetailedDDLAudit (
AuditID INT IDENTITY(1,1) PRIMARY KEY,
EventTime DATETIME2(3) DEFAULT SYSDATETIME(),
EventType NVARCHAR(100),
SchemaName NVARCHAR(128),
ObjectName NVARCHAR(256),
ObjectType NVARCHAR(100),
TSQLCommand NVARCHAR(MAX),
LoginName NVARCHAR(128) DEFAULT SUSER_SNAME(),
HostName NVARCHAR(128) DEFAULT HOST_NAME(),
AppName NVARCHAR(128) DEFAULT PROGRAM_NAME(),
IPAddress NVARCHAR(48) DEFAULT CONNECTIONPROPERTY('client_net_address') -- 需要sysadmin权限
);
GO
-- 2. 创建审计触发器(记录详细信息)
CREATE OR ALTER TRIGGER trg_DDL_DetailedAudit
ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS
AS
BEGIN
SET NOCOUNT ON;
DECLARE @EventData XML = EVENTDATA();
DECLARE @IPAddress NVARCHAR(48);
-- 获取IP地址(可能失败,使用TRY...CATCH)
BEGIN TRY
SELECT @IPAddress = CONVERT(NVARCHAR(48), CONNECTIONPROPERTY('client_net_address'));
END TRY
BEGIN CATCH
SET @IPAddress = 'Unknown';
END CATCH
INSERT INTO dbo.DetailedDDLAudit (
EventType,
SchemaName,
ObjectName,
ObjectType,
TSQLCommand,
HostName,
AppName,
IPAddress
)
VALUES (
@EventData.value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)'),
@EventData.value('(/EVENT_INSTANCE/SchemaName)[1]', 'NVARCHAR(128)'),
@EventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(256)'),
@EventData.value('(/EVENT_INSTANCE/ObjectType)[1]', 'NVARCHAR(100)'),
@EventData.value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]', 'NVARCHAR(MAX)'),
HOST_NAME(),
PROGRAM_NAME(),
@IPAddress
);
END
GO
-- 3. 创建合规性触发器:禁止删除重要表
CREATE OR ALTER TRIGGER trg_DDL_BlockDropCriticalTables
ON DATABASE
FOR DROP_TABLE
AS
BEGIN
SET NOCOUNT ON;
DECLARE @EventData XML = EVENTDATA();
DECLARE @ObjectName NVARCHAR(256) = @EventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(256)');
DECLARE @SchemaName NVARCHAR(128) = @EventData.value('(/EVENT_INSTANCE/SchemaName)[1]', 'NVARCHAR(128)');
DECLARE @FullObjectName NVARCHAR(400) = QUOTENAME(@SchemaName) + '.' + QUOTENAME(@ObjectName);
-- 定义关键表列表
IF @FullObjectName IN ('[dbo].[Employees]', '[dbo].[Departments]', '[dbo].[Orders]')
BEGIN
ROLLBACK;
DECLARE @Msg NVARCHAR(500) = '禁止删除关键表:' + @FullObjectName + '!操作已回滚。';
RAISERROR(@Msg, 16, 1);
END
END
GO
-- 4. 创建命名规范触发器:强制表名以"tbl_"开头
CREATE OR ALTER TRIGGER trg_DDL_EnforceTableNaming
ON DATABASE
FOR CREATE_TABLE
AS
BEGIN
SET NOCOUNT ON;
DECLARE @EventData XML = EVENTDATA();
DECLARE @ObjectName NVARCHAR(256) = @EventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(256)');
DECLARE @SchemaName NVARCHAR(128) = @EventData.value('(/EVENT_INSTANCE/SchemaName)[1]', 'NVARCHAR(128)');
-- 检查表名是否以"tbl_"开头(忽略系统表)
IF @SchemaName = 'dbo' AND LEFT(@ObjectName, 4) <> 'tbl_'
BEGIN
ROLLBACK;
DECLARE @Msg NVARCHAR(500) =
'表名规范:dbo架构下的表名必须以"tbl_"开头!当前名称:' + @ObjectName;
RAISERROR(@Msg, 16, 1);
END
END
GO
-- 5. 测试综合功能
-- (1) 尝试创建不符合命名规范的表(应失败)
-- CREATE TABLE dbo.TestNaming (ID INT); -- 失败!
-- (2) 创建符合规范的表
CREATE TABLE dbo.tbl_TestNaming (ID INT); -- 成功
-- (3) 尝试删除关键表(应失败)
-- DROP TABLE dbo.Employees; -- 失败!
-- (4) 删除测试表
DROP TABLE dbo.tbl_TestNaming; -- 成功
-- (5) 查看审计记录
SELECT * FROM dbo.DetailedDDLAudit ORDER BY EventTime DESC;
GO
-- 6. 清理(可选)
-- DROP TRIGGER trg_DDL_DetailedAudit ON DATABASE;
-- DROP TRIGGER trg_DDL_BlockDropCriticalTables ON DATABASE;
-- DROP TRIGGER trg_DDL_EnforceTableNaming ON DATABASE;
-- DROP TABLE dbo.DetailedDDLAudit;
GO
注释:
- 演示了多个DDL触发器协同工作:审计、安全控制、合规检查。
- 详细审计记录包含IP地址、主机名、应用程序名等。
- 强制命名规范和关键表保护提升数据库治理水平。
- 使用
ROLLBACK和RAISERROR实现主动防护。
六、最佳实践与注意事项
✅ 最佳实践:
- 明确目的:每个触发器应有单一、明确的职责。
- 性能考虑:避免在触发器中执行复杂或耗时操作。
- 事务管理 :在触发器中使用
TRY...CATCH和事务控制。 - 避免递归:谨慎使用递归触发器,防止无限循环。
- 文档化:在触发器代码中添加注释说明逻辑。
- 测试充分:触发器影响数据一致性,必须充分测试。
- 使用 SET NOCOUNT ON:避免返回额外结果集影响应用程序。
⚠️ 注意事项:
- 隐式执行:触发器自动执行,开发人员可能忘记其存在。
- 调试困难:触发器错误可能难以追踪。
- 性能瓶颈:复杂触发器会拖慢DML操作。
- 嵌套深度:注意嵌套和递归的深度限制(32层)。
- 权限要求 :创建触发器需要
ALTER权限。 - 复制环境 :考虑
NOT FOR REPLICATION选项。 - 维护成本:过多触发器增加系统复杂性。
✅ 本章全面覆盖 SQL Server 触发器的类型、创建、管理及实战案例。触发器是实现自动化业务逻辑、数据审计和强制约束的利器,但需谨慎使用以避免性能和维护问题。合理设计触发器可大幅提升数据库的健壮性和安全性!