is_deleted字段推荐使用tinyint unsigned not null default 0;unsigned防止负数写入,not null避免null导致查询遗漏,default 0使新数据默认可见。

is_deleted 字段该用什么类型和默认值
别用 TINYINT(1) 或 BOOLEAN —— MySQL 没原生布尔类型,TINYINT(1) 容易被 ORM 或客户端误当成显示宽度(实际只是别名),还可能参与数值计算;也别用 VARCHAR 存 'true'/'false',索引失效、空间浪费、无法比较。
推荐直接定义为:TINYINT UNSIGNED NOT NULL DEFAULT 0。理由很实在:
-
UNSIGNED防止写入负数(比如 -1),避免脏数据干扰状态判断 -
NOT NULL强制每行都有明确状态,避免NULL导致WHERE is_deleted = 0漏掉整行(NULL = 0永远为 false) -
DEFAULT 0让新插入数据天然“可见”,不用每次INSERT都手动写is_deleted = 0
所有 SELECT 必须显式加 AND is_deleted = 0
常见错误现象:SELECT * FROM order WHERE user_id = 123 返回空,但后台日志显示这人明明下过单——八成是漏加 AND is_deleted = 0 条件。
不能依赖应用层统一拦截(比如 MyBatis-Plus 的全局逻辑删除插件),因为手写 SQL、临时脚本、DBA 直连、报表工具直查等场景极易遗漏。必须在每条业务 SQL 中显式写出过滤条件:
- 单表查询:
SELECT * FROM user WHERE status = 'active' AND is_deleted = 0 - 多表 JOIN:
LEFT JOIN order o ON u.id = o.user_id AND o.is_deleted = 0(注意:ON 里加,不是 WHERE) - 子查询或 EXISTS 中,同样要带
is_deleted = 0,否则可能误关联已删除记录
UPDATE / DELETE 操作要防重复与误覆盖
逻辑删除本质是 UPDATE,不是 DELETE。直接写 UPDATE user SET is_deleted = 1 WHERE id = 123 有风险:
- 若该记录已被删过,再次执行会把状态从 1 改回 1(无害但浪费),更糟的是恢复操作可能被覆盖
- 应加上前置状态校验:
UPDATE user SET is_deleted = 1 WHERE id = 123 AND is_deleted = 0 - 恢复操作同理:
UPDATE user SET is_deleted = 0 WHERE id = 123 AND is_deleted = 1,防止多次点击“还原”导致翻转 - 执行后务必检查
ROW_COUNT()是否为 1,等于 0 表示记录不存在或已删,需给前端明确提示
唯一索引冲突怎么破
当字段(如 username)有唯一索引,又要做逻辑删除时,username = 'alice' AND is_deleted = 1 和新插入的 username = 'alice' AND is_deleted = 0 会触发唯一键冲突。
最稳妥解法是改唯一索引为联合索引:ALTER TABLE user ADD UNIQUE INDEX uk_username_deleted (username, is_deleted)。
这样允许同一 username 存在多条记录,只要 is_deleted 值不同即可。但要注意:
- 旧数据的
is_deleted是0或1,新插入时默认为0,不会和已删的1冲突 - 如果历史数据
is_deleted允许为NULL(不推荐),那UNIQUE索引对多个NULL不校验,反而埋坑 - 千万别用
deleted_at DATETIME NULL替代is_deleted后还保留单列唯一索引——NULL在唯一约束下行为复杂,MySQL 8.0 前甚至允许多个NULL,极易出错
真实项目里最容易被忽略的,是 JOIN 语句中把 is_deleted = 0 错放 WHERE 而非 ON,导致 LEFT JOIN 变 INNER JOIN;还有就是唯一索引改造后没清理历史重复数据,上线就报错。











