软删除必须用update而非delete,通过deleted_at或is_deleted字段标记删除状态;所有读操作须显式过滤deleted_at is null;恢复数据即update回null或0。

软删除必须用 UPDATE,不能用 DELETE
软删除不是“换个写法的删除”,而是把「删」这个动作语义上转为「标记为已删除」。一旦执行 DELETE FROM user WHERE id = 123,数据就从页中物理移除(InnoDB)或标记为可覆盖(SQL Server),后续恢复只能靠日志或备份——这已经脱离软删除范畴。
正确做法是建表时预留状态字段:deleted_at TIMESTAMP NULL 或 is_deleted TINYINT(1) DEFAULT 0。所有业务层的“删除”操作统一改写为:
UPDATE user SET deleted_at = NOW() WHERE id = 123UPDATE user SET is_deleted = 1 WHERE id = 123
注意:deleted_at 比布尔型更实用——能查删除时间、支持按保留期清理、不干扰唯一索引(如邮箱字段在未删时仍需全局唯一);但千万别给它设 DEFAULT CURRENT_TIMESTAMP,否则新插入记录默认就是“已删除”。
查询和关联必须显式过滤 deleted_at IS NULL
只要引入软删除,所有读操作就不再是“原样查”,否则会出现:页面显示总数 120 条,点进去只有 115 条;后台统计用户数虚高;订单导出含已删客户信息……这些都不是 BUG,是漏加过滤条件的必然结果。
实操要点:
- 基础查询一律加
WHERE deleted_at IS NULL,建议封装成视图(如v_user_active)或 ORM 的全局作用域(Laravel 的SoftDeletestrait) -
JOIN时不能只写ON u.id = o.user_id,必须补上状态过滤:LEFT JOIN order o ON u.id = o.user_id AND o.deleted_at IS NULL - 聚合统计别偷懒写
COUNT(*),要用COUNT(CASE WHEN deleted_at IS NULL THEN 1 END)或先WHERE deleted_at IS NULL再计数 - 给
deleted_at字段单独建索引,否则IS NULL查询可能全表扫描(尤其大表)
恢复数据就是 UPDATE 回 NULL 或 0
软删除的“恢复”不是从某处捞数据再插一遍,而是把标记位翻回去。它快、原子、无主键冲突风险,且天然兼容事务。
典型操作:
- 单条恢复:
UPDATE user SET deleted_at = NULL WHERE id = 123 - 批量恢复:
UPDATE user SET deleted_at = NULL WHERE id IN (123, 456, 789) - 按时间范围恢复:
UPDATE user SET deleted_at = NULL WHERE deleted_at >= '2026-08-01' AND deleted_at
注意:恢复前建议先 SELECT 确认目标记录存在且状态确实为已删,避免误更新;若业务要求留痕(谁在何时恢复),需额外加字段如 restored_at 和 restored_by,并在应用层填充。
定期物理清理 deleted_at 过期数据
软删除不是存档。长期堆积会导致表体积膨胀、查询变慢、备份耗时增加、主从同步延迟加剧。必须设定明确的清理策略并自动化执行。
推荐做法:
- 定义保留期,例如:仅保留
deleted_at 的记录用于恢复 - 用定时任务(如 cron +
mysql命令)执行物理删除:DELETE FROM user WHERE deleted_at - 避免在应用层循环调用
DELETE单条——网络开销大、易超时;改用批量IN或分页LIMIT控制每次删除量 - 清理前务必确认归档策略已生效(如有审计/合规要求),且该操作不可逆
真正容易被忽略的是:清理脚本没加索引导致慢查询拖垮数据库,或者清理逻辑没进监控告警——等发现磁盘爆满才想起回收站早该清了。











