软删除需配套deleted_at时间戳、deleted_by操作人、过滤索引及可审计恢复路径,而非仅is_deleted字段;查询应通过视图或存储过程默认隔离已删数据,恢复须经专用过程记录日志。

软删除不是加个 is_deleted 字段就完事,数据恢复也不是简单把字段改回来——关键在查询隔离、索引设计、恢复路径是否可审计,否则会漏查、慢查、甚至误恢复。
软删除字段必须带时间戳和操作人
只用 is_deleted BIT DEFAULT 0 是危险的:无法区分“谁删的”“什么时候删的”,后续恢复时容易覆盖真实业务逻辑(比如某条记录本该被新流程覆盖,结果被误还原)。
-
deleted_at DATETIME2 NULL(SQL Server)或deleted_at TIMESTAMP NULL(MySQL),避免用GETDATE()直接赋默认值——它会在 INSERT 时就写入,而不是 DELETE 时 - 必须配套
deleted_by VARCHAR(50) NULL,从应用层传入用户名/工号,不能靠触发器查SYSTEM_USER,后者在连接池场景下不可靠 - 建复合索引:
CREATE INDEX IX_table_deleted_at ON table_name(deleted_at) WHERE deleted_at IS NOT NULL(SQL Server 过滤索引),否则全表扫描WHERE deleted_at IS NULL会越来越慢
SELECT 查询必须默认过滤已删除数据
应用代码里写 WHERE deleted_at IS NULL 容易漏,且分散难维护。更稳的方式是用视图或带条件的存储过程封装读取逻辑。
- 不要依赖 ORM 的全局软删除钩子(如 Laravel 的
SoftDeletestrait),它可能被手动绕过或在 raw query 中失效 - 推荐创建只读视图:
CREATE VIEW v_orders AS SELECT * FROM orders WHERE deleted_at IS NULL,让业务查询只面向视图,物理表只供管理脚本访问 - 如果必须用存储过程查,参数加
@include_deleted BIT = 0,默认不返回已删数据,需要时才显式传1,避免误查
恢复数据不能只 UPDATE deleted_at
单纯执行 UPDATE orders SET deleted_at = NULL WHERE id = 123 会丢失删除上下文,且无法回溯“为什么恢复”。生产环境必须走可审计的恢复路径。
- 恢复动作必须走专用存储过程,例如
usp_restore_order @order_id INT, @restored_by VARCHAR(50),内部做三件事:检查该记录是否真被软删(deleted_at IS NOT NULL)、INSERT 一条恢复日志到restore_log表、再 UPDATE 原表 -
restore_log表至少含:id、table_name、record_id、restored_by、restore_time、original_deleted_at(方便比对时间差) - 禁止直接在应用层拼 SQL 恢复,尤其不能把
@order_id直接拼进字符串——要用sp_executesql+ 参数化,哪怕只一个参数
大表软删除要防锁表和索引膨胀
对千万级订单表执行批量软删除(比如 UPDATE orders SET deleted_at = GETDATE() WHERE status = 'cancelled'),可能阻塞写入、撑爆事务日志、拖慢索引维护。
- 分批执行:用
TOP (10000)+WHILE @@ROWCOUNT > 0循环,每次提交事务,避免单次长事务 - 避免在
deleted_at上建非过滤索引——它会包含所有行,导致索引体积翻倍;只保留上面说的过滤索引(WHERE deleted_at IS NOT NULL) - 定期归档真正废弃的数据:
DELETE FROM orders WHERE deleted_at ,但必须先确认无下游依赖(如报表、对账系统还在查半年前的软删记录)
最常被忽略的是恢复后的数据一致性校验——比如订单恢复后,关联的支付流水、物流单是否仍有效?软删除和恢复从来不是单表操作,而是业务状态机的一环,不联动校验,就只是把数据“挪回原位”,不是真恢复。











