DISTINCT默认建临时表是因为无索引时需全表扫描并缓存所有值去重;有匹配联合索引时可利用B+树有序性“边扫边跳”,实现流式去重,避免临时表。

因为联合索引的有序性让MySQL能“边扫边跳”,无需缓存所有行再比对重复。
DISTINCT 为什么默认要建临时表
没有索引时,MySQL 只能全表扫描,把所有 DISTINCT 字段值先塞进内存或磁盘临时表,再排序或哈希去重。一旦结果集超过 tmp_table_size 或 max_heap_table_size,就会落盘,性能断崖下跌。
常见错误现象包括:EXPLAIN 中出现 Using temporary 和 Using filesort 同时存在;查询响应时间随数据量非线性增长;SHOW STATUS LIKE 'Created_tmp%' 显示大量磁盘临时表创建。
- 单列
DISTINCT a却只给b建了索引 → 完全无效 - 写成
SELECT DISTINCT UPPER(name)→ 函数包裹直接让任何索引失效 - 查询是
DISTINCT user_id, status,但索引是(status, user_id)→ 顺序错配,无法利用有序性
联合索引怎么让 DISTINCT “流式去重”
当 DISTINCT a, b 对应的联合索引是 (a, b) 时,B+ 树叶子节点天然按 a 升序、a 相同时按 b 升序排列。MySQL 扫描索引即可:遇到第一个 (1, 'x') 记录就输出,后续连续的 (1, 'x') 全部跳过;下一个不同值 (1, 'y') 再输出……全程只需常数级内存保存“上一个值”,不建临时表。
关键条件有三个:
- 索引字段顺序必须和
DISTINCT列顺序严格一致(DISTINCT a, b→ 必须INDEX(a, b),不是(b, a)) - 查询不能对字段做任何计算或函数操作(
DISTINCT CONCAT(a,b)或UPPER(a)都不行) - 如果
SELECT还包含其他非索引字段(如DISTINCT a, b, c),则需覆盖索引INDEX(a, b, c),否则仍要回表,可能触发临时表
为什么单列索引有时不够用
比如 SELECT DISTINCT user_id FROM orders,只建 INDEX(user_id) 就够——这是最简情况。但一旦变成 DISTINCT user_id, order_status,单列索引就失效了:MySQL 无法保证 user_id=100 的所有行里,order_status 值在索引中是连续排列的。
此时必须建联合索引,且注意 NULL 值影响:如果字段允许 NULL,而业务上又大量存在,会导致索引页内值分布稀疏,降低“跳过重复”的效率。建议关键去重字段设为 NOT NULL。
验证方式很简单:
- 执行
EXPLAIN SELECT DISTINCT a, b FROM t;,确认Extra字段不再含Using temporary - 对比加索引前后
Created_tmp_tables和Created_tmp_disk_tables的变化 - 用
SELECT COUNT(*)替换DISTINCT看执行计划是否一致——若一致,说明索引已覆盖逻辑
真正容易被忽略的是:即使索引建对了,WHERE 条件也可能破坏流式去重。例如 SELECT DISTINCT a, b FROM t WHERE c = 'x',若索引是 (a, b) 而没包含 c,MySQL 仍得先全表过滤再对结果去重——临时表照旧。这时候得权衡:加 (c, a, b) 覆盖索引,还是接受过滤后的小结果集走临时表。


















