单独为 is_deleted 建单列索引往往无效,因其低区分度导致优化器弃用索引转全表扫描;必须将其作为等值条件置于联合索引最左位,与高频查询字段组合(如 is_deleted, status, created_at),并避免孤立索引引发写放大。

为什么单独给 is_deleted 建索引往往没用
因为 is_deleted 是典型的低区分度字段:绝大多数业务里,99% 的数据都是 is_deleted = 0。MySQL 对这种值重复率极高的字段建单列索引,优化器大概率会直接放弃走索引,转而全表扫描——尤其是当查询还带其他条件(如 WHERE is_deleted = 0 AND status = 'paid')时。
常见错误现象:EXPLAIN 显示 type: ALL、key: NULL,哪怕你已经执行了 CREATE INDEX idx_is_deleted ON user(is_deleted)。
- 别把
is_deleted当成独立过滤维度来建索引 - 它必须和业务高频查询的字段组合,才能真正生效
- 联合索引顺序很重要:等值查询字段放前,范围/排序字段放后
is_deleted 必须参与联合索引,且位置要对
假设用户表常查「未删除的活跃用户」:SELECT * FROM user WHERE is_deleted = 0 AND status = 'active' AND created_at > '2025-01-01',那么最优索引是:
ALTER TABLE user ADD INDEX idx_isdel_status_created (is_deleted, status, created_at);
理由很实在:
-
is_deleted = 0是确定的等值条件,放最左能快速定位“未删除”数据块 -
status = 'active'也是等值,紧随其后可进一步缩小范围 -
created_at > ...是范围查询,放最后,B+ 树仍能利用前两列做索引下推(Index Condition Pushdown) - 如果把
created_at放前面,is_deleted就无法被高效使用
唯一索引冲突时,is_deleted 怎么进联合索引才不翻车
当 username 有唯一约束,又要做逻辑删除,直接加 UNIQUE(username, is_deleted) 看似合理,但会出问题:已删用户 ('alice', 1) 和新用户 ('alice', 0) 能共存;但如果新用户也删了,再注册一个 'alice',就会撞上第二个 ('alice', 1)。
更稳的做法是把 is_deleted 替换为可唯一标识的值:
- 字段类型改为
is_deleted BIGINT UNSIGNED NOT NULL DEFAULT 0 - 删除时执行:
UPDATE user SET is_deleted = id WHERE id = 123 AND is_deleted = 0 - 建联合唯一索引:
ALTER TABLE user ADD UNIQUE INDEX uk_username_isdel (username, is_deleted) - 查询未删除用户时,仍用
WHERE is_deleted = 0,性能不受影响
这个方案让每条逻辑删除记录的 is_deleted 值都不同,彻底避开唯一性冲突,也不用新增字段或改业务主键。
别忽略写操作对索引的隐性压力
逻辑删除本质是 UPDATE,不是 DELETE。每次执行 UPDATE user SET is_deleted = 1 WHERE id = 123,都会触发索引树的更新——包括 PRIMARY KEY、所有含 is_deleted 的联合索引、以及普通单列索引。
容易被忽略的一点是:如果给 is_deleted 单独建了索引(比如 idx_is_deleted),那每次逻辑删除都要维护这个索引页,而它几乎从不被用于查询加速。这纯属写放大。
- 线上表如果日均百万级逻辑删除,建议删掉孤立的
is_deleted单列索引 - 只保留它参与的、真实被
WHERE条件用到的联合索引 - 定期用
sys.schema_unused_indexes检查哪些索引从未被查询使用过
索引不是越多越好,尤其是逻辑删除字段这种高频更新、低效过滤的列,它的索引价值全绑定在组合场景里。漏掉联合索引设计,或者误建单列索引,比不建索引还糟——既拖慢写入,又骗不过查询优化器。











