MySQL 效能調校實戰 2026:EXPLAIN、索引優化與查詢提速
為什麼 2026 年還要死磕 MySQL 調校
儘管 NewSQL 與向量資料庫興起,MySQL 仍承載著全球絕大多數 OLTP 流量。多數「慢」問題並非硬體不足,而是索引缺失、執行計畫走偏、或連線管理混亂。本文聚焦可落地的調校動作。
| 症狀 | 常見根因 |
|---|---|
| 偶發卡頓 | 缺少索引,全表掃描 |
| 越用越慢 | 緩衝池過小,磁碟 IO 飆升 |
| 高峰雪崩 | 連線數失控,執行緒爭用 |
| 單條 SQL 逾時 | 深分頁 / 不合理 JOIN |
第一步:用 EXPLAIN 看清執行計畫
任何調校都從 EXPLAIN 開始。重點關注 type、key、rows、Extra。
EXPLAIN SELECT id, user_id, amount
FROM orders
WHERE user_id = 10086 AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;
關鍵欄位判讀:
| 欄位 | 健康值 | 危險信號 |
|---|---|---|
| type | ref / range / const | ALL(全表掃描) |
| key | 命中預期索引 | NULL |
| rows | 接近實際命中行 | 遠大於預期 |
| Extra | Using index | Using filesort / Using temporary |
拿到複雜 SQL 先到 SQL 格式化 工具裡美化,便於定位巢狀與 JOIN 順序。
索引設計的三個核心法則
法則一:聯合索引遵循最左前綴
索引 (user_id, status, created_at) 能命中 user_id=,也能命中 user_id+status,但無法單獨命中 status。
-- 命中索引
SELECT * FROM orders WHERE user_id = 1 AND status = 'PAID';
-- 用不上該聯合索引的 status 部分(索引跳躍掃描除外)
SELECT * FROM orders WHERE status = 'PAID';
法則二:優先覆蓋索引(Covering Index)
如果查詢欄位全部在索引裡,MySQL 無需回表,效能飛躍。
-- 建立覆蓋索引
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, created_at);
-- Using index:直接從索引返回,不回表
EXPLAIN SELECT user_id, status, created_at
FROM orders WHERE user_id = 10086;
法則三:區分度高的列放前面
把 user_id(區分度高)放聯合索引前列,把低區分度(如 status 僅有幾種值)靠後。
慢查詢日誌:讓問題自己現形
-- 開啟慢查詢日誌(超過 200ms 記錄)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.2;
SET GLOBAL log_queries_not_using_indexes = 'ON';
配合 pt-query-digest 或內建 sys.statement_analysis 視圖定位 Top SQL。
InnoDB 緩衝池調校
緩衝池(buffer pool)是 InnoDB 的效能心臟,目標是讓熱點資料常駐記憶體。
-- 查看緩衝池命中率(應 > 99%)
SHOW ENGINE INNODB STATUS\G
-- Buffer pool hit rate 幾乎接近 1000/1000 為佳
-- 生產建議:buffer pool 設為可用記憶體的 60%~75%
-- 在 my.cnf 中:
-- innodb_buffer_pool_size = 12G
-- innodb_buffer_pool_instances = 8 -- 減少多核爭用
查詢層優化
避免深分頁的「LIMIT 100000, 20」
深分頁會讓 MySQL 先掃描 10 萬行再丟棄。用游標/延遲關聯改寫:
-- 反模式
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;
-- 優化:先取主鍵,再 JOIN 回表
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 20) t ON o.id = t.id;
用 EXISTS 替代 IN 大列表
-- 當子查詢結果集很大時
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 1000);
只查需要的列
SELECT * 不僅浪費 IO,還會讓覆蓋索引失效。明確列名是性價比最高的優化。
JOIN 優化要點
- 驅動表選小表,被驅動表連接列必須有索引。
- 關聯欄位型別必須一致(避免隱式轉換導致索引失效)。
- 大表 JOIN 考慮冗餘維度欄位,減少跨表。
連線池:別讓連線成為瓶頸
應用側連線管理混亂是高峰雪崩的主因。以 Node.js 為例:
import mysql from "mysql2/promise";
const pool = mysql.createPool({
host: "db.example.com",
user: "app",
database: "shop",
connectionLimit: 20, // 控制並發連線
waitForConnections: true,
queueLimit: 50, // 超出排隊的請求上限
enableKeepAlive: true
});
// 永遠用 try/finally 釋放連線,避免洩漏
const [rows] = await pool.query("SELECT * FROM orders WHERE user_id = ?", [uid]);
用 Hash 計算 工具可以產生快取鍵,將熱點查詢結果快取,減輕資料庫壓力。
常見問題 FAQ
Q1:加了索引為什麼還是慢?
可能:索引選擇性差、統計資訊過期(ANALYZE TABLE)、或查詢沒走最左前綴。用 EXPLAIN 驗證實際命中的 key。
Q2:索引越多越好嗎?
不是。索引拖慢寫入、佔用空間。遵循「讀多寫少的热点查詢才建索引」原則。
Q3:buffer pool 越大越好嗎?
不是無腦大。超過實體記憶體會觸發 swap,反而更慢。一般設為可用記憶體 60%~75%。
Q4:VARCHAR 索引要注意什麼?
長字串建索引用前綴索引:INDEX(col(20)),但前綴索引無法用於覆蓋掃描的精確匹配場景,需權衡。
Q5:怎麼判斷該不該分表?
單表行數上億、或熱點行鎖競爭嚴重、或備份視窗過長時,再考慮分庫分表。優先索引與查詢優化。
工具推薦
在 MySQL 調校實務中,以下 工具庫 能幫上忙:
MySQL 調校沒有銀彈,但有方法論:先用 EXPLAIN 找到瓶頸,再用索引與緩衝池解決 IO,最後用連線池穩住高峰。把這三層做對,絕大多數慢查詢都會消失。
本站提供瀏覽器本地工具,免註冊即可試用 →