DISTINCT 與 GROUP BY 都能實現「去重」,但語義、回傳欄位、排序行為、效能特徵截然不同。MySQL 對 GROUP BY 有非標準擴展 (隱含欄位),易導致不可預期結果。本文系統對比兩者,並給出最佳實踐與避坑指南。
SELECT DISTINCT col1, col2 FROM table;
DISTINCT 只能出現在 SELECT 緊後,不可單獨作用於單欄位 (如 SELECT DISTINCT(col), col2 實際對所有列去重)。-- 錯誤預期:只對 username 去重,保留 uid SELECT DISTINCT(username), uid FROM user; -- 實際:對 (username, uid) 組合去重,若 uid 不同則視為不同行
**修正**:若需「每個 username 取一行」,用 GROUP BY username 或視窗函數。
COUNT、SUM、MAX、MIN、AVG、GROUP_CONCAT) 可用於非分組欄位。-- MySQL 預設允許 (sql_mode 不含 ONLY_FULL_GROUP_BY) SELECT uid, username, COUNT(*) FROM article GROUP BY uid; -- username 未在 GROUP BY,MySQL 任意取該 uid 組內的一個 username 值 (不可預測)
**風險**:結果不確定,升級 MySQL 或開啟 ONLY_FULL_GROUP_BY 後報錯。
-- 方案 1: 加入 GROUP BY SELECT uid, username, COUNT(*) FROM article GROUP BY uid, username; -- 方案 2: 聚合函數包裝 SELECT uid, ANY_VALUE(username), COUNT(*) FROM article GROUP BY uid; -- ANY_VALUE() 明確告知優化器「任意值皆可」,避免 ONLY_FULL_GROUP_BY 報錯 -- 方案 3: 子查詢先聚合再關聯 SELECT a.uid, u.username, a.cnt FROM (SELECT uid, COUNT(*) AS cnt FROM article GROUP BY uid) a JOIN user u ON a.uid = u.uid;
| 場景 | 推薦 | 原因 |
|---|---|---|
| 單純去重、無聚合 | DISTINCT | 語意清晰,優化器內部同樣用臨時表/排序 |
| 需聚合統計 (計數、求和、最大值) | GROUP BY | 一次掃描完成分組與聚合 |
| 需「每組取第一行」 (如最新文章) | 視窗函數 ROW_NUMBER() | MySQL 8.0+ 標準寫法,避免隱含欄位陷阱 |
| 大表去重且有索引 | GROUP BY + 索引 | 可用 Using index for group-by (Loose Index Scan) 避免臨時表 |
條件:
- GROUP BY 欄位為索引最左前綴。
- 無 WHERE、無 JOIN、無聚合函數需掃描非索引列。
- EXPLAIN 顯示 Using index for group-by。
效能:直接遍歷索引葉節點,**不回表、不建臨時表**,極快。
-- 每位用戶最新一篇文章 (標準、確定性、高效)
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY uid ORDER BY created_at DESC) AS rn
FROM article
)
SELECT * FROM ranked WHERE rn = 1;
優勢:
- 語意明確:PARTITION BY 分組、ORDER BY 排序、ROW_NUMBER 編號。
- 無隱含欄位問題。
- 優化器可下推條件、使用索引。
| 現象 | 原因 | 修正 |
|---|---|---|
DISTINCT 速度極慢 | 大表無索引,建臨時表 + filesort | 加索引、或改用 GROUP BY 利用 Loose Index Scan |
GROUP BY 結果不穩定 | 隱含欄位 (非 ONLY_FULL_GROUP_BY) | 開啟 sql_mode=ONLY_FULL_GROUP_BY、用 ANY_VALUE() 或視窗函數 |
GROUP BY col1, col2 不用索引 | 索引順序為 (col2, col1) | 調整索引順序匹配 GROUP BY 順序 |
COUNT(DISTINCT col) 慢 | 需去重後計數 | 近似算法 APPROX_COUNT_DISTINCT() (MySQL 8.0.13+)、或預聚合表 |
結語: DISTINCT 適合簡單去重;GROUP BY 適合聚合統計,**務必開啟 ONLY_FULL_GROUP_BY 並配合 ANY_VALUE() 或視窗函數** 確保結果確定性。MySQL 8.0 視窗函數已成為「分組取 Top-N」的標準解法,建議全面遷移。