-- 第 1 頁,每頁 20 筆
SELECT * FROM product LIMIT 0, 20;
-- 第 N 頁
SELECT * FROM product LIMIT (N-1)*20, 20;
當 OFFSET 很大時(如第 10000 頁),MySQL 需掃描並丟棄前 200,000 筆資料,導致查詢緩慢:
-- 低效:掃描 200,000 行後取 20 行
SELECT * FROM product LIMIT 200000, 20;
| 方案 | 適用場景 | 原理 | 優/缺點 |
|---|---|---|---|
| 子查詢 + 索引覆蓋 | 單表、有主鍵/唯一索引 | 先只查 ID,再回表 | ✅ 大幅減少回表 ❌ 需覆蓋索引 |
| 延遲關聯 | 複雜 JOIN 查詢 | 先分頁查主鍵,再 JOIN 明細 | ✅ 通用性強 ❌ SQL 較複雜 |
| 游標/Keyset 分頁 | 無需跳頁、API 無限滾動 | WHERE id > last_id LIMIT 20 | ✅ 極快、穩定 ❌ 不支援隨機跳頁 |
| 覆蓋索引優化 | 只需索引欄位 | 索引包含所有查詢欄位 | ✅ 避免回表 ❌ 受限於查詢欄位 |
-- 步驟 1:只查主鍵(利用索引快速定位)
SELECT id FROM product
WHERE category_id = 5
ORDER BY created_at DESC
LIMIT 200000, 20;
-- 步驟 2:用 IN 關聯查明細
SELECT * FROM product
WHERE id IN (上述查詢的 20 個 ID)
ORDER BY created_at DESC;
public List<Product> getProducts(int pageNo, int pageSize) {
String sql = """
SELECT p.* FROM product p
INNER JOIN (
SELECT id FROM product
WHERE category_id = ?
ORDER BY created_at DESC
LIMIT ?, ?
) tmp ON p.id = tmp.id
ORDER BY p.created_at DESC
""";
// 參數: categoryId, (pageNo-1)*pageSize, pageSize
return jdbcTemplate.query(sql, new ProductRowMapper(),
categoryId, (pageNo - 1) * pageSize, pageSize);
}
測試環境:1000 萬筆資料、InnoDB、主鍵 id、索引 (category_id, created_at)
| 分頁方式 | 第 1 頁 | 第 50000 頁 |
|---|---|---|
| 直接 LIMIT | 5 ms | 1,200 ms |
| 延遲關聯 | 6 ms | 15 ms |
| Keyset 分頁 | 3 ms | 3 ms |