在一張擁有 30 萬條記錄的表 test 中,主鍵 id 已建立聚簇索引。需求是分頁查詢:從第 208,888 條記錄開始取 50 條。直覺寫法如下:
SELECT * FROM `test` ORDER BY id ASC LIMIT 208888, 50;
實測執行時間 **40~50 秒**,嚴重拖慢業務。為什麼有索引卻這麼慢?
ORDER BY ... LIMIT offset, n 的深分頁陷阱offset 行(此處 208,888 行)並丟棄,才能返回後 50 行。id 是主鍵(B+ 樹),但「掃描 + 丟棄」仍需遍歷大量葉子節點,產生大量隨機 I/O。LIMIT offset, n 隨著 offset 增大性能呈線性下降,百萬級數據常見秒級甚至分鐘級延遲。若業務允許「按 ID 範圍查詢」而非「按位置偏移查詢」,可改寫為:
SELECT * FROM `test`
WHERE id BETWEEN 208838 AND 208888;
實測執行時間 **約 0.06 秒**,提升 **近 1000 倍**。
BETWEEN 如此之快?ORDER BY 的臨時表與 filesort 開銷。| 場景 | 推薦寫法 | 備註 |
|---|---|---|
| 已知 ID 範圍(如游標分頁、時間窗口) | WHERE id > last_id LIMIT n 或 BETWEEN | 性能最佳,無深分頁問題 |
| 傳統頁碼分頁(用戶可跳頁) | 延遲關聯 / 子查詢優化 | 見下文「延遲關聯」模式 |
| 非連續 ID 或需複雜排序 | 覆蓋索引 + LIMIT | 需評估索引覆蓋度 |
若必須保留 LIMIT offset, n 語義,可用「先查主鍵、再回表」兩步走:
-- 步驟 1:只走主鍵索引,極快
SELECT id FROM `test` ORDER BY id LIMIT 208888, 50;
-- 步驟 2:用 IN 或 JOIN 回表取完整列
SELECT t.* FROM `test` t
JOIN (
SELECT id FROM `test` ORDER BY id LIMIT 208888, 50
) tmp USING (id);
原理:子查詢只掃描主鍵索引(極窄),不回表;外層按主鍵精確點查 50 次,總耗時通常在 **毫秒級**。
SELECT *:只取必要列,提高覆蓋索引命中率。WHERE a=1 AND b=2 ORDER BY c → 索引 (a,b,c)。EXPLAIN 分析:關注 type(應為 range/ref/eq_ref)、rows、Extra(避免 Using filesort/Using temporary)。LIMIT 200000, 50 強制全掃描前 20 萬行。WHERE id BETWEEN ... 或 WHERE id > last_id 利用 B+ 樹順序讀取。EXPLAIN:每次 SQL 變更後驗證執行計劃,防止索引失效。掌握這些模式,能讓 MySQL 在千萬級甚至億級數據下依然保持毫秒級響應。