游乐游手机版
首页/数据库/文章详情

MySQL DISTINCT去重的几种方法

时间:2026-06-28 06:43
先来看一个真实案例。有位同学在统计不重复用户数时,写了 SELECT DISTINCT user_id FROM orders,查询执行了 30 秒。DBA 查看执行计划后发现没有走索引,触发了 Using temporary(临时表)。 很多场景下,SQL 查询变慢的根源就在这里。今天我们来彻底拆

先来看一个真实案例。有位同学在统计不重复用户数时,写了 SELECT DISTINCT user_id FROM orders,查询执行了 30 秒。DBA 查看执行计划后发现没有走索引,触发了 Using temporary(临时表)。

MySQL DISTINCT 去重的几种方法使用

很多场景下,SQL 查询变慢的根源就在这里。今天我们来彻底拆解 DISTINCT 的去重原理与 MySQL 优化方案,耐心读完这篇文章,你完全可以把 30 秒的慢查询优化到 0.01 秒以内。

DISTINCT 是什么?

DISTINCT 是 SQL 中用于去除重复行的关键字,这也是它最基础、最核心的功能。

基本用法

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

关键问题来了:假如 orders 表有 2000 万行数据,user_id 重复值很多,DISTINCT 就需要扫描 2000 万行再去重,查询速度自然会非常慢。

DISTINCT 的两种算法

MySQL 实现 DISTINCT 去重主要有两种方式:临时表去重和索引去重。

1. 临时表去重(性能较差)

当 DISTINCT 查询的字段没有索引时,MySQL 会把所有数据行先加载到临时表中,再对临时表进行去重处理。

执行流程

1. 扫描所有行 → 放入临时表
2. 对临时表去重 → 返回最终结果

这个过程的缺陷很明显:

  • 需要扫描所有数据行(很可能触发全表扫描)
  • 依赖临时表(如果数据量大可能会写入磁盘,速度更慢)

验证一下

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

两个需要警惕的信号:

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

2. 索引去重(性能优异)

如果 DISTINCT 去重的字段建有索引,MySQL 可以利用 B+ 树索引的有序性来高效去重,完全不需要临时表。

原理说明

索引采用 B+ 树结构存储,相同值的记录在索引中是连续排列的。MySQL 顺序扫描索引时,遇到相同值可以直接跳过,效率极高。

索引:user_id[1, 1, 1, 2, 2, 3, 3, 3, ...]
扫描去重:1 → 跳过相同的 1, 1 → 2 → 跳过相同的 2 → 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 |       |
+----+-------------+--------+-------+---------------+-----------------+---------+------+----------+-------+

优化效果非常显著:

  • type = index(走索引扫描)
  • Extra 中已经没有了 Using temporary
  • 查询执行时间从 30 秒降低到 0.1 秒(实现了 300 倍性能提升)

DISTINCT 的性能陷阱:临时表

临时表是导致 DISTINCT 查询性能低下的关键因素。什么情况下会触发临时表?主要有三个典型场景。

陷阱 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_at
SELECT DISTINCT user_id FROM orders ORDER BY created_at;  -- Using temporary

问题分析:DISTINCT 想利用 user_id 索引去重,而 ORDER BY 要求按 created_at 排序,两者冲突,MySQL 只能启用临时表。解决方案是让 DISTINCT 和 ORDER BY 使用同一个索引。

-- 优化后: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 本身已经具备去重能力。

-- 优化后:去掉 DISTINCT
SELECT 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 同样具备去重功能,而且 MySQL 对 GROUP BY 的优化策略比 DISTINCT 更成熟,更容易利用索引。

优化前

-- DISTINCT 可能触发临时表
SELECT DISTINCT user_id FROM orders;  -- Using temporary

优化后

-- GROUP BY 可以走索引
SELECT user_id FROM orders GROUP BY user_id;  -- 没有 Using temporary

优化方案 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 步:初始化汇总表

INSERT INTO user_order_count (user_id, order_count, updated_at)
SELECT user_id, COUNT(*), NOW() FROM orders GROUP BY user_id;

第 3 步:定时更新汇总表

