deleted表仅在delete和update触发器中存在,insert触发器中不可用;其结构与原表一致但无索引约束,仅存于触发器执行期间,须用主键或高选择性列join以保障性能。

deleted表只在DELETE和UPDATE触发器里自动存在
你不能在INSERT触发器里查deleted,它根本不存在——SQL Server 只在涉及“旧值”的场景才生成这张表。常见错误是写了个AFTER INSERT触发器却还写SELECT * FROM deleted,直接报错Invalid object name 'deleted'。
真实可用场景只有两个:AFTER DELETE(deleted存全部被删行)和AFTER UPDATE(deleted存更新前的原始值)。注意:哪怕UPDATE只改一列,deleted里也是整行快照,不是“只含被改字段”。
-
INSTEAD OF UPDATE也提供deleted,但语义不同:它反映的是“用户想改但还没执行的原始状态” - 如果触发器类型写成
AFTER INSERT, UPDATE,deleted在INSERT分支里不可用,必须用IF EXISTS (SELECT 1 FROM inserted) AND NOT EXISTS (SELECT 1 FROM deleted)做分支判断 - 别依赖
@@ROWCOUNT来判断deleted是否有数据——它可能被前面的PRINT或SET语句清零
deleted表结构与原表一致,但没索引也没约束
deleted是内存快照,字段名、类型、NULL性全继承原表,但它不带主键、唯一约束、索引,甚至没有默认值。这意味着:JOIN或WHERE条件如果没走原表已有索引,性能会明显下降,尤其在批量UPDATE上。
例如原表Orders有OrderId聚集索引,你在触发器里写SELECT i.Price FROM inserted i INNER JOIN deleted d ON i.OrderId = d.OrderId,SQL Server 能利用索引;但若用CustomerName(非索引列)关联,就会走哈希匹配,拖慢整个事务。
- 务必用主键或高选择性索引列做
JOIN或WHERE,避免全表扫描 - 不要对
deleted建临时索引——语法不支持,CREATE INDEX #ix ON deleted(...)直接报错 -
TEXT/NTEXT/IMAGE列在AFTER触发器中无法访问,哪怕只是LEN()也会失败;换成VARCHAR(MAX)等现代类型才能读
deleted表只读且生命周期极短,别试图缓存或跨作用域引用
deleted表在触发器开始执行时就已就绪,结束时立刻释放,不能INSERT INTO、UPDATE或DELETE它,也不能把它当普通表传给存储过程(除非用表变量显式复制)。
常见踩坑是想“先存下来再慢慢处理”,比如写SELECT * INTO #temp_deleted FROM deleted——这看似可行,但本地临时表#temp_deleted在触发器外完全不可见,后续逻辑根本拿不到;全局临时表##temp_deleted又面临并发覆盖风险。
- 真要复用数据,用表变量
@deleted TABLE(...)显式复制,但注意大结果集会吃内存 - 别在触发器里调用另一个含
SELECT FROM deleted的存储过程——那个过程里的deleted是空的,不是当前触发器的副本 - 嵌套触发器中,内层触发器的
deleted只反映内层DML的旧值,和外层无关
还原逻辑删除时,deleted表是唯一可信来源,但必须配合INSTEAD OF
如果你用AFTER DELETE触发器去软删(比如UPDATE IsDeleted=1),deleted表的数据在触发器执行前已被物理移除,这时查到的其实是空的——因为AFTER意味着“删完再触发”,而行已经没了。
正确做法是用INSTEAD OF DELETE:它拦截物理删除,在行被真正移走前就把deleted里的完整快照抄进归档表,再执行软删动作。这是唯一能保证不丢数据的路径。
- 必须加
SET NOCOUNT ON,否则某些ORM会把归档表的INSERT行数误判为主表操作结果 - 归档表建议建在独立数据库或文件组,避免主库损坏时备份也一起丢失
- 还原操作(UPDATE IsDeleted=0)不应再走触发器,而是应用层直连执行,防止循环触发
最常被忽略的一点:deleted表不是“历史归档”,它只活在当前触发器这一瞬间;你看到的每一行,都是这次DML刚发生前的状态,不多也不少——想靠它查三天前的删记录?不可能。所有持久化需求,必须由你主动写入归档表。










