LIMIT offset, count 在深度分頁 (大 offset) 時效能崩塌,本文深度解析原理,提供延遲關聯、游標/ID 範圍分頁、覆蓋索引 三大核心方案,實現毫秒級響應。
-- ❌ 深度分頁殺手
SELECT * FROM huge_table WHERE condition ORDER BY id LIMIT 1000000, 20;
-- MySQL 必須掃描前 1,000,020 行,丟棄前 1,000,000 行,僅返回 20 行
-- 效能隨 offset 線性惡化,百萬級 offset 秒級→分鐘級
EXPLAIN SELECT * FROM t WHERE c=1 ORDER BY id LIMIT 1000000, 20;
-- type: ref → ALL (全表掃描)
-- rows: 1000020 (掃描行數 = offset + limit)
核心思想: 先在索引上完成排序+定位,再回表查詢完整欄位。
-- ✅ 優化後:子查詢只查主鍵,回表僅 20 行
SELECT t.*
FROM huge_table t
INNER JOIN (
SELECT id FROM huge_table WHERE condition ORDER BY id LIMIT 1000000, 20
) tmp USING (id);
| 指標 | 原始 OFFSET | 延遲關聯 | 提升 |
|---|---|---|---|
| 掃描行數 | offset + limit | offset (索引) + limit (回表) | 回表行數減少 99.99% |
| IO 操作 | 大量隨機讀 | 索引順序讀 + 少量隨機讀 | 數量級降低 |
| 適用場景 | 任意條件、排序 | 需有排序索引 | 通用性強 |
核心思想: 不用 OFFSET,用上一頁最後一條記錄的排序鍵值作為下一頁起點。
-- 第 1 頁 (客戶端不傳游標)
SELECT * FROM huge_table WHERE condition ORDER BY id LIMIT 20;
-- 第 2 頁 (客戶端傳上一頁最後 id=1000020)
SELECT * FROM huge_table WHERE condition AND id > 1000020 ORDER BY id LIMIT 20;
-- 第 N 頁
SELECT * FROM huge_table WHERE condition AND id > last_seen_id ORDER BY id LIMIT 20;
| 限制 | 解決方案 |
|---|---|
| 不支援跳頁 (直接跳第 50 頁) | UI 設計改為「載入更多」/ 無限滾動 |
| 排序欄位非唯一 (如 created_at 有重複) | 複合游標:WHERE (created_at, id) > (last_ts, last_id) |
| 需倒序/多欄位排序 | 游標邏輯對應調整 (如 id < last_id) |
若查詢欄位全在索引中,無需回表,即使 OFFSET 掃描也極快。
-- 建立覆蓋索引 (查詢欄位: id, name, status)
CREATE INDEX idx_cover ON huge_table (condition_col, id, name, status);
-- 查詢只需掃描索引樹
SELECT id, name, status FROM huge_table WHERE condition_col = ? ORDER BY id LIMIT 1000000, 20;
需要跳頁 (直接輸入頁碼)?
├── 是 → 延遲關聯 (方案一) + 適度覆蓋索引
└── 否 (僅下一頁/無限滾動)
├── 排序鍵唯一且單調? → 游標分頁 (方案二) **最優**
└── 排序鍵非唯一? → 複合游標 (方案二變體) + 覆蓋索引
queryset[1000000:1000020]、Laravel skip(1000000)->take(20)Rows_examined 遠大於 Rows_sent 的查詢ANALYZE TABLE 確保優化器正確選擇索引本文提到的「分頁狀態儲存」屬於應用層設計:
LRANGE核心原則: 將「定位」與「取數」分離,利用索引有序性避免全掃描 — 這是所有關聯式資料庫分頁優化的通用邏輯。