MySQL在处理带DISTINCT的SELECT语句时,并不会固定使用某一种去重方式。优化器会评估表结构、可用索引、结果列和排序需求,选择基于有序索引扫描、临时表去重或排序后去重等路径。这些路径会通过执行计划中的Using index、Using temporary和Using filesort等线索暴露出来,因此读懂执行计划是判断去重成本的第一步。

一、执行计划里如何识别DISTINCT的去重路径
在MySQL中,可以用EXPLAIN或EXPLAIN ANALYZE查看DISTINCT语句的执行计划。表格格式的Extra列通常会出现几个关键标记:Using index表示仅通过索引树就能完成查询,不需要回表读取完整行;Using temporary表示优化器决定创建内部临时表来处理结果;Using filesort表示结果需要额外排序。
对于去重来说,如果Extra只出现Using index而没有Using temporary,通常意味着MySQL借助索引的有序性完成去重。如果出现了Using temporary,则说明存在临时表去重,数据量较大时还可能伴随磁盘临时表。虽然临时表不一定是坏设计,但它是去重成本的重要信号。
下面创建一张订单表,后续示例都会围绕它展开:
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
category_id INT NOT NULL,
customer_name VARCHAR(50) NOT NULL,
KEY idx_category_id (category_id)
);
表里category_id列有普通二级索引,customer_name列没有索引。这个差异会让MySQL在去重时选择完全不同的执行路径。
二、MySQL实现DISTINCT去重的几种算法
1. 基于有序索引的顺序扫描去重
如果DISTINCT涉及的列恰好可以从某个索引中按顺序读取,MySQL会优先选择索引顺序扫描。二级索引的叶节点是有序的,因此相同键值会相邻存放。SQL层在读取记录时只需要保存上一次输出的键值,遇到与上一次相同的值就跳过,遇到新值才返回给客户端。
这种方式的优点是几乎不需要额外内存,也不会产生临时表,去重代价非常低。执行计划通常表现为Using index:
EXPLAIN SELECT DISTINCT category_id FROM orders;
上述语句只需读取idx_category_id索引的键值即可拿到去重后的分类ID,属于覆盖索引去重。
2. 临时表唯一键去重
当去重列没有索引,或者查询需要返回非索引列而不得不回表时,MySQL可能无法利用有序索引直接去重。此时优化器会创建内部临时表,把中间结果写入临时表,并为需要去重的列建立唯一索引。插入数据时如果出现重复键值,MySQL会忽略后续重复行,只保留第一条记录。完成所有数据插入后,再扫描临时表输出最终结果。
如果临时表数据量较小,MySQL会使用内存中的MEMORY存储引擎,基于哈希索引完成唯一性判断,速度很快;一旦数据量超过tmp_table_size或max_heap_table_size,临时表会落到磁盘上,改为使用InnoDB存储引擎,这时唯一键去重的I/O成本会明显上升。
EXPLAIN SELECT DISTINCT customer_name FROM orders;
这条SQL中customer_name没有索引,MySQL需要扫描全表,再把所有客户名称插入临时表完成去重,执行计划的Extra中大概率会出现Using temporary。
3. 排序后相邻比较去重
如果DISTINCT和ORDER BY一起出现,并且ORDER BY的列不能复用现有索引顺序,MySQL会先对中间结果进行排序,然后在有序结果上比较相邻值完成去重,或者先生成去重临时表再排序。执行计划通常同时出现Using temporary和Using filesort:
EXPLAIN SELECT DISTINCT customer_name FROM orders ORDER BY customer_name;
严格来说,排序去重并不是完全独立的第三种算法,它常与临时表配合使用。排序的目的是让重复值相邻,便于后续去重,但排序本身会增加CPU和磁盘开销,尤其是对较长的字符串列。
三、结合EXPLAIN判断去重成本
仅从表格Extra列有时还不够直观。MySQL 8.0提供了EXPLAIN FORMAT=TREE,能够以树形结构显示查询计划,其中可能包含Remove duplicate rows之类的去重节点。通过树形计划可以更清楚地区分去重发生在索引扫描之后还是临时表写入之后。
EXPLAIN FORMAT=TREE SELECT DISTINCT category_id FROM orders; EXPLAIN FORMAT=TREE SELECT DISTINCT customer_name FROM orders;
执行第一个语句时,计划通常表现为从二级索引读取数据后直接去除相邻重复值;第二个语句则可能显示全表扫描、临时表写入以及唯一键去重。对比两个计划能直观看到索引缺失对去重路径的影响。
另一个常用工具是优化器跟踪,通过SET optimizer_trace='enabled=on';执行查询后,再读取information_schema.optimizer_trace表。跟踪结果会详细列出优化器是否考虑临时表、排序以及索引访问方式,适合定位复杂查询中去重成本过高的原因。
四、DISTINCT去重的优化建议
最直接的优化方式是为去重列建立索引。例如上面的customer_name去重查询,如果经常执行,可以创建普通索引:
CREATE INDEX idx_customer_name ON orders (customer_name);
建索引后,DISTINCT查询可以利用索引有序性去重,临时表通常不再出现。但如果去重列是很大的文本字段,索引体积和排序成本会上升,需要结合业务查询频率综合判断。
如果DISTINCT包含多个列,可以创建联合索引,并且尽量让查询只访问这些列,形成覆盖索引。例如经常执行SELECT DISTINCT category_id, customer_name FROM orders,可以建立idx_category_customer (category_id, customer_name)联合索引,使MySQL直接沿联合索引顺序扫描并跳过重复组合,避免回表和临时表。
CREATE INDEX idx_category_customer ON orders (category_id, customer_name); EXPLAIN SELECT DISTINCT category_id, customer_name FROM orders;
还要注意DISTINCT与ORDER BY的列顺序。如果去重和排序能够共用同一个索引,排序和去重就可能同时被索引优化吸收;否则会出现Using filesort。例如SELECT DISTINCT category_id FROM orders ORDER BY category_id可以复用idx_category_id;而如果按无索引的列排序,则需要额外排序。
最后,在某些子查询场景中,DISTINCT可能会限制半连接优化。把DISTINCT去掉并改用JOIN或EXISTS,有时能让优化器选择更高效的关联顺序。优化不应该盲目录,而应该先通过执行计划确认去重路径,再针对Index、Temporary或Filesort做定向调整。
MySQL执行计划Distinct去重去重算法修改时间:2026-08-28 10:00:01