首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫

MySQL DISTINCT 與 GROUP BY 去重分組完整指南:語義差異、隱含欄位、排序優化與實戰陷阱

     編輯:MySQL 資料庫相關知識

前言

DISTINCT 與 GROUP BY 都能實現「去重」,但語義、回傳欄位、排序行為、效能特徵截然不同。MySQL 對 GROUP BY 有非標準擴展 (隱含欄位),易導致不可預期結果。本文系統對比兩者,並給出最佳實踐與避坑指南。

一、DISTINCT

1.1 語法與語義

SELECT DISTINCT col1, col2 FROM table;
  • 對 **SELECT 清單所有欄位的組合** 去重,保留第一次出現的行 (順序不保證)。
  • DISTINCT 只能出現在 SELECT 緊後,不可單獨作用於單欄位 (如 SELECT DISTINCT(col), col2 實際對所有列去重)。
  • **效能**:需建立臨時表/排序去重,大資料量極慢。

1.2 常見誤區

-- 錯誤預期:只對 username 去重,保留 uid
SELECT DISTINCT(username), uid FROM user;
-- 實際:對 (username, uid) 組合去重,若 uid 不同則視為不同行

**修正**:若需「每個 username 取一行」,用 GROUP BY username 或視窗函數。

二、GROUP BY

2.1 標準 SQL 語義

  • SELECT 清單中的非聚合欄位,**必須出現在 GROUP BY 子句中**。
  • 聚合函數 (COUNT、SUM、MAX、MIN、AVG、GROUP_CONCAT) 可用於非分組欄位。

2.2 MySQL 非標準擴展:隱含欄位

-- 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 後報錯。

2.3 正確寫法

-- 方案 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 vs GROUP BY 效能對比

場景推薦原因
單純去重、無聚合DISTINCT語意清晰,優化器內部同樣用臨時表/排序
需聚合統計 (計數、求和、最大值)GROUP BY一次掃描完成分組與聚合
需「每組取第一行」 (如最新文章)視窗函數 ROW_NUMBER()MySQL 8.0+ 標準寫法,避免隱含欄位陷阱
大表去重且有索引GROUP BY + 索引可用 Using index for group-by (Loose Index Scan) 避免臨時表

四、進階:Loose Index Scan (鬆散索引掃描)

條件:
- GROUP BY 欄位為索引最左前綴。
- 無 WHERE、無 JOIN、無聚合函數需掃描非索引列。
- EXPLAIN 顯示 Using index for group-by。
效能:直接遍歷索引葉節點,**不回表、不建臨時表**,極快。

五、視窗函數替代方案 (MySQL 8.0+)

-- 每位用戶最新一篇文章 (標準、確定性、高效)
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」的標準解法,建議全面遷移。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved