不能直接用DELETE语句做软删除,因为DELETE是物理删除,会彻底移除数据、丢失审计线索、破坏外键级联;软删除本质是标记删除,须用deleted_at或is_deleted字段配合UPDATE实现,并在所有查询中显式过滤。

为什么不能直接用 DELETE 语句做软删除
直接执行 DELETE FROM users WHERE id = 123 就是物理删除,数据彻底丢失,无法恢复,审计和历史追踪也断了。软删除本质是“标记删除”,不是删数据,而是加一个状态字段让查询自动过滤掉已删记录。
必须加的字段:deleted_at 或 is_deleted
主流做法是加一个 deleted_at 字段(DATETIME 类型,允许 NULL),删时设为当前时间;少数场景用布尔型 is_deleted(TINYINT(1) 或 BOOLEAN)。前者更推荐,因为能知道删的时间点,也方便后续做软删除清理或恢复。
-
deleted_at为NULL表示未删除;非NULL表示已软删除 - 务必给该字段加索引,否则带
WHERE deleted_at IS NULL的查询可能全表扫描 - 不要用默认值(如
DEFAULT '1970-01-01')代替NULL,会混淆“未删”和“误删”
所有 SELECT 都要显式过滤未删除记录
这是最容易漏、也最致命的一环——只要漏写一次 WHERE deleted_at IS NULL,业务就可能读到已被“删除”的脏数据。不能依赖应用层统一拦截,必须在 SQL 层硬约束。
- 常规查询写法:
SELECT * FROM orders WHERE status = 'paid' AND deleted_at IS NULL - 关联查询时,JOIN 条件里也要加:
JOIN users u ON o.user_id = u.id AND u.deleted_at IS NULL - 避免用视图封装“安全查询”,视图无法阻止开发绕过它直接查原表
- ORM 如 Laravel Eloquent 或 Django ORM 提供了软删除 trait,但底层仍是靠自动追加
WHERE deleted_at IS NULL,你要确认它真生效了(比如查生成的 SQL 日志)
UPDATE 和 DELETE 语句要重定义语义
对外暴露的 “DELETE /users/123” 接口,后端不能执行物理 DELETE,而应转为 UPDATE users SET deleted_at = NOW() WHERE id = 123 AND deleted_at IS NULL。同理,恢复操作就是 UPDATE ... SET deleted_at = NULL。
- UPDATE 时一定要加
AND deleted_at IS NULL条件,防止误恢复已删记录 - 物理删除(如归档清理)应走独立脚本,命名明确(如
purge_soft_deleted_users),且需二次确认 - 注意唯一约束:如果
email字段有UNIQUE索引,软删除后插入同 email 新用户会冲突——解决办法是把唯一索引改成函数索引(MySQL 8.0+ 支持CREATE UNIQUE INDEX idx_email ON users (email) WHERE deleted_at IS NULL)或用复合索引(email, deleted_at)
软删除看着简单,真正落地时最常翻车的是查询漏过滤、索引没建、唯一约束崩掉这三处。别指望 ORM 全帮你兜住,每个 SQL 语句都得亲手过一遍逻辑。

















