分页查询是后端开发中最常见的场景之一,但同时也是最容易遇到性能问题的地方。通过一个常见的 SQL 示例,深入剖析传统分页 (OFFSET/LIMIT) 的性能瓶颈,并介绍更高效的“Keyset 分页(游标分页)”方案。
一、传统分页方式:OFFSET + LIMIT
我们先看一个典型的分页查询:
SELECT *
FROM operation
WHERE type = 'SQLStats'
AND name = 'SlowLog'
ORDER BY create_time
LIMIT 1000, 10;
DBA 通常会建议在 (type, name, create_time) 字段上建立联合索引,以便 WHERE 条件与 ORDER BY 都能利用索引。这样,在页码较小时性能确实不错。
然而,当分页偏移量增大,例如:
LIMIT 1000000, 10;
性能问题就会凸显。即使有索引,数据库仍需从头扫描并跳过 100 万行数据,因为它不知道第 1,000,000 行从何处开始。这种操作的复杂度是 O(n),查询时间会随着数据量线性增长。
二、性能优化思路:Keyset 分页(游标分页)
在数据浏览或批量导出等场景中,我们往往可以利用上一页的“锚点”记录来优化查询。将分页 SQL 改写为:
SELECT *
FROM operation
WHERE type = 'SQLStats'
AND name = 'SlowLog'
AND create_time > '2017-03-16 14:00:00' -- 上一页的最大 create_time
ORDER BY create_time
LIMIT 10;
这种写法被称为 Keyset Pagination 或 Seek Pagination。它不依赖于 OFFSET,而是通过索引直接定位到“上一页的最后一条记录”之后继续查询。
优点:
- 查询性能稳定,不随页码增长而变慢;
- 可以充分利用索引做范围扫描;
- 避免 OFFSET 带来的“跳过成本”。
三、Keyset 分页的实现细节
1. 处理重复排序键
若 create_time 存在重复值,可能导致数据漏查或重复。推荐做法是使用复合排序键:
ORDER BY create_time, id
AND (create_time, id) > (?, ?)
索引设计可为:(type, name, create_time, id)。
2. 支持前后翻页
- 下一页:使用
>(升序)或<(降序)条件; - 上一页:可保存上一页的游标值实现回退。
3. 覆盖索引
若能通过覆盖索引(所有字段都包含在索引中)返回结果,可进一步减少磁盘 I/O。
4. 边界处理
第一页没有锚点条件,最后一页不足 10 条时注意结果集的完整性。
四、何时仍使用 OFFSET/LIMIT?
- 数据量较小,分页偏移量不大;
- 临时查询或调试需求。
但在大数据场景中,Keyset 分页几乎总是更优方案。
五、总结
| 比较项 | OFFSET/LIMIT 分页 | Keyset 分页 |
|---|---|---|
| 性能随页数增长 | 线性变慢 | 基本恒定 |
| 可随机跳页 | 支持 | 不支持(顺序访问) |
| 稳定性 | 受数据变动影响 | 稳定 |
| 实现复杂度 | 简单 | 略复杂 |
一句话总结:
小数据量可用 OFFSET,大数据分页请用 Keyset。利用索引 + 锚点查询。







Comments NOTHING