LIMIT offset, count 是 MySQL 分頁的標準語法,但隨著 offset 增大(深分頁),性能急劇下降。本文深度解析深分頁性能瓶頸原理,提供四種優化方案:延遲關聯、覆蓋索引、游標分頁、ID 範圍過濾,附適用場景對比。
-- 淺分頁:極快
SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 0;
-- 深分頁:極慢(掃描 100 萬行丟棄 999,990 行)
SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 1000000;
根因:MySQL 必須掃描 offset + count 行,丟棄前 offset 行,再返回 count 行。offset 越大,掃描越多,IO 與 CPU 消耗線性增長。
-- 原始慢查詢
SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 1000000;
-- 優化:先只查主鍵,再回表
SELECT o.*
FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 10 OFFSET 1000000
) tmp USING (id);
原理:子查詢只掃描主鍵索引(窄、快),外層按主鍵回表僅 10 行。將「掃描寬表 100 萬行」降為「掃描窄索引 100 萬行 + 回表 10 行」。
-- 建立覆蓋索引(包含所有查詢列)
CREATE INDEX idx_orders_cover ON orders (id, user_id, amount, status, created_at);
-- 查詢只需掃描索引,無需回表
SELECT id, user_id, amount, status, created_at
FROM orders
ORDER BY id LIMIT 10 OFFSET 1000000;
條件:查詢列全部包含在索引中。代價:索引體積大、寫入開銷增大。
-- 第一頁
SELECT * FROM orders WHERE id > 0 ORDER BY id LIMIT 20;
-- 後續頁:用上一頁最大 ID 作為游標
SELECT * FROM orders WHERE id > 1000020 ORDER BY id LIMIT 20;
優點:無 OFFSET,性能恆定、不受頁碼影響;天然防重複/遺漏(數據變更時)。
缺點:無法跳轉任意頁、需前端配合(傳遞游標而非頁碼)。
-- 用戶只看「最近 30 天」訂單,不翻歷史深頁
SELECT * FROM orders
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY id LIMIT 20 OFFSET 0;
適用:業務邏輯天然限制查詢範圍(如「最近 N 天」、「狀態=進行中」)。
| 方案 | 原理 | 性能提升 | 適用場景 | 缺點 |
|---|---|---|---|---|
| 延遲關聯 | 窄索引掃描 + 主鍵回表 | 5-10 倍 | 通用、需任意跳頁 | 仍需掃描 offset 行索引 |
| 覆蓋索引 | 純索引掃描,零回表 | 10-50 倍 | 查詢列固定、讀多寫少 | 索引膨脹、寫入變慢 |
| 游標分頁 | 無 OFFSET,游標定位 | 恆定極快 | 高併發、無限滾動、API 分頁 | 不支援任意跳頁 |
| ID 範圍過濾 | 業務邏輯縮小掃描範圍 | 數量級 | 天然有時間/狀態範圍的業務 | 需業務配合 |
rows:預估掃描行數(應遠小於 offset+limit)Extra:Using index(覆蓋索引)、Using where、Using temporary; Using filesort(需優化)深分頁本質是 OFFSET 掃描浪費。游標分頁徹底消除 OFFSET,是架構級解法;延遲關聯、覆蓋索引是存量系統低成本優化手段。根據業務約束選擇:API/無限滾動→游標;後台管理/任意跳頁→延遲關聯/覆蓋索引;有時間範圍→範圍過濾疊加。配合 EXPLAIN 驗證 rows 下降,即可量化優化效果。