SELECT * FROM t WHERE ... ORDER BY x LIMIT 100000, 20 —— 當 OFFSET 達到十萬、百萬級,MySQL 必須掃描並捨棄前 10 萬行,I/O 與 CPU 雙重暴擊。隨著資料增長、用戶翻閱末頁,系統將陷入「磁碟狂轉、回應超時」的惡性循環。
-- 先只取主鍵 ID,再回表
SELECT a.* FROM t a
JOIN (SELECT id FROM t WHERE ... ORDER BY x LIMIT 100000, 20) b
USING (id);
-- 首頁
SELECT * FROM t WHERE ... ORDER BY x LIMIT 20;
-- 記住最後一行的排序鍵值
-- 次頁
SELECT * FROM t WHERE ... AND x > 'last_x_value' ORDER BY x LIMIT 20;
針對「複雜條件 + 需支援任意跳頁 + 深分頁」場景,提出兩階段查詢 + 會話級 ID 緩存:
SELECT id FROM t WHERE $condition ORDER BY $field LIMIT $max_pages * $page_size;
# PHP 陣列範例
$ids = []; // 從步驟1取得的所有 ID
$page_ids = array_chunk($ids, $page_size); // 切成 $page_ids[0]..[$max_pages-1]
# 存入 Session(檔案/Redis/Memcached)
$_SESSION['search_ids'][$search_token] = [
'ids' => $page_ids,
'time' => time(),
'cond' => $condition_hash // 防止不同查詢互串
];
$ids_str = implode(',', $_SESSION['search_ids'][$token]['ids'][$page_no]);
$sql = "SELECT * FROM t WHERE id IN ($ids_str) ORDER BY FIELD(id, $ids_str);";
# ORDER BY FIELD 保持原序,避免 IN 亂序
gc_maxlifetime 或自訂掃描器清理過期 token。search_cache(token, page_no, ids_csv, created_at),定期 DELETE WHERE created_at < NOW() - INTERVAL 1 HOUR。CREATE TEMPORARY TABLE tmp_search_ids (
token VARCHAR(64),
page_no INT,
ids_csv TEXT,
PRIMARY KEY(token, page_no)
) ENGINE=MEMORY;
INSERT INTO tmp_search_ids
SELECT $token, FLOOR((@rownum:=@rownum+1-1)/$page_size), GROUP_CONCAT(id)
FROM (SELECT id FROM t WHERE $condition ORDER BY $field LIMIT $max_pages*$page_size) t1
JOIN (SELECT @rownum:=0) t2
GROUP BY FLOOR((@rownum-1)/$page_size);
| 場景 | 推薦方案 |
|---|---|
| 簡單條件、淺分頁、需跳頁 | 延遲關聯 / 覆蓋索引 |
| 無限滾動、無需跳頁 | Keyset/游標分頁 |
| 複雜條件、深分頁、需跳頁 | ID 緩存機制(本文方案) |
| 超大資料量(億級)、高併發搜尋 | Elasticsearch / OpenSearch 全文檢索引擎 |