MySQL LIMIT offset, rows 在深分頁(大 offset)時需掃描並丟棄 offset 行,導致性能隨頁碼線性惡化。本文介紹基於覆蓋索引+延遲關聯、游標分頁、記憶上一頁最大 ID 三種優化方案。
-- 第 10 萬頁,每頁 20 行
SELECT * FROM huge_table ORDER BY id LIMIT 2000000, 20;
-- 執行計劃:type=ALL / index,rows≈200 萬,需讀取並丟棄 200 萬行
| 方案 | 原理 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|---|
| 覆蓋索引+延遲關聯 | 先只查主鍵 SELECT id FROM t ORDER BY id LIMIT offset, n,再 JOIN 回表取列 | 單表、有序主鍵、需任意跳頁 | 減少回表 IO,兼容任意頁碼 | 仍需掃描 offset 行索引 |
| 游標/Keyset 分頁 | 記住上一頁最大 ID,WHERE id > last_id ORDER BY id LIMIT n | 單向瀏覽(列表、Feed、API 分頁) | O(1) 性能,無 offset 掃描 | 不支持隨機跳頁、需唯一有序鍵 |
| 記憶上一頁最大值(應用層) | 前端傳遞 after_id 參數 | 無限滾動、移動端列表 | 極簡實現、極致性能 | 同上 |
-- 原始慢查詢
SELECT * FROM orders WHERE status=1 ORDER BY created_at DESC LIMIT 100000, 20;
-- 優化:覆蓋索引 (status, created_at, id) + 延遲關聯
SELECT o.*
FROM orders o
JOIN (
SELECT id
FROM orders
WHERE status=1
ORDER BY created_at DESC
LIMIT 100000, 20
) tmp USING (id);
子查詢僅掃描索引(Using index),外層按主鍵精確回表 20 行。
SELECT id, title, created_at
FROM articles
WHERE status=1
ORDER BY id DESC
LIMIT 20;
last_id)SELECT id, title, created_at
FROM articles
WHERE status=1 AND id < 987654321 -- last_id
ORDER BY id DESC
LIMIT 20;
索引 (status, id) 即可完美支持,EXPLAIN 顯示 range、rows=20。
大表 COUNT(*) 同樣昂貴,建議:
EXPLAIN SELECT COUNT(*) ... 讀取 rows 估算INSERT/DELETE 時 UPDATE stats SET cnt=cnt±1DATE(created_at)),導致索引失效filesort,配合 LIMIT 仍需全量排序LIMIT ... OFFSET ... 語法糖,底層機制未變深分頁優化核心思想:**減少掃描行數、利用索引順序、業務接受游標式分頁**。根據業務場景選擇方案,能帶來數量級性能提升。