「按用戶分組,取每組最新的一筆記錄」是經典分組 Top-N 問題。本文對比三種常見 MySQL 寫法,分析執行計畫差異,給出效能最優建議。
表 member_payment:id (PK)、member_id、amount、create_time。需求:每個 member_id 取 id 最大(最新)的一筆,限制 10 組。
SELECT * FROM (
SELECT * FROM member_payment ORDER BY id DESC
) t GROUP BY member_id LIMIT 10;
執行計畫:Using temporary; Using filesort —— 內層全表排序產生臨時表,外層再分組,雙重開銷。資料量大時極慢。
SELECT s.*
FROM (
SELECT MAX(id) AS id FROM member_payment GROUP BY member_id LIMIT 10
) t
LEFT JOIN member_payment s ON t.id = s.id;
執行計畫:
t:GROUP BY member_id 走 idx_member_id 掃描,取各組 MAX(id)(索引最右值),極快。LEFT JOIN:按主鍵 id 精準回表 10 次,ref 類型,零臨時表。SELECT * FROM member_payment mp
WHERE EXISTS (
SELECT 1 FROM (
SELECT MAX(id) AS id FROM member_payment GROUP BY member_id LIMIT 10
) t WHERE t.id = mp.id
);
執行計畫:類似寫法二,但優化器可能將 EXISTS 轉為 SEMI JOIN,在某些版本上比 LEFT JOIN 略慢。
| 寫法 | 臨時表 | Filesort | 掃描行數 | 推薦等級 |
|---|---|---|---|---|
| 排序後 GROUP BY | 是 | 是 | 全表 | ❌ 避免 |
| MAX(id) + LEFT JOIN | 否 | 否 | 索引掃描 + 10 次主鍵查找 | ✅ 首選 |
| EXISTS 子查詢 | 否 | 否 | 索引掃描 + 10 次主鍵查找 | ✅ 次選 |
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY member_id ORDER BY id DESC) AS rn
FROM member_payment
) t WHERE rn = 1 LIMIT 10;
PARTITION BY 分組,ORDER BY 排序,ROW_NUMBER()=1 取第一筆。rn <= 3)。Window Function,無臨時表,效能與寫法二持平。為支援上述所有寫法,請建立複合索引:
CREATE INDEX idx_member_id_id ON member_payment (member_id, id DESC);
此索引同時服務 GROUP BY member_id 取 MAX(id) 及窗口函數分組排序。
拒絕「先排序再 GROUP BY」的反模式。採用 MAX(pk) + JOIN 或 窗口函數 ROW_NUMBER(),配合 (group_col, pk DESC) 複合索引,即可獲得穩定高效的分組 Top-N 查詢。