MySQL 8.0 通过函数索引(如 IF(deleted_at IS NULL, 1, 0))、降序索引和隐藏索引组合,使 WHERE deleted_at IS NULL 高效走索引并避免 filesort;关键在于查询条件必须字面匹配函数表达式,且需灰度验证后启用。

MySQL 8.0 不支持标准意义上的“部分索引”(如 PostgreSQL 的 WHERE 条件过滤建索引),但可以用函数索引 + 隐藏索引 + 降序索引组合,安全、高效地优化软删除查询——关键不是“只索引未删除行”,而是让 WHERE deleted_at IS NULL 这类高频条件真正走索引,并避免 filesort。
为什么 WHERE deleted_at IS NULL 很难走索引?
软删除字段(如 deleted_at)通常取值为 NULL(未删)或具体时间戳(已删),选择性极低。直接在该列建普通索引,优化器大概率忽略它,因为全表扫描比索引回表更快。
- MySQL 不会为
IS NULL单独优化索引访问路径,除非它能嵌入到复合索引的最左前缀中 -
deleted_at IS NULL是等值判断,但NULL在 B+ 树中不参与排序比较,无法利用范围扫描优势 - 如果只建
INDEX(deleted_at),EXPLAIN中type往往是index或更差,Extra出现Using where,说明只是扫了索引再过滤,没真正“定位”
用函数索引把 soft-delete 状态固化成可索引值
把 deleted_at IS NULL 转换成确定性表达式,让 MySQL 能把它存进索引树里。最常用的是 COALESCE 或布尔转换:
- 建索引:
CREATE INDEX idx_active_user ON users ((COALESCE(deleted_at, '1970-01-01')));—— 把NULL统一转成一个极小时间,使活跃用户集中在索引左侧 - 但更推荐:
CREATE INDEX idx_is_active ON users ((IF(deleted_at IS NULL, 1, 0)));—— 直接产出 0/1 布尔值,选择性高、体积小、匹配精准 - 查询必须字面一致:
WHERE IF(deleted_at IS NULL, 1, 0) = 1才能命中;写成WHERE deleted_at IS NULL就完全不走 - 注意:
IF和COALESCE都是确定性函数,但IF(deleted_at IS NULL, 1, 0)比deleted_at IS NULL多一次计算,需权衡可读性与性能
复合索引 + 降序方向,支撑带排序的软删除分页
真实场景不止查“未删除”,还要按时间倒序分页,比如:SELECT * FROM posts WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT 20。这时单靠函数索引不够,必须结合降序索引结构:
- 正确建法:
CREATE INDEX idx_active_created ON posts ((IF(deleted_at IS NULL, 1, 0)), created_at DESC); - 首列函数值固定为
1(活跃态),满足最左前缀匹配;第二列created_at DESC与ORDER BY完全一致,可消除filesort -
EXPLAIN应显示key: idx_active_created且Extra为Using index(覆盖索引)或至少无Using filesort - 错误建法:
INDEX(deleted_at, created_at DESC)——deleted_at列本身选择性差,优化器仍可能跳过;且IS NULL不等于等值匹配= NULL,无法触发最左前缀
用隐藏索引灰度验证,别急着上线
函数索引对查询写法极其敏感,线上直接启用风险高。先建隐藏索引,用 optimizer_switch 临时打开测试:
- 建隐藏函数索引:
CREATE INDEX idx_active_created INVISIBLE ON posts ((IF(deleted_at IS NULL, 1, 0)), created_at DESC); - 临时启用它做验证:
SET optimizer_switch='use_invisible_indexes=on'; EXPLAIN SELECT ...; - 观察
key是否命中、rows是否显著下降、慢查日志是否减少 - 确认无误后才设为可见:
ALTER TABLE posts ALTER INDEX idx_active_created VISIBLE; - 切记:主键和唯一约束索引不能隐藏,但软删除场景下这类索引一般不涉及
deleted_at字段,所以不影响
真正难的不是写出那个 IF(...) 表达式,而是让所有业务代码里的查询条件都统一改成匹配索引定义的写法——漏改一处,就全表扫描。函数索引生效与否,只看 EXPLAIN 输出,不看感觉,也不信注释里写的“已优化”。


















