Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL DISTINCT 去重

MySQL DISTINCT 去重的几种方法使用

作者:乱码字符

本文主要介绍了MySQL DISTINCT 去重的几种方法使用,重点讲解了两种去重算法临时表去重与索引去重和使用临时表,通过优化索引、使用覆盖索引或使用GROUPBY代替DISTINCT等种方法,可以显著提升查询性能

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

今天咱们就来扒一扒 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, 1
2 → 跳过相同的 2
3 → 跳过相同的 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 的字段没索引

坑 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 的索引,矛盾,只能用临时表。

解决方案:要么都走 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 已经去重了)。

-- 优化后:去掉 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 也会去重,但可以用索引,性能可能更好。

优化前

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

```sql
INSERT 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_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 步:查询时直接读汇总表

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

优化效果:

实战建议

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

-- 优化后:去掉 DISTINCT
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id;  -- 没有 Using temporary

4. 用 WHERE 限制范围

如果 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 混用、用 WHERE 限制范围、用汇总表
如果你能把 DISTINCT 的两种算法、临时表的坑、5 种优化方案讲清楚,面试官绝对觉得你有实战经验。

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

到此这篇关于MySQL DISTINCT 去重的几种方法使用的文章就介绍到这了,更多相关MySQL DISTINCT 去重内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文