逻辑删除必须用 is_deleted 字段而非 delete 语句,因需留痕审计、防外键误删;读操作须显式加 and is_deleted = 0,推荐 tinyint unsigned not null default 0 类型;恢复数据应 update is_deleted = 0 并校验原状态;禁用视图或生成列替代,字段名须语义明确。

为什么不能直接用 DELETE FROM 做逻辑删除
因为真实业务里,删掉的数据往往要留痕——比如订单、用户操作记录、审批流节点,一删就查不到历史了,审计和对账全崩。硬删还可能触发外键级联,误伤关联数据。is_deleted 字段本质是“软开关”,靠查询条件过滤,而不是靠数据库删行。
常见错误现象:SELECT * FROM order WHERE user_id = 123 返回空,但后台日志显示这人明明下过单——八成是漏加 AND is_deleted = 0 条件。
- 所有读操作(
SELECT)必须显式加上AND is_deleted = 0,不能依赖应用层统一拦截(容易漏) - 写操作(
UPDATE/INSERT)不碰is_deleted,除非是“恢复删除”或“真正清理” - 索引要考虑
is_deleted:高频查询字段 +is_deleted组合建联合索引,否则WHERE is_deleted = 0 AND status = 'paid'可能全表扫
is_deleted 字段该用什么类型和默认值
别用 TINYINT(1) 或 BOOLEAN —— MySQL 没原生布尔类型,TINYINT 容易被误当成 0/1 数值参与计算;也别用 VARCHAR 存 'true'/'false',浪费空间且无法走索引。
推荐方案:TINYINT UNSIGNED NOT NULL DEFAULT 0,其中 0 表示未删除,1 表示已删除。理由很实在:
-
UNSIGNED防止负数写入(比如 -1),避免脏数据干扰判断 -
NOT NULL强制每行都有状态,避免NULL导致WHERE is_deleted = 0漏掉NULL行(NULL = 0永远为 false) - 默认值设为
0,新插入数据天然“可见”,不用每次INSERT都手动写is_deleted = 0
怎么安全地“恢复”一条逻辑删除的数据
不是执行 INSERT SELECT 回填——那是复制,不是恢复。真正的恢复是把 is_deleted 改回 0,但得防冲突和覆盖。
典型场景:运营发现某条优惠券被误点删除,要还原,但它的 code 字段是唯一索引,而当前已有同 code 的新数据存在。
- 先查:用
SELECT * FROM coupon WHERE code = 'ABC123' AND is_deleted = 1 LIMIT 1确认待恢复目标 - 再改:用
UPDATE coupon SET is_deleted = 0 WHERE id = 12345 AND is_deleted = 1,带上is_deleted = 1条件,防止重复执行导致状态翻转 - 如果涉及唯一约束冲突(如
code已被新数据占),得先处理冲突——要么删新数据,要么改新数据的code,不能强行覆盖
MySQL 8.0+ 可以用生成列自动过滤吗
不能。有人想用 GENERATED COLUMN + WHERE 视图隐藏删除行,但生成列只能基于本表字段计算,没法“动态过滤”。视图倒是可以,但有坑:
比如建视图 CREATE VIEW user_active AS SELECT * FROM user WHERE is_deleted = 0,看似干净,但实际会带来两个问题:
- 写操作不支持:对视图
INSERT/UPDATE有限制,尤其带WHERE条件的视图,MySQL 默认不允许更新 - 性能陷阱:如果底层表没给
is_deleted建索引,这个视图每次查询都可能全表扫描 - 更麻烦的是——ORM 框架通常不识别视图,
user_active在代码里得单独映射,维护成本反而更高
所以老老实实用 is_deleted = 0 条件,配合好索引和代码规范,比绕弯子搞视图或生成列靠谱得多。
最常被忽略的一点:软删除字段名别叫 deleted,它容易和 DELETE 关键字混淆(尤其在拼 SQL 字符串时),也别叫 status 这种泛化字段——后面加个“审核中”“已作废”就全乱套了。is_deleted 就是 is_deleted,语义清晰,改起来不牵连。