通过 MySQL 事件或 cron 定时任务定期执行:

-- MySQL 事件:每小时更新一次
CREATE EVENT update_user_order_count
ON SCHEDULE EVERY 1 HOUR
DO
    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 步:查询直接读取汇总表

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

问题确认:全表扫描并且使用了临时表。

第 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,不再有 Using temporary,执行时间从 30 秒降低到 0.1 秒。

实战建议汇总

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 的字段必须保持一致

字段不一致必然会触发临时表。

-- 优化前:字段不一致
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
-- 优化后:去掉 DISTINCT
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id;  -- 没有 Using temporary

4. 尽量用 WHERE 条件限制数据范围

能过滤就过滤,需要去重的数据行数减少了,查询性能自然会提升。

-- 优化前:没有 WHERE 条件
SELECT 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 和 ORDER BY 字段不一致、DISTINCT 和 GROUP BY 混用
  • 五种实用优化方案:添加索引、覆盖索引、用 GROUP BY 替代、WHERE 限制范围、使用汇总表
  • 面试时如果能把这套逻辑讲清楚,从原理分析到实战优化,一定能够体现出扎实的技术功底

以上所有 SQL 语句均在 MySQL 8.0 环境下验证通过,可以放心在实际项目中参考使用。

来源:https://www.jb51.net/database/366074vak.htm
上一篇MySQL三大concat函数详解与用法 下一篇Linux系统下MySQL远程连接配置与用户授权
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
金仓数据库逻辑备份实战:全库导出与模式替换全流程
数据库 · 2026-07-03

金仓数据库逻辑备份实战:全库导出与模式替换全流程

在长期的运维实践中,我越来越体会到,备份就像一份保险——平时看似无用,但关键时刻却是唯一的救命稻草。逻辑备份看似简单,可真正执行恢复时,各种陷阱接连浮现:表名大小写不一致、Schema 未正确切换、Owner 属性未同步修改……任何一个环节处理不当,最终恢复出的数据库就会与预期相去甚远。 本文将深入

金仓数据库sys_rman物理备份全流程演练与误覆盖恢复
数据库 · 2026-07-03

金仓数据库sys_rman物理备份全流程演练与误覆盖恢复

干运维这行,逻辑备份和物理备份我都接触过,但说句实在话,真正能在生产环境里扛住事儿的,还得是物理备份。逻辑备份导出的是 SQL 语句,数据量一大,那速度慢得让人抓狂,而且最关键的是,它没法做时间点恢复。物理备份不一样,它直接拷贝数据文件,再配上 WAL 归档日志,想恢复到过去哪一秒都行,这是它最硬核

Windows下将MySQL注册为系统自启服务教程
数据库 · 2026-07-03

Windows下将MySQL注册为系统自启服务教程

先说一个关键前提:务必以管理员身份运行终端,否则 mysqld --install 这条命令几乎不可能成功。问题不在于命令写错,而是 Windows 系统的用户账户控制(UAC)机制会在中途拦截——在普通 CMD 或 PowerShell 窗口执行这条命令,要么直接提示 Access is deni

Mac版Navicat中快速对比两个数据库的表结构异同
数据库 · 2026-07-03

Mac版Navicat中快速对比两个数据库的表结构异同

直接说结论:Mac 版 Navicat 和 Windows 版在表结构比对逻辑上完全一致。但默认配置下,它确实无法承受“全库一键比对上万张表”的压力。要想避免卡死、内存溢出、进度条永远停在 0%,你必须手动将表分批处理,或者利用前缀过滤来控制扫描范围。 为什么 Mac 上点击「结构同步」后界面会卡住

MySQL中UNION操作推荐用UNION ALL的原因
数据库 · 2026-07-03

MySQL中UNION操作推荐用UNION ALL的原因

MySQL中UNION与UNION ALL性能对比:别再被“保险”迷惑,差距远超预期 先给出核心结论:UNION ALL 的性能通常比 UNION 高出不止一个数量级。原因在于,UNION 在合并结果集后会自动触发去重操作,这往往伴随着隐式排序,进而产生临时表和文件排序。而 UNION ALL 则直