松散索引扫描对distinct至关重要,因其能让mysql跳过重复值、仅取每组首个索引项,避免临时表和全量排序;但仅适用于单列distinct且该列须为复合索引的连续最左前缀,同时where条件限于等值过滤更左列,explain中显示“using index for group-by”即生效。

DISTINCT 性能差,大概率是因为没走索引扫描,而是被迫建临时表;松散索引扫描(Loose Index Scan)是 MySQL 5.7+ 对单列 DISTINCT 的关键加速机制,但触发条件很苛刻,不是加了索引就自动生效。
为什么松散索引扫描对 DISTINCT 很重要
松散索引扫描能让 MySQL 在遍历索引时“跳过重复值”,直接取每组第一个值,避免把所有匹配行拉进临时表再排序去重。它不读全索引页,只扫最左前缀的“骨架”,所以快。但它的前提是:DISTINCT 字段必须是复合索引的最左前缀,且不能有 WHERE 条件干扰索引使用顺序。
- ✅ 有效场景:
SELECT DISTINCT dept FROM employees;+ 索引INDEX(dept, city) - ❌ 失效场景:
SELECT DISTINCT city FROM employees WHERE dept = 'tech';+ 同样索引 —— 因为dept是前导列但被用于等值过滤,city变成非最左前缀,无法松散扫描 - ⚠️ 注意:
EXPLAIN中看到Extra: Using index for group-by就代表松散索引扫描已启用(别被字段名误导,它也用于 DISTINCT)
如何让 DISTINCT 触发松散索引扫描
核心是让优化器能“顺着索引头一路跳着读”,而不是回表或全扫。操作上要同时满足三件事:
- 被
DISTINCT的字段必须是某个复合索引的**连续最左前缀**,例如DISTINCT a, b需要索引INDEX(a, b, c),不能是INDEX(b, a)或INDEX(a, c, b) -
WHERE条件只能作用于该索引的**更左前缀列**,且必须是等值条件(如WHERE a = ?),不能是范围(>、BETWEEN)、函数(UPPER(a))或隐式转换(a = 123但 a 是字符串) - 查询中不能出现
ORDER BY或LIMIT以外的排序依赖;如果带ORDER BY,字段顺序必须和索引一致,否则退化为紧凑索引扫描甚至临时表
DISTINCT 多列时松散扫描还管用吗
不管用。松散索引扫描只支持单列 DISTINCT,这是 MySQL 的硬限制。一旦写成 SELECT DISTINCT a, b FROM t,即使有 INDEX(a, b),优化器也无法跳着读——它得保证每组 (a,b) 值只取一次,必须逐行比对,大概率触发 Using temporary。
- 替代方案:改用
GROUP BY a, b,并确认执行计划是否出现Using index for group-by(某些 5.7+ 场景下 GROUP BY 比 DISTINCT 更容易触发松散逻辑) - 更稳做法:确保
INDEX(a, b)是覆盖索引,避免回表;同时用WHERE尽量缩小数据集,减少临时表体积 - 极端情况:如果只是想取前 N 个唯一组合,强制加
LIMIT N,MySQL 有时会提前终止扫描,比全量去重省得多
常见误判和隐形坑
很多人看 EXPLAIN 显示 type: index 就以为优化成功了,其实那只是“索引全扫描”,和松散扫描完全是两回事——前者仍需读完全部索引项,后者只读部分节点。
- NULL 值会影响松散扫描效果:索引中大量
NULL会让跳跃逻辑失效,建议对DISTINCT字段加NOT NULL约束 -
SELECT DISTINCT *永远无法触发松散扫描,因为要比较整行,必须回表读所有字段,务必改成明确列名 - 字符集/排序规则不一致(比如表用
utf8mb4_0900_as_cs,索引列却是utf8mb4_general_ci)会导致索引失效,DISTINCT 退化为临时表 - MySQL 8.0.22+ 引入了哈希去重(
hash_group),但在 DISTINCT 场景默认不启用;如真卡在临时表,可尝试SET optimizer_switch='hash_group=on';测试,但注意内存开销
松散索引扫描不是银弹,它是高度依赖索引结构和查询写法的“窄通道”。真正要稳,得从建索引那一刻就想好 DISTINCT 的字段顺序,而不是事后补救。











