SQL Server触发器 #
一、触发器概述 #
1.1 什么是触发器 #
触发器是一种特殊的存储过程,在特定事件发生时自动执行。
text
触发器特点:
├── 自动执行
├── 不能直接调用
├── 与表关联
├── 可以访问inserted/deleted表
└── 可以回滚操作
1.2 触发器类型 #
text
SQL Server触发器类型:
├── DML触发器
│ ├── AFTER触发器(后触发)
│ └── INSTEAD OF触发器(替代触发)
├── DDL触发器
│ └── 数据库/服务器级别
└── LOGON触发器
└── 登录时触发
二、DML触发器 #
2.1 AFTER触发器 #
sql
-- 创建AFTER INSERT触发器
CREATE TRIGGER tr_users_after_insert
ON users
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
-- 记录插入操作
INSERT INTO users_audit (user_id, action, action_time)
SELECT id, 'INSERT', GETDATE() FROM inserted;
PRINT 'User inserted';
END
GO
-- 创建AFTER UPDATE触发器
CREATE TRIGGER tr_users_after_update
ON users
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- 记录更新操作
INSERT INTO users_audit (user_id, action, old_value, new_value, action_time)
SELECT
i.id,
'UPDATE',
d.name,
i.name,
GETDATE()
FROM inserted i
INNER JOIN deleted d ON i.id = d.id;
END
GO
-- 创建AFTER DELETE触发器
CREATE TRIGGER tr_users_after_delete
ON users
AFTER DELETE
AS
BEGIN
SET NOCOUNT ON;
-- 记录删除操作
INSERT INTO users_audit (user_id, action, action_time)
SELECT id, 'DELETE', GETDATE() FROM deleted;
END
GO
2.2 INSTEAD OF触发器 #
sql
-- 创建INSTEAD OF INSERT触发器
CREATE TRIGGER tr_users_instead_insert
ON users
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
-- 验证数据
IF EXISTS (SELECT 1 FROM inserted WHERE name IS NULL OR name = '')
BEGIN
RAISERROR('Name cannot be empty', 16, 1);
RETURN;
END
-- 执行插入
INSERT INTO users (name, email, status)
SELECT name, email, ISNULL(status, 1) FROM inserted;
END
GO
-- 创建INSTEAD OF UPDATE触发器
CREATE TRIGGER tr_users_instead_update
ON users
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- 不允许更新id
IF UPDATE(id)
BEGIN
RAISERROR('Cannot update id', 16, 1);
RETURN;
END
-- 执行更新
UPDATE u
SET
u.name = i.name,
u.email = i.email,
u.status = i.status
FROM users u
INNER JOIN inserted i ON u.id = i.id;
END
GO
-- 创建INSTEAD OF DELETE触发器
CREATE TRIGGER tr_users_instead_delete
ON users
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON;
-- 检查是否有关联数据
IF EXISTS (
SELECT 1 FROM orders o
INNER JOIN deleted d ON o.user_id = d.id
)
BEGIN
RAISERROR('Cannot delete user with orders', 16, 1);
RETURN;
END
-- 执行删除
DELETE FROM users
WHERE id IN (SELECT id FROM deleted);
END
GO
2.3 inserted和deleted表 #
sql
-- inserted表:包含新数据
-- deleted表:包含旧数据
/*
操作类型 | inserted表 | deleted表
------------|------------|----------
INSERT | 新数据 | 空
UPDATE | 新数据 | 旧数据
DELETE | 空 | 旧数据
*/
-- 查看变化的数据
CREATE TRIGGER tr_users_track_changes
ON users
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
-- 插入操作
IF EXISTS (SELECT 1 FROM inserted) AND NOT EXISTS (SELECT 1 FROM deleted)
BEGIN
PRINT 'INSERT operation';
SELECT * FROM inserted;
END
-- 更新操作
IF EXISTS (SELECT 1 FROM inserted) AND EXISTS (SELECT 1 FROM deleted)
BEGIN
PRINT 'UPDATE operation';
SELECT 'Old' AS type, * FROM deleted
UNION ALL
SELECT 'New' AS type, * FROM inserted;
END
-- 删除操作
IF NOT EXISTS (SELECT 1 FROM inserted) AND EXISTS (SELECT 1 FROM deleted)
BEGIN
PRINT 'DELETE operation';
SELECT * FROM deleted;
END
END
GO
2.4 UPDATE函数 #
sql
-- 使用UPDATE()函数检查列是否被更新
CREATE TRIGGER tr_users_check_update
ON users
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- 检查特定列是否被更新
IF UPDATE(salary)
BEGIN
PRINT 'Salary was updated';
-- 记录薪资变更
INSERT INTO salary_history (user_id, old_salary, new_salary, change_date)
SELECT
i.id,
d.salary,
i.salary,
GETDATE()
FROM inserted i
INNER JOIN deleted d ON i.id = d.id;
END
-- 检查多列
IF UPDATE(name) OR UPDATE(email)
BEGIN
PRINT 'Name or email was updated';
END
END
GO
-- 使用COLUMNS_UPDATED检查多列
CREATE TRIGGER tr_users_columns_updated
ON users
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- 检查第1、2、3列是否被更新
IF COLUMNS_UPDATED() & 14 > 0 -- 14 = 2+4+8
BEGIN
PRINT 'Columns 2, 3, or 4 were updated';
END
END
GO
三、DDL触发器 #
3.1 数据库级别DDL触发器 #
sql
-- 创建数据库级别DDL触发器
CREATE TRIGGER tr_prevent_drop_table
ON DATABASE
FOR DROP_TABLE
AS
BEGIN
PRINT 'Cannot drop tables in this database';
ROLLBACK;
END
GO
-- 记录DDL操作
CREATE TRIGGER tr_log_ddl_operations
ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO ddl_audit (event_type, object_name, sql_text, event_time)
SELECT
EVENTDATA().value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)'),
EVENTDATA().value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(100)'),
EVENTDATA().value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]', 'NVARCHAR(MAX)'),
GETDATE();
END
GO
3.2 服务器级别DDL触发器 #
sql
-- 创建服务器级别DDL触发器
CREATE TRIGGER tr_prevent_create_login
ON ALL SERVER
FOR CREATE_LOGIN
AS
BEGIN
PRINT 'Cannot create logins';
ROLLBACK;
END
GO
-- 记录服务器级别DDL操作
CREATE TRIGGER tr_log_server_ddl
ON ALL SERVER
FOR DDL_SERVER_LEVEL_EVENTS
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO master.dbo.server_ddl_audit (event_type, sql_text, event_time)
SELECT
EVENTDATA().value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)'),
EVENTDATA().value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]', 'NVARCHAR(MAX)'),
GETDATE();
END
GO
3.3 EVENTDATA函数 #
sql
-- 使用EVENTDATA获取事件信息
CREATE TRIGGER tr_show_eventdata
ON DATABASE
FOR CREATE_TABLE
AS
BEGIN
DECLARE @data XML;
SET @data = EVENTDATA();
SELECT
@data.value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)') AS EventType,
@data.value('(/EVENT_INSTANCE/PostTime)[1]', 'NVARCHAR(100)') AS PostTime,
@data.value('(/EVENT_INSTANCE/SPID)[1]', 'INT') AS SPID,
@data.value('(/EVENT_INSTANCE/ServerName)[1]', 'NVARCHAR(100)') AS ServerName,
@data.value('(/EVENT_INSTANCE/LoginName)[1]', 'NVARCHAR(100)') AS LoginName,
@data.value('(/EVENT_INSTANCE/UserName)[1]', 'NVARCHAR(100)') AS UserName,
@data.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'NVARCHAR(100)') AS DatabaseName,
@data.value('(/EVENT_INSTANCE/SchemaName)[1]', 'NVARCHAR(100)') AS SchemaName,
@data.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(100)') AS ObjectName,
@data.value('(/EVENT_INSTANCE/ObjectType)[1]', 'NVARCHAR(100)') AS ObjectType,
@data.value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]', 'NVARCHAR(MAX)') AS CommandText;
END
GO
四、LOGON触发器 #
sql
-- 创建登录触发器
CREATE TRIGGER tr_limit_logins
ON ALL SERVER
FOR LOGON
AS
BEGIN
-- 限制特定用户在特定时间登录
IF ORIGINAL_LOGIN() = 'restricted_user'
AND DATEPART(HOUR, GETDATE()) NOT BETWEEN 9 AND 17
BEGIN
PRINT 'Login not allowed outside business hours';
ROLLBACK;
END
END
GO
五、触发器管理 #
5.1 查看触发器 #
sql
-- 查看表的触发器
EXEC sp_helptrigger 'users';
-- 查看触发器定义
EXEC sp_helptext 'tr_users_after_insert';
-- 查看触发器信息
SELECT
name,
type_desc,
create_date,
modify_date,
is_disabled
FROM sys.triggers
WHERE parent_id = OBJECT_ID('users');
5.1 禁用和启用触发器 #
sql
-- 禁用触发器
DISABLE TRIGGER tr_users_after_insert ON users;
-- 禁用所有触发器
ALTER TABLE users DISABLE TRIGGER ALL;
-- 启用触发器
ENABLE TRIGGER tr_users_after_insert ON users;
-- 启用所有触发器
ALTER TABLE users ENABLE TRIGGER ALL;
5.3 修改触发器 #
sql
-- 修改触发器
ALTER TRIGGER tr_users_after_insert
ON users
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
PRINT 'Modified trigger';
END
GO
5.4 删除触发器 #
sql
-- 删除触发器
DROP TRIGGER tr_users_after_insert;
-- 删除DDL触发器
DROP TRIGGER tr_prevent_drop_table ON DATABASE;
-- 删除服务器级别触发器
DROP TRIGGER tr_prevent_create_login ON ALL SERVER;
六、触发器最佳实践 #
6.1 设计原则 #
text
触发器设计原则:
├── 保持触发器简短
├── 避免递归触发器
├── 使用SET NOCOUNT ON
├── 正确处理多行操作
├── 避免在触发器中使用事务
└── 考虑使用约束替代触发器
6.2 性能考虑 #
sql
-- 正确处理多行操作
CREATE TRIGGER tr_users_multirow
ON users
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
-- 正确:使用集合操作
INSERT INTO users_audit (user_id, action)
SELECT id, 'INSERT' FROM inserted;
-- 错误:假设只有一行
-- DECLARE @id INT;
-- SELECT @id = id FROM inserted;
END
GO
6.3 避免递归 #
sql
-- 检查是否递归触发
-- 数据库选项:RECURSIVE_TRIGGERS
-- 查看设置
SELECT name, is_recursive_triggers_on
FROM sys.databases;
-- 禁用递归触发器
ALTER DATABASE mydb SET RECURSIVE_TRIGGERS OFF;
七、总结 #
触发器要点:
| 类型 | 说明 |
|---|---|
| AFTER | 操作后触发 |
| INSTEAD OF | 替代操作 |
| DDL | 数据定义触发 |
| LOGON | 登录触发 |
最佳实践:
- 保持触发器简短
- 使用SET NOCOUNT ON
- 正确处理多行操作
- 避免递归触发器
- 考虑使用约束替代
下一步,让我们学习CLR集成!
最后更新:2026-03-27