EXPLAIN SELECT * FROM message ORDER BY id DESC LIMIT 10000, 20;
-- type: index, rows: 10020, Extra: Using index
-- 實際掃描 10020 行,丟棄前 10000 行,返回後 20 行
隨著偏移量 M 增大,掃描行數 = M + N 線性增長,大表深分頁 (如第 10000 頁) 直接導致全表掃描、緩存污染、鎖爭用、響應超時。
InnoDB 聚簇索引按主鍵有序。LIMIT M, N 需從最左葉子節點順序掃描 M 條記錄定位起始位置,再讀取 N 條。**無法跳躍**,必須逐行訪問。
適用場景:前端只需「上一頁/下一頁」或「加載更多」,不需跳轉任意頁碼 (如 Feed 流、評論列表、日誌查詢)。
記住當前頁**最大/最小 ID** (或排序字段值),下一頁直接用範圍查詢定位,利用索引範圍掃描 (Range Scan) 直接定位,掃描行數固定為 N。
-- 假設按 id 降序,每頁 20 條
-- 當前頁最大 id = 9527,最小 id = 9500
-- 下一頁 (id < 9500)
SELECT * FROM message WHERE id < 9500 ORDER BY id DESC LIMIT 20;
-- 上一頁 (id > 9527)
SELECT * FROM message WHERE id > 9527 ORDER BY id ASC LIMIT 20; -- 取出後應用層反轉
優勢:無論翻多深,每次只掃描 20 行,毫秒級響應。**缺點**:無法直接跳轉第 N 頁。
適用場景:後台管理、報表、需「首頁/尾頁/頁碼跳轉」的傳統分頁。
先在**覆蓋索引**上按排序掃描定位主鍵 ID (極快,不回表),再用主鍵 ID 回表查詢完整列。
-- 原始慢查詢
SELECT * FROM message ORDER BY id DESC LIMIT 10000, 20;
-- 優化:子查詢只取 id (覆蓋索引),外層回表
SELECT m.*
FROM message m
INNER JOIN (
SELECT id FROM message ORDER BY id DESC LIMIT 10000, 20
) tmp ON m.id = tmp.id
ORDER BY m.id DESC;
原理解析:內層 SELECT id ... LIMIT 10000, 20 走主鍵索引掃描 10020 行,**只讀取 id 列 (覆蓋索引),不回表**,極快。外層僅 20 次主鍵點查。總開銷 ≈ 索引掃描 10020 行 (極窄) + 20 次點查,遠優於原查詢 10020 次完整行回表。
適用場景**:用戶多在當前頁附近跳轉 (如 10→12 頁),極少直接跳首/尾頁。
-- 當前頁:id 最大 9527,最小 9500,每頁 20 條
-- 跳轉第 8 頁 (向前 2 頁,偏移 40 行)
SELECT * FROM message WHERE id > 9527 ORDER BY id ASC LIMIT 40, 20;
-- 結果需應用層反轉
-- 跳轉第 13 頁 (向後 3 頁,偏移 60 行)
SELECT * FROM message WHERE id < 9500 ORDER BY id DESC LIMIT 60, 20;
原理**:利用當前頁邊界 ID 作為錨點,計算目標頁相對偏移。偏移量 = |目標頁 - 當前頁| × 頁大小。就近跳轉偏移量小,掃描行數可控。極端跳轉 (首→尾) 退化為全掃描,可回退方案二。
| 方案 | 支援任意頁跳轉 | 性能複雜度 | 適用業務 | 實現複雜度 |
|---|---|---|---|---|
| 游標式 (ID 範圍) | 否 (僅上/下頁) | O(N) 固定 | Feed、日誌、無限滾動 | 低 |
| 延遲關聯 (子查詢回表) | 是 | O(Index Scan M + N 点查) | 後台管理、報表、傳統分頁 | 中 |
| 相對偏移跳轉 | 就近頁碼 | O(Offset + N) | 就近翻頁為主、極少首尾跳轉 | 中 |
long_query_time=1 捕獲深分頁慢 SQL,定期分析優化。