資料表 test 有 30 萬筆,主鍵 id 為自增索引。執行:
SELECT * FROM `test` ORDER BY id ASC LIMIT 208888, 50;
MySQL 必須掃描前 208,888 行再丟棄,僅取後 50 行。實測耗時 40-50 秒。
SELECT * FROM `test`
WHERE id BETWEEN 208838 AND 208888
ORDER BY id ASC;
實測耗時 0.06 秒,快 600 倍。
| 查詢方式 | 執行計劃 | 掃描行數 | 耗時 |
|---|---|---|---|
| LIMIT offset, N | index (全索引掃描) | offset + N | O(offset) |
| WHERE id > last_id LIMIT N | range (索引範圍) | N | O(1) |
| WHERE id BETWEEN ... | range (索引範圍) | N | O(1) |
B+ 樹索引的葉子節點透過雙向鏈表連結,WHERE id > x 可直接定位到葉子節點,沿鏈表向後取 N 行,無需掃描前段資料。
<?php
// 第一頁
$stmt = $pdo->query('SELECT * FROM test ORDER BY id ASC LIMIT 50');
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$lastId = $rows ? end($rows)['id'] : 0;
// 後續頁(前端傳 last_id)
$lastId = (int)($_GET['last_id'] ?? 0);
$stmt = $pdo->prepare('SELECT * FROM test WHERE id > ? ORDER BY id ASC LIMIT 50');
$stmt->execute([$lastId]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 回傳 rows + next_last_id
?>
優點:無 offset、效能恆定、支援無限滾動。缺點:無法直接跳到第 N 頁(需記錄每頁首尾 ID 或接受此限制)。
INDEX (a, b, c) 支援 WHERE a=1、WHERE a=1 AND b=2,不支援 WHERE b=2。type(目標:ref/range/eq_ref)、rows、Extra(Using index/Using where/Using filesort)。SELECT COUNT(*) 在大表極慢,改用估算值、快取總數、或不顯示總頁數(僅「上一頁/下一頁」)。SELECT t.* FROM test t
JOIN (SELECT id FROM test WHERE ... ORDER BY id LIMIT 100000, 20) tmp USING (id);
ANALYZE TABLE 更新統計資訊。Handler_read_rnd_next(全表掃描計數)暴增。pt-query-digest 分析慢查詢日誌。「LIMIT 100000, 20 是效能殺手」——請改用 WHERE id > last_id LIMIT N(游標分頁)或 延遲關聯。配合 EXPLAIN 驗證執行計劃,讓每個查詢都跑在索引上,是 PHP + MySQL 高效能的核心心法。