某業務表 2000 萬行,需求:查找相同 name、rule_id 下 status=1 的最大 id 記錄。本文記錄從原始 SQL 到最終優化方案的完整演進,含執行計劃分析、Profile 火焰圖定位、參數調優、硬件升級建議。
CREATE TABLE `tt` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `name` CHAR(16) NOT NULL DEFAULT '', `rule_id` INT(10) UNSIGNED NOT NULL DEFAULT '0', `status` TINYINT(3) UNSIGNED NOT NULL DEFAULT '1', PRIMARY KEY (`id`), KEY `ttx` (`name`, `rule_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 數據量:2000W 行,name/rule_id 組合基數約 117 萬
SELECT * FROM tt WHERE id IN (SELECT MAX(id) FROM tt GROUP BY name, rule_id) AND status = 1;
執行計劃:外層 type=ALL 全表掃描 117 萬行,內層 DEPENDENT SUBQUERY 每行執行一次索引掃描 → 極慢,需殺進程
SELECT a.id, b.name, b.rule_id FROM (SELECT MAX(id) AS id FROM tt GROUP BY name, rule_id) a LEFT JOIN tt b ON a.id = b.id;
執行計劃:
type=ALL(物化到臨時表)117 萬行type=eq_ref 主鍵查找Profile 定位:converting HEAP to MyISAM 耗時 0.07s,Sending data 總耗時 1.77s
調優嘗試:max_heap_table_size=64M 避免臨時表落盤 → 耗時 1.69s(提升微乎其微)
SELECT MAX(CONCAT_WS(' ', LPAD(id,5,' '), status)) AS res, name, rule_id
FROM tt GROUP BY name, rule_id;
結果:1.82s,更慢。GROUP BY 默認返回首行,MAX() 無法正確攜帶非聚合列值,屬於「看似優雅實則錯誤」的寫法。
| 階段 | 耗時佔比 | 根因 |
|---|---|---|
| 衍生表物化 | ~30% | 全索引掃描 117 萬行構建臨時表 |
| 臨時表轉換 | ~5% | HEAP → MyISAM 落盤(內存不足) |
| JOIN 回表 | ~65% | 117 萬次主鍵查找(隨機 I/O) |
rule_id 或 id 範圍分區,縮小掃描範圍rule_id 取模分片,中間件路由ALTER TABLE tt ADD INDEX idx_cover (name, rule_id, id, status); 使衍生表與 JOIN 均走覆蓋索引,避免回表DEPENDENT SUBQUERY 仍可能退化derived_merge 合併優化,但複雜聚合仍會物化ONLY_FULL_GROUP_BY=OFF 允許,結果不可預期-- 開啟 Profile SET profiling=1; -- 執行目標 SQL -- 查看詳細耗時 SHOW PROFILES; SHOW PROFILE FOR QUERY; -- 查看優化器追蹤 (5.6+) SET optimizer_trace="enabled=on"; SELECT ...; SELECT * FROM information_schema.optimizer_trace;