慢查詢優化是「觀察 → 假設 → 驗證 → 迭代」的科學實驗。本文記錄一條 2000 萬行表上的 GROUP BY ... MAX(id) 慢查詢優化全過程:EXPLAIN 執行計劃解讀、子查詢改 JOIN、臨時表參數調優、Profile 火焰圖定位、多種寫法效能對比,展示從「跑不出結果」到「秒級返回」的工程化優化閉環。
CREATE TABLE `tt` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `name` CHAR(16) NOT NULL DEFAULT '', `rule_id` INT UNSIGNED NOT NULL DEFAULT '0', `status` TINYINT UNSIGNED NOT NULL DEFAULT '1', PRIMARY KEY (`id`), KEY `ttx` (`name`, `rule_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 數據量:~2000 萬行
業務需求**:找出「相同 name、相同 rule_id、status=1」分組下的最後一條記錄(最大 id)。
SELECT * FROM tt WHERE id IN ( SELECT MAX(id) AS max_id FROM tt GROUP BY name, rule_id ) AND status = 1;
+----+--------------------+-------+------+---------------+------+---------+------+---------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------------+-------+------+---------------+------+---------+------+---------+-------------+ | 1 | PRIMARY | tt | ALL | NULL | NULL | NULL | NULL | 1176818 | Using where | | 2 | DEPENDENT SUBQUERY | tt | index| NULL | ttx | 52 | NULL | 1 | Using index | +----+--------------------+-------+------+---------------+------+---------+------+---------+-------------+
核心問題**:外層 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 ) AS a LEFT JOIN tt b ON a.id = b.id;
+----+-------------+------------+--------+---------------+---------+---------+------+---------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+------------+--------+---------------+---------+---------+------+---------+-------------+ | 1 | PRIMARY || ALL | NULL | NULL | NULL | NULL | 1176503 | | | 1 | PRIMARY | b | eq_ref | PRIMARY | PRIMARY | 4 | a.id | 1 | | | 2 | DERIVED | tt | index | NULL | ttx | 52 | NULL | 1176818 | Using index |
改進**:派生表掃描 117 萬行,但主表走 eq_ref 主鍵回表,總耗時 1.78s。
SET profiling=1; -- 執行查詢 SHOW PROFILE FOR QUERY 6; +------------------------------+----------+ | Status | Duration | +------------------------------+----------+ | Sending data | 1.101490 | | converting HEAP to MyISAM | 0.070147 | | Sending data | 1.193679 | | removing tmp table | 0.001491 | +------------------------------+----------+
關鍵發現**:converting HEAP to MyISAM 表明內存臨時表超過 max_heap_table_size(預設 16MB)轉磁碟 MyISAM,導致大量 I/O。
SELECT @@max_heap_table_size/1024/1024; -- 16MB SET max_heap_table_size = 16777216 * 4; -- 64MB SELECT @@max_heap_table_size/1024/1024; -- 64MB
效果**:耗時 1.78s → 1.69s(-5%),Profile 中 converting HEAP to MyISAM 消失,熱點集中在 Sending data(存儲引擎層讀取行)。
SELECT MAX(CONCAT_WS(' ', LPAD(id,5,' '), status)) AS res, name, rule_id
FROM tt GROUP BY name, rule_id;
+----+-------------+-------+------+---------------+------+---------+------+---------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+---------+-------+ | 1 | SIMPLE | tt | index| NULL | ttx | 52 | NULL | 1176818 | | +----+-------------+-------+------+---------------+------+---------+------+---------+-------+
1.83s(反而更慢),Profile 顯示純 Sending data 1.82s,無臨時表轉換但單線程掃描 117 萬行索引葉子節點,字串拼接開銷大。
| 寫法 | 耗時 | 核心特徵 | 評價 |
|---|---|---|---|
| 子查詢 IN | 跑不完 | DEPENDENT SUBQUERY + 外層全表掃 | ❌ 不可用 |
| LEFT JOIN 派生表 | 1.78s → 1.69s | 派生表掃描 + eq_ref 回表 | ✅ 可用,參數調優有效 |
| MAX + CONCAT_WS | 1.83s | 單索引掃描 + 字串拼接 | ⚠️ 更慢,不適用多列返回 |
GROUP BY name, rule_id 必須掃描全表索引(117 萬行),無法通過索引直接定位分組最大值Sending data I/O 待機時間可大幅降低name Hash 分區,分區裁剪減少掃描範圍rule_id 或 user_id 分片,單表數據量降階IN (id_list) 主鍵批量查詢Sending data、converting HEAP to MyISAM、Copying to tmp tablemax_heap_table_size/tmp_table_size、sort_buffer_size、join_buffer_size慢查詢優化沒有銀彈,只有「觀測 → 假設 → 驗證 → 迭代」的工程紀律。建立「慢查詢收集 → EXPLAIN/Profile 分析 → 參數/寫法/索引/架構多維嘗試 → 效果驗證 → 知識庫沉澱」閉環,讓優化從藝術變為工程。