MySQL?DISTINCT?去重的幾種方法使用
我剛工作的時候,有次要統(tǒng)計不重復的用戶數(shù),寫了 SELECT DISTINCT user_id FROM orders,結(jié)果執(zhí)行了 30 秒。DBA 幫我一看執(zhí)行計劃,發(fā)現(xiàn)沒走索引,導致 Using temporary(用臨時表)。
今天咱們就來扒一扒 DISTINCT 的去重原理,看完這篇,你就能把 30 秒的查詢優(yōu)化到 0.01 秒。
DISTINCT 是啥?
DISTINCT 用于去重(去掉重復行)。
基本用法
-- 統(tǒng)計不重復的用戶數(shù) SELECT DISTINCT user_id FROM orders;
問題:如果 orders 表有 2000 萬行,user_id 有很多重復值,DISTINCT 要掃描 2000 萬行,還要去重,很慢。
DISTINCT 的兩種算法
MySQL 的 DISTINCT 有兩種算法:臨時表去重 和 索引去重。
1. 臨時表去重(慢?。?/h3>
如果 DISTINCT 的字段沒索引,MySQL 會先把所有行放到臨時表里,再對臨時表去重。
執(zhí)行流程
1. 掃描所有行 → 放到臨時表 2. 2. 對臨時表去重 → 返回結(jié)果 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 | +----+-------------+--------+------+---------------+------+---------+------+----------+----------------+
問題:
- type = ALL(全表掃描)
- 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 | | +----+-------------+--------+-------+---------------+-----------------+---------+------+----------+-------+
優(yōu)化效果:
- type = index(索引掃描)
- Extra 里沒有 Using temporary 了(走索引去重,不需要臨時表)
- 執(zhí)行時間從 30 秒降到 0.1 秒(300 倍提升?。?/li>
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_at SELECT DISTINCT user_id FROM orders ORDER BY created_at; -- Using temporary
問題:DISTINCT 要走 user_id 的索引,但 ORDER BY 要走 created_at 的索引,矛盾,只能用臨時表。
解決方案:要么都走 user_id 的索引,要么都走 created_at 的索引。
-- 優(yōu)化后: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 已經(jīng)去重了)。
-- 優(yōu)化后:去掉 DISTINCT SELECT user_id, COUNT(*) FROM orders GROUP BY user_id; -- 沒有 Using temporary
優(yōu)化方案 1:給 DISTINCT 的字段加索引(推薦?。?/h2>
思路:讓 DISTINCT 走索引去重,避免臨時表。
優(yōu)化前
-- user_id 沒有索引 SELECT DISTINCT user_id FROM orders; -- 執(zhí)行 30 秒(Using temporary)
優(yōu)化后
-- 給 user_id 加索引 CREATE INDEX idx_user_id ON orders(user_id); SELECT DISTINCT user_id FROM orders; -- 執(zhí)行 0.1 秒(沒有 Using temporary)
優(yōu)化效果:執(zhí)行時間從 30 秒降到 0.1 秒(300 倍提升?。?/p>
優(yōu)化方案 2:用覆蓋索引
思路:如果查詢的字段都在索引里,不需要回表,性能更好。
優(yōu)化前
-- 查詢所有字段,要回表 SELECT DISTINCT * FROM orders; -- 執(zhí)行 30 秒
優(yōu)化后
-- 查詢的字段都在索引里,不需要回表 SELECT DISTINCT user_id FROM orders; -- 執(zhí)行 0.1 秒
優(yōu)化效果:不需要回表,性能提升 10 倍。
優(yōu)化方案 3:用 GROUP BY 代替 DISTINCT
思路:GROUP BY 也會去重,但可以用索引,性能可能更好。
優(yōu)化前
-- DISTINCT 可能用臨時表 SELECT DISTINCT user_id FROM orders; -- Using temporary
優(yōu)化后
-- GROUP BY 可以用索引 SELECT user_id FROM orders GROUP BY user_id; -- 沒有 Using temporary
為什么? GROUP BY 的優(yōu)化比 DISTINCT 更成熟,更容易走索引。
優(yōu)化方案 4:用 WHERE 限制范圍
思路:如果 WHERE 條件能過濾掉大部分行,去重的行數(shù)就少了,性能更好。
優(yōu)化前
-- 沒有 WHERE,要去重 2000 萬行 SELECT DISTINCT user_id FROM orders; -- 執(zhí)行 30 秒
優(yōu)化后
-- 用 WHERE 限制范圍,只去重 100 萬行 SELECT DISTINCT user_id FROM orders WHERE created_at > '2024-01-01'; -- 執(zhí)行 1 秒
優(yōu)化效果:要去重的行數(shù)從 2000 萬降到 100 萬,性能提升 30 倍。
優(yōu)化方案 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 秒
優(yōu)化效果:執(zhí)行時間從 30 秒降到 0.001 秒(30000 倍提升?。?/p>
實戰(zhàn):優(yōu)化一個慢 DISTINCT
假設(shè)有個訂單表,要統(tǒng)計不重復的用戶數(shù),很慢:
SELECT COUNT(DISTINCT user_id) FROM orders; -- 執(zhí)行 30 秒
第 1 步:看執(zhí)行計劃
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);
再看執(zhí)行計劃:
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 | | +----+-------------+--------+-------+---------------+-----------------+---------+------+----------+-------+
優(yōu)化效果:
- type = index(索引掃描)
- Extra 里沒有 Using temporary 了(走索引去重,不需要臨時表)
- 執(zhí)行時間從 30 秒降到 0.1 秒(300 倍提升?。?/li>
實戰(zhàn)建議
1. 給 DISTINCT 的字段加索引(最重要!)
這是最重要的建議。DISTINCT 的字段沒索引,絕對會用臨時表,性能炸裂。
-- 優(yōu)化前:沒索引 SELECT DISTINCT user_id FROM orders; -- Using temporary -- 優(yōu)化后:加索引 CREATE INDEX idx_user_id ON orders(user_id); SELECT DISTINCT user_id FROM orders; -- 沒有 Using temporary
2. DISTINCT 和 ORDER BY 的字段要一樣
如果 DISTINCT 和 ORDER BY 的字段不一樣,會用臨時表。
-- 優(yōu)化前:字段不一樣 SELECT DISTINCT user_id FROM orders ORDER BY created_at; -- Using temporary -- 優(yōu)化后:字段一樣 SELECT DISTINCT user_id FROM orders ORDER BY user_id; -- 沒有 Using temporary
3. 不要 DISTINCT 和 GROUP BY 混用
DISTINCT 和 GROUP BY 功能重復,混用會用臨時表。
-- 優(yōu)化前:混用 SELECT DISTINCT user_id, COUNT(*) FROM orders GROUP BY user_id; -- Using temporary -- 優(yōu)化后:去掉 DISTINCT SELECT user_id, COUNT(*) FROM orders GROUP BY user_id; -- 沒有 Using temporary
4. 用 WHERE 限制范圍
如果 WHERE 條件能過濾掉大部分行,去重的行數(shù)就少了,性能更好。
-- 優(yōu)化前:沒有 WHERE SELECT DISTINCT user_id FROM orders; -- 執(zhí)行 30 秒 -- 優(yōu)化后:用 WHERE 限制范圍 SELECT DISTINCT user_id FROM orders WHERE created_at > '2024-01-01'; -- 執(zhí)行 1 秒
5. 用匯總表(對實時性要求不高)
如果可以接受數(shù)據(jù)滯后,用匯總表,性能炸裂。
-- 直接讀匯總表 SELECT COUNT(DISTINCT user_id) FROM user_order_count; -- 0.001 秒
總結(jié)
DISTINCT 去重的兩種算法:臨時表去重(慢)、索引去重(快)
- DISTINCT 的坑:臨時表(DISTINCT 的字段沒索引、DISTINCT 和 ORDER BY 的字段不一樣、DISTINCT 和 GROUP BY 混用)
- 優(yōu)化方案 1:給 DISTINCT 的字段加索引(推薦?。?/li>
- 優(yōu)化方案 2:用覆蓋索引
- 優(yōu)化方案 3:用 GROUP BY 代替 DISTINCT
- 優(yōu)化方案 4:用 WHERE 限制范圍
- 優(yōu)化方案 5:用匯總表(對實時性要求不高)
實戰(zhàn)建議:給 DISTINCT 的字段加索引、DISTINCT 和 ORDER BY 的字段要一樣、不要 DISTINCT 和 GROUP BY 混用、用 WHERE 限制范圍、用匯總表
如果你能把 DISTINCT 的兩種算法、臨時表的坑、5 種優(yōu)化方案講清楚,面試官絕對覺得你有實戰(zhàn)經(jīng)驗。
實戰(zhàn)代碼都在我本地跑過,你可以放心復制。
到此這篇關(guān)于MySQL DISTINCT 去重的幾種方法使用的文章就介紹到這了,更多相關(guān)MySQL DISTINCT 去重內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- MySQL中數(shù)據(jù)去重的兩種方式詳解(DISTINCT和GROUP BY)
- 詳解MySQL中DISTINCT去重的核心注意事項
- mysql distinct去重,IFNULL空值處理方式
- MySQL中distinct和group by去重的區(qū)別解析
- MySQL中使用distinct單、多字段去重方法
- MySQL中distinct和group?by去重效率區(qū)別淺析
- MySQL去重中distinct和group?by的區(qū)別淺析
- MySQL中使用去重distinct方法的示例詳解
- MySQL去重該使用distinct還是group by?
- Mysql中distinct與group by的去重方面的區(qū)別
相關(guān)文章
MySQL數(shù)據(jù)庫中正則表達式(Regex)和like的區(qū)別詳析
MySQL正則表達式是一種強大的文本匹配工具,允許執(zhí)行復雜的字符串搜索和處理,這篇文章主要介紹了MySQL數(shù)據(jù)庫中正則表達式(Regex)和like區(qū)別的相關(guān)資料,文中通過代碼需要的朋友可以參考下2025-11-11
分頁技術(shù)原理與實現(xiàn)之分頁的意義及方法(一)
這篇文章主要介紹了分頁技術(shù)原理與實現(xiàn)第一篇:為什么要進行分頁及怎么分頁,感興趣的小伙伴們可以參考一下2016-06-06
Last_Errno:?1062,Last_Error:?Error?Duplicate?entry
Last_Errno:?1062,Last_Error:?Error?Duplicate?entry?...?for?key?PRIMARY2014-02-02
SQL實現(xiàn)LeetCode(180.連續(xù)的數(shù)字)
這篇文章主要介紹了SQL實現(xiàn)LeetCode(180.連續(xù)的數(shù)字),本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-08-08

