INSTEAD OF DELETE触发器只能建在视图上,需先创建包装原表的视图,再在其上定义触发器;触发器内须用JOIN关联deleted伪表与原表执行UPDATE实现软删;业务DELETE语句必须指向视图而非表,并建议在原表加AFTER DELETE触发器阻断直删。

INSTEAD OF DELETE 触发器必须建在视图上,不能直接建在表上
SQL Server 和 PostgreSQL(通过规则或触发器)支持 INSTEAD OF DELETE,但它的作用对象只能是视图——这是最常被忽略的前提。如果你试图在基础表上创建该类型触发器,会直接报错:The INSTEAD OF DELETE trigger cannot be created on table 'xxx'。原因在于:SQL Server 要求 INSTEAD OF 触发器用于“可更新视图”的逻辑拦截,而物理表的 DML 由 AFTER 或 FOR 触发器接管。
所以第一步必须创建一个包装原表的视图,例如:
CREATE VIEW v_users AS SELECT id, name, email, is_deleted, deleted_at FROM users;
之后才能在这个 v_users 上定义触发器。
触发器里要显式更新标记字段,且必须处理多行删除
INSTEAD OF DELETE 不会自动执行 DELETE,它完全替代原始操作,因此你得手动把 DELETE FROM v_users WHERE ... 转成 UPDATE。关键点是:不能只写 UPDATE users SET is_deleted = 1,因为 deleted 伪表可能含多行,必须用 JOIN 关联原表和 deleted。
典型写法如下(SQL Server):
CREATE TRIGGER tr_soft_delete_on_v_users
ON v_users
INSTEAD OF DELETE
AS
BEGIN
UPDATE u
SET is_deleted = 1,
deleted_at = GETDATE()
FROM users u
INNER JOIN deleted d ON u.id = d.id;
END;
-
deleted是系统提供的临时表,包含所有本该被删的行,结构同视图 - 必须用
INNER JOIN而非WHERE id IN (SELECT id FROM deleted),后者在高并发或大结果集下性能差,且无法保证原子性 - 如果原表主键不是
id,需按实际字段名替换连接条件
DELETE 语句必须指向视图,而非原表
软删除生效的前提,是业务代码调用的是 DELETE FROM v_users WHERE ...,而不是 DELETE FROM users WHERE ...。后者完全绕过触发器,直删物理行。
常见疏漏包括:
- ORM 框架(如 Entity Framework、Django ORM)默认生成对表的 DELETE,需显式配置为操作视图
- 旧有存储过程或脚本未同步修改,仍删原表
- 开发人员调试时手写
DELETE FROM users,绕过软删逻辑
建议在原表上加个 AFTER DELETE 触发器抛异常,防止误删:
CREATE TRIGGER tr_block_direct_delete_on_users
ON users
AFTER DELETE
AS
BEGIN
RAISERROR('Direct DELETE on table "users" is forbidden. Use view "v_users" instead.', 16, 1);
ROLLBACK;
END;
注意 NULL 值和默认约束对软删除字段的影响
如果 is_deleted 是 BIT 类型且允许 NULL,UPDATE 时不显式赋值会导致字段保持 NULL,后续查询 WHERE is_deleted = 0 就漏数据。同样,deleted_at 若为 DATETIME2 且无默认值,不赋值就是 NULL,影响审计和清理逻辑。
推荐做法:
-
is_deleted设为BIT NOT NULL DEFAULT 0 -
deleted_at设为DATETIME2 NULL,仅在软删时更新,不设默认值 - 查询时统一用
WHERE is_deleted = 0,避免IS NULL或三值逻辑干扰
另外,如果业务需要恢复删除,得额外实现 INSTEAD OF UPDATE 触发器来重置 is_deleted 和清空 deleted_at,但这属于另一层逻辑,不在当前触发器职责内。
真正难的不是写这个触发器,而是让所有数据访问路径——包括报表、ETL、第三方工具、历史脚本——都收敛到视图入口。一旦漏掉一处,软删除就形同虚设。










