当前位置: 首页
数据库
MySQL DISTINCT去重的几种方法

MySQL DISTINCT去重的几种方法

热心网友 时间:2026-06-28
转载

先来看一个真实案例。有位同学在统计不重复用户数时,写了 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

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
如何使用MongoDB聚合管道进行数据模型转换?

如何使用MongoDB聚合管道进行数据模型转换?

$project通过显式声明字段和计算表达式重塑文档结构,需注意_id处理。$unwind展开数组前应先$match过滤,空数组需保留。$lookup关联时类型不匹配用$toString或$toObjectId转换。$out全量替换,$merge增量更新,需关注阶段间数据形态衔接。

时间:2026-07-08 06:58
MongoDB 6.0/7.0用索引分析器优化慢查询方法

MongoDB 6.0/7.0用索引分析器优化慢查询方法

优化MongoDB6 0 7 0慢查询:用db setProfilingLevel()捕获慢操作,explain( "executionStats ")分析索引,$indexStats清理冗余索引;联合索引字段顺序需遵循等值→范围→排序原则,并定期监控与调整。

时间:2026-07-08 06:58
MongoDB中$pullAll删除数组多个元素的方法

MongoDB中$pullAll删除数组多个元素的方法

MongoDB6 0+已彻底移除$pullAll,需改用$pull+$in组合删除数组多个元素。删除嵌套数组元素须用$[]或位置操作符。需注意重复值会影响删除性能,且该操作不可原子回滚,必须自行实现幂等补偿。

时间:2026-07-08 06:58
MongoDB写关注配置保障数据安全

MongoDB写关注配置保障数据安全

事务中写关注仅在commitTransaction时生效,内部单条操作参数被忽略。commitTransaction的writeConcern控制事务oplog被确认的节点数,可设w:majority或j:true。非事务写操作可灵活配置,高优先级覆盖低优先级。即便降低w值,事务仍有两阶段锁和oplog写入的固有开销。

时间:2026-07-08 06:58
如何设计MongoDB全文索引文档结构最佳实践指南

如何设计MongoDB全文索引文档结构最佳实践指南

MongoDB全文索引仅支持string类型字段,多字段需显式设权重,中文搜索须指定default_language: "zh "或用短语匹配,且不提供语义相似性检索。正确设计文档、校验字段类型及合理调整权重,是确保$text查询有效的关键。

时间:2026-07-08 06:58
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 日榜
  • 周榜
  • 月榜