一聚教程网:一个值得你收藏的教程网站

热门教程

MySQL DISTINCT 去重的几种使用方法

时间:2026-06-25 08:37:54 编辑:袖梨 来源:一聚教程网

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

MySQL DISTINCT 去重的几种方法使用

今天咱们就来扒一扒 DISTINCT 的去重原理,看完这篇,你就能把 30 秒的查询优化到 0.01 秒。

DISTINCT 是啥?

DISTINCT 用于去重(去掉重复行)。

基本用法

-- 统计不重复的用户数SELECT DISTINCT user_id FROM orders;

问题:如果 orders 表有 2000 万行,user_id 有很多重复值,DISTINCT 要扫描 2000 万行,还要去重,很慢。

DISTINCT 的两种算法

MySQL 的 DISTINCT 有两种算法:临时表去重 和 索引去重。

1. 临时表去重(慢!)

如果 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 |+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+

问题:

  1. type = ALL(全表扫描)
    1. Extra = Using temporary(用临时表)

2. 索引去重(快!)

如果 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 |       |+----+-------------+--------+-------+---------------+-----------------+---------+------+----------+-------+

优化效果:

  1. type = index(索引扫描)
    1. Extra 里没有 Using temporary 了(走索引去重,不需要临时表)
    1. 执行时间从 30 秒降到 0.1 秒(300 倍提升!)

DISTINCT 的坑:临时表

DISTINCT 最大的坑是临时表。

什么时候会用临时表?

DISTINCT 的字段没索引

  • DISTINCT 和 ORDER BY 的字段不一样
  • DISTINCT 和 GROUP BY 混用

坑 1: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

坑 2:DISTINCT 和 ORDER BY 的字段不一样

-- 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

坑 3:DISTINCT 和 GROUP BY 混用

-- 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

优化方案 1:给 DISTINCT 的字段加索引(推荐!)

思路:让 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 倍提升!)

优化方案 2:用覆盖索引

思路:如果查询的字段都在索引里,不需要回表,性能更好。

优化前

-- 查询所有字段,要回表SELECT DISTINCT * FROM orders;  -- 执行 30 秒

优化后

-- 查询的字段都在索引里,不需要回表SELECT DISTINCT user_id FROM orders;  -- 执行 0.1 秒

优化效果:不需要回表,性能提升 10 倍。

优化方案 3:用 GROUP BY 代替 DISTINCT

思路: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 更成熟,更容易走索引。

优化方案 4:用 WHERE 限制范围

思路:如果 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 倍。

优化方案 5:用汇总表

思路:建一张汇总表,定期更新(比如每小时更新一次),查询时直接读汇总表。

第 1 步:建汇总表

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;

第 3 步:定时更新汇总表

用定时任务(比如 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 倍提升!)

实战:优化一个慢 DISTINCT

假设有个订单表,要统计不重复的用户数,很慢:

SELECT COUNT(DISTINCT user_id) FROM orders;  -- 执行 30 秒

第 1 步:看执行计划

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 |+----+-------------+--------+------+---------------+------+---------+------+----------+----------------+

问题:

  • type = ALL(全表扫描)
  • Extra = Using temporary(用临时表)

第 2 步:给 DISTINCT 的字段加索引

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 |       |+----+-------------+--------+-------+---------------+-----------------+---------+------+----------+-------+

优化效果:

  • type = index(索引扫描)
  • Extra 里没有 Using temporary 了(走索引去重,不需要临时表)
  • 执行时间从 30 秒降到 0.1 秒(300 倍提升!)

实战建议

1. 给 DISTINCT 的字段加索引(最重要!)

这是最重要的建议。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

2. DISTINCT 和 ORDER BY 的字段要一样

如果 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

3. 不要 DISTINCT 和 GROUP BY 混用

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

4. 用 WHERE 限制范围

如果 WHERE 条件能过滤掉大部分行,去重的行数就少了,性能更好。

-- 优化前:没有 WHERESELECT DISTINCT user_id FROM orders;  -- 执行 30 秒-- 优化后:用 WHERE 限制范围SELECT DISTINCT user_id FROM orders WHERE created_at > '2024-01-01';  -- 执行 1 秒

5. 用汇总表(对实时性要求不高)

如果可以接受数据滞后,用汇总表,性能炸裂。

-- 直接读汇总表SELECT COUNT(DISTINCT user_id) FROM user_order_count;  -- 0.001 秒

总结

DISTINCT 去重的两种算法:临时表去重(慢)、索引去重(快)

  • DISTINCT 的坑:临时表(DISTINCT 的字段没索引、DISTINCT 和 ORDER BY 的字段不一样、DISTINCT 和 GROUP BY 混用)
  • 优化方案 1:给 DISTINCT 的字段加索引(推荐!)
  • 优化方案 2:用覆盖索引
  • 优化方案 3:用 GROUP BY 代替 DISTINCT
  • 优化方案 4:用 WHERE 限制范围
  • 优化方案 5:用汇总表(对实时性要求不高)

实战建议:给 DISTINCT 的字段加索引、DISTINCT 和 ORDER BY 的字段要一样、不要 DISTINCT 和 GROUP BY 混用、用 WHERE 限制范围、用汇总表

如果你能把 DISTINCT 的两种算法、临时表的坑、5 种优化方案讲清楚,面试官绝对觉得你有实战经验。

实战代码都在我本地跑过,你可以放心复制。

热门栏目