逻辑删除本质是UPDATE改状态字段而非DELETE,因后者不可逆、破坏外键与审计;必须全局过滤status=0、建复合索引、显式校验状态以保障一致性。

逻辑删除不是 DELETE,恢复也不靠 binlog 或备份 —— 它本质就是一条 UPDATE 语句改回状态字段。
为什么不能用 DELETE 实现逻辑删除
物理删除(DELETE FROM table WHERE ...)会真正移除数据行,不可逆,且无法通过简单操作“还原”。逻辑删除必须保留原始记录,仅改变其可见性标识。常见做法是加一个 is_deleted 或 status 字段,值为 0(正常)、1(已删除)等。
- 用
DELETE会导致关联查询丢失外键引用,比如子表查不到父记录,抛出NULL或关联失败 - 前端分页、统计、导出都依赖全量数据存在,删了就没了
- 审计、溯源、历史比对都需要原始行,物理删等于主动丢弃证据
UPDATE 设置删除状态的正确写法
核心动作是更新状态字段,但必须带精准 WHERE 条件,否则整表变“已删除”——这是线上事故高发点。
- 推荐写法:
UPDATE user SET status = 1, updated_at = NOW() WHERE id = 123 AND status = 0(加双重校验,避免重复操作) - 禁止写法:
UPDATE user SET status = 1(没WHERE,全表沦陷) - 如果用 MyBatis,XML 中要确保
<if test="id != null">AND id = #{id}</if>不被绕过 - 建议在 SQL 层或 DAO 方法里默认追加
AND status = 0,防止误删已删过的记录
恢复数据就是反向 UPDATE
恢复不是“从备份捞”,而是把状态字段改回来。前提是:该字段没被其他业务逻辑覆盖,且没有级联硬删除子表。
- 基础恢复语句:
UPDATE user SET status = 0, updated_at = NOW() WHERE id = 123 AND status = 1 - 若涉及多表联动(如订单下有订单项),需同步更新子表:
UPDATE order_item SET status = 0 WHERE order_id IN (SELECT id FROM `order` WHERE id = 456) - 注意事务边界:主表和子表更新必须在同一个事务中,否则出现“主表恢复了、子表没恢复”的不一致
- 不要依赖
INSERT ... SELECT恢复——那是在补数据,不是逻辑恢复;原记录 ID、创建时间等元信息必须保留
容易被忽略的陷阱
逻辑删除看着简单,但真实项目里踩坑最多的地方不在 SQL 本身,而在配套机制缺失。
- 所有
SELECT查询必须默认带上AND status = 0,否则“已删”数据会意外曝光(尤其报表、后台导出) - 索引没包含
status字段时,大表查询性能会断崖下跌,建议复合索引如(status, created_at) - 软删后
COUNT(*)和真实有效数据量不等,监控告警若只看总行数会误判 - MySQL 8.0+ 支持生成列,可用
is_active TINYINT AS (CASE WHEN status = 0 THEN 1 ELSE 0 END) STORED配合索引加速,但别忘了 DML 仍要手动维护status
逻辑删除的可靠性,取决于每一条 SELECT、UPDATE、INSERT 是否都显式处理了状态字段——它不是一次配置,而是一套贯穿全链路的约束习惯。


















