MySQL仅在GROUP BY字段为联合索引最左前缀、SELECT仅含GROUP BY列及紧邻MIN/MAX、无破坏顺序的WHERE/HAVING/ORDER BY时自动启用Loose Index Scan;EXPLAIN不显式标注,需通过type为range/ref、无Using temporary、rows远小于总行数等间接证据判断。

松散索引扫描(Loose Index Scan)不是开关,而是MySQL在特定条件下自动启用的优化行为——它只在满足严格索引结构和查询模式时才生效,强行“期待它出现”反而会掩盖真正的问题。
什么情况下MySQL会用Loose Index Scan?
Loose Index Scan本质是跳着读索引:对每个分组值,只取索引中第一个匹配项,不逐行扫描整个索引段。但它有硬性前提:
- GROUP BY字段必须是**联合索引的最左前缀**,且不能被WHERE条件“切断”顺序(例如索引是
(a, b, c),GROUP BY a可触发,但GROUP BY b不行) - SELECT中只能包含:
GROUP BY字段 +MIN()/MAX()聚合(且参数必须是索引中紧邻的后续列,如GROUP BY a时可用MIN(b),但不能用MIN(c)跳过b) - 不能有
WHERE条件落在非最左列上(比如索引(a, b),WHERE b = 1 GROUP BY a就无法触发Loose Scan,因为a的有序性已被b过滤破坏) - 不能有
HAVING、ORDER BY(除非ORDER BY与GROUP BY完全一致且方向相同)
为什么EXPLAIN里没写“Loose Index Scan”?
MySQL的EXPLAIN输出中**从不显式标注“Loose Index Scan”**,你只能通过间接证据判断它是否生效:
-
type为range或ref(不是index或ALL) -
Extra里**没有**Using temporary和Using filesort -
rows值远小于表总行数(比如千万级表,rows=1.2万) - 实际执行时间毫秒级,且稳定不随数据量线性增长
如果EXPLAIN显示type=index或Extra=Using temporary,说明MySQL退化到了紧凑索引扫描(Tight Index Scan)甚至全表扫描,Loose Scan根本没启动。
常见误操作:以为加了索引就等于启用Loose Scan
很多人建了索引却没效果,问题常出在这些细节:
- 给
user_id单独建了索引,但查询是SELECT user_id, MAX(created_at) FROM orders GROUP BY user_id——created_at不在索引里,MySQL无法仅靠索引完成MAX(),被迫回表或建临时表 - 索引是
(status, user_id),但写成WHERE user_id = 123 GROUP BY status——user_id在前,status在后,最左前缀失效 - 用了
GROUP BY DATE(created_at)——函数导致索引无法按值直接比较,Loose Scan彻底放弃 - SELECT里多了
name字段:SELECT user_id, name, COUNT(*) GROUP BY user_id,而name不在索引中,MySQL必须回表,Loose Scan条件被破坏
验证Loose Scan是否真起效的最小检查清单
别只看索引存在与否,执行这三步:
- 运行
EXPLAIN FORMAT=JSON SELECT ...,重点看query_block.nested_loop.table.used_columns是否只列出索引列,且attached_condition为空 - 查
information_schema.INNODB_METRICS里innodb_rows_read增量,对比分组前后——Loose Scan下该值应接近分组键去重后的数量,而非原始行数 - 关掉
innodb_stats_on_metadata并手动ANALYZE TABLE,避免统计信息陈旧误导优化器选择错误算法
Loose Index Scan不是银弹,它的触发窗口很窄。真正稳定的优化路径,是先确保EXPLAIN里不出现Using temporary,再谈是否能进一步压到Loose Scan级别——后者只是锦上添花,前者才是生死线。


















