分页查询性能优化:从 LIMIT 到 Keyset 分页

发布于 2025-10-20  9 次阅读


分页查询是后端开发中最常见的场景之一,但同时也是最容易遇到性能问题的地方。通过一个常见的 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 PaginationSeek 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。利用索引 + 锚点查询。