我刚工作的时候,有次要统计不重复的用户数,写了 SELECT DISTINCT user_id FROM orders,结果执行了 30 秒。DBA 帮我一看执行计划,发现没走索引,导致 Using temporary(用临时表)。

今天咱们就来扒一扒 DISTINCT 的去重原理,看完这篇,你就能把 30 秒的查询优化到 0.01 秒。
DISTINCT 用于去重(去掉重复行)。
-- 统计不重复的用户数SELECT DISTINCT user_id FROM orders;
问题:如果 orders 表有 2000 万行,user_id 有很多重复值,DISTINCT 要扫描 2000 万行,还要去重,很慢。
MySQL 的 DISTINCT 有两种算法:临时表去重 和 索引去重。
如果 DISTINCT 的字段没索引,MySQL 会先把所有行放到临时表里,再对临时表去重。
1. 扫描所有行 → 放到临时表2. 2. 对临时表去重 → 返回结果3. ```**问题**:4. 要扫描所有行(可能全表扫描)5. 2. 要用临时表(可能写到磁盘)#### 验证一下```sql-- user_id 没有索引EXPLAIN SELECT DISTINCT user_id FROM orders;
输出:
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+| 1 | SIMPLE | orders | ALL | NULL | NULL | NULL | NULL | 20000000 | Using temporary |+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+
问题:
如果 DISTINCT 的字段有索引,MySQL 可以利用索引的有序性去重,不需要临时表。
索引是有序的(B+ 树),相同的值会挨在一起。MySQL 只需要顺序扫描索引,遇到相同的就跳过,不需要临时表。
索引:user_id[1, 1, 1, 2, 2, 3, 3, 3, ...]扫描去重:1 → 跳过相同的 1, 12 → 跳过相同的 23 → 跳过相同的 3, 3...
-- 给 user_id 加索引CREATE INDEX idx_user_id ON orders(user_id);EXPLAIN SELECT DISTINCT user_id FROM orders;
输出:
+----+-------------+--------+-------+---------------+-----------------+---------+------+----------+-------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+--------+-------+---------------+-----------------+---------+------+----------+-------+| 1 | SIMPLE | orders | index | NULL | idx_user_id | 5 | NULL | 20000000 | |+----+-------------+--------+-------+---------------+-----------------+---------+------+----------+-------+
优化效果:
DISTINCT 最大的坑是临时表。
DISTINCT 的字段没索引
-- user_id 没有索引SELECT DISTINCT user_id FROM orders; -- Using temporary
解决方案:给 DISTINCT 的字段加索引。
CREATE INDEX idx_user_id ON orders(user_id);SELECT DISTINCT user_id FROM orders; -- 没有 Using temporary
-- user_id 有索引,但 ORDER BY created_atSELECT DISTINCT user_id FROM orders ORDER BY created_at; -- Using temporary
问题:DISTINCT 要走 user_id 的索引,但 ORDER BY 要走 created_at 的索引,矛盾,只能用临时表。
解决方案:要么都走 user_id 的索引,要么都走 created_at 的索引。
-- 优化后:DISTINCT 和 ORDER BY 都用 user_id 的索引SELECT DISTINCT user_id FROM orders ORDER BY user_id; -- 没有 Using temporary
-- DISTINCT 和 GROUP BY 混用,用临时表SELECT DISTINCT user_id, COUNT(*) FROM orders GROUP BY user_id; -- Using temporary
问题:DISTINCT 和 GROUP BY 功能重复,MySQL 不知道用哪个,只能用临时表。
解决方案:去掉 DISTINCT(GROUP BY 已经去重了)。
-- 优化后:去掉 DISTINCTSELECT user_id, COUNT(*) FROM orders GROUP BY user_id; -- 没有 Using temporary
思路:让 DISTINCT 走索引去重,避免临时表。
-- user_id 没有索引SELECT DISTINCT user_id FROM orders; -- 执行 30 秒(Using temporary)
-- 给 user_id 加索引CREATE INDEX idx_user_id ON orders(user_id);SELECT DISTINCT user_id FROM orders; -- 执行 0.1 秒(没有 Using temporary)
优化效果:执行时间从 30 秒降到 0.1 秒(300 倍提升!)
思路:如果查询的字段都在索引里,不需要回表,性能更好。
-- 查询所有字段,要回表SELECT DISTINCT * FROM orders; -- 执行 30 秒
-- 查询的字段都在索引里,不需要回表SELECT DISTINCT user_id FROM orders; -- 执行 0.1 秒
优化效果:不需要回表,性能提升 10 倍。
思路:GROUP BY 也会去重,但可以用索引,性能可能更好。
-- DISTINCT 可能用临时表SELECT DISTINCT user_id FROM orders; -- Using temporary
-- GROUP BY 可以用索引SELECT user_id FROM orders GROUP BY user_id; -- 没有 Using temporary
为什么? GROUP BY 的优化比 DISTINCT 更成熟,更容易走索引。
思路:如果 WHERE 条件能过滤掉大部分行,去重的行数就少了,性能更好。
-- 没有 WHERE,要去重 2000 万行SELECT DISTINCT user_id FROM orders; -- 执行 30 秒
-- 用 WHERE 限制范围,只去重 100 万行SELECT DISTINCT user_id FROM orders WHERE created_at > '2024-01-01'; -- 执行 1 秒
优化效果:要去重的行数从 2000 万降到 100 万,性能提升 30 倍。
思路:建一张汇总表,定期更新(比如每小时更新一次),查询时直接读汇总表。
CREATE TABLE user_order_count ( user_id INT PRIMARY KEY, order_count INT NOT NULL, updated_at DATETIME NOT NULL ); ```### 第 2 步:初始化汇总表```sqlINSERT INTO user_order_count (user_id, order_count, updated_at)SELECT user_id, COUNT(*), NOW() FROM orders GROUP BY user_id;
用定时任务(比如 cron、MySQL 事件)定期更新:
-- MySQL 事件:每小时更新一次CREATE EVENT update_user_order_countON SCHEDULE EVERY 1 HOURDO TRUNCATE user_order_count; INSERT INTO user_order_count (user_id, order_count, updated_at) SELECT user_id, COUNT(*), NOW() FROM orders GROUP BY user_id; ```### 第 4 步:查询时直接读汇总表```sqlSELECT COUNT(DISTINCT user_id) FROM user_order_count; -- 0.001 秒
优化效果:执行时间从 30 秒降到 0.001 秒(30000 倍提升!)
假设有个订单表,要统计不重复的用户数,很慢:
SELECT COUNT(DISTINCT user_id) FROM orders; -- 执行 30 秒
EXPLAIN SELECT COUNT(DISTINCT user_id) FROM orders;
输出:
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+| 1 | SIMPLE | orders | ALL | NULL | NULL | NULL | NULL | 20000000 | Using temporary |+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+
问题:
CREATE INDEX idx_user_id ON orders(user_id);
再看执行计划:
EXPLAIN SELECT COUNT(DISTINCT user_id) FROM orders;
输出:
+----+-------------+--------+-------+---------------+-----------------+---------+------+----------+-------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+--------+-------+---------------+-----------------+---------+------+----------+-------+| 1 | SIMPLE | orders | index | NULL | idx_user_id | 5 | NULL | 20000000 | |+----+-------------+--------+-------+---------------+-----------------+---------+------+----------+-------+
优化效果:
这是最重要的建议。DISTINCT 的字段没索引,绝对会用临时表,性能炸裂。
-- 优化前:没索引SELECT DISTINCT user_id FROM orders; -- Using temporary-- 优化后:加索引CREATE INDEX idx_user_id ON orders(user_id);SELECT DISTINCT user_id FROM orders; -- 没有 Using temporary
如果 DISTINCT 和 ORDER BY 的字段不一样,会用临时表。
-- 优化前:字段不一样SELECT DISTINCT user_id FROM orders ORDER BY created_at; -- Using temporary-- 优化后:字段一样SELECT DISTINCT user_id FROM orders ORDER BY user_id; -- 没有 Using temporary
DISTINCT 和 GROUP BY 功能重复,混用会用临时表。
-- 优化前:混用SELECT DISTINCT user_id, COUNT(*) FROM orders GROUP BY user_id; -- Using temporary-- 优化后:去掉 DISTINCTSELECT user_id, COUNT(*) FROM orders GROUP BY user_id; -- 没有 Using temporary
如果 WHERE 条件能过滤掉大部分行,去重的行数就少了,性能更好。
-- 优化前:没有 WHERESELECT DISTINCT user_id FROM orders; -- 执行 30 秒-- 优化后:用 WHERE 限制范围SELECT DISTINCT user_id FROM orders WHERE created_at > '2024-01-01'; -- 执行 1 秒
如果可以接受数据滞后,用汇总表,性能炸裂。
-- 直接读汇总表SELECT COUNT(DISTINCT user_id) FROM user_order_count; -- 0.001 秒
DISTINCT 去重的两种算法:临时表去重(慢)、索引去重(快)
实战建议:给 DISTINCT 的字段加索引、DISTINCT 和 ORDER BY 的字段要一样、不要 DISTINCT 和 GROUP BY 混用、用 WHERE 限制范围、用汇总表
如果你能把 DISTINCT 的两种算法、临时表的坑、5 种优化方案讲清楚,面试官绝对觉得你有实战经验。
实战代码都在我本地跑过,你可以放心复制。