游标分页能绕过limit offset扫描开销,因为它用上一页末尾排序字段值(如created_at、id)构造where条件直接定位起始位置,仅扫描limit行,时间复杂度从o(offset+size)降至o(size);需有序唯一排序字段、匹配索引及严格一致的order by与where逻辑。

游标分页为什么能绕过 LIMIT offset 的扫描开销
因为 MySQL 执行 LIMIT 1000000, 10 时,并不是“跳到第 1000001 行”,而是从索引头开始顺序读取 1000010 条记录,丢弃前 1000000 条,只返回后 10 条。扫描行数 = offset + size,和页码强相关。
游标分页用「上一页末尾的排序字段值 + 唯一标识」构造 WHERE 条件,让 MySQL 直接定位到下一页起始位置,实际只扫描 size 行(比如 10 行),时间复杂度从 O(offset + size) 降到 O(size)。
- 必须依赖有序且稳定的排序字段(如
created_at、id) - 排序字段不能有大量重复值,否则需要补充唯一字段(如
id)防错序 - 无法支持跳转任意页(比如直接输入“第 5000 页”),只适合无限滚动或连续翻页
怎么写游标分页的 SQL(含升序/降序双场景)
核心是把“第 N 页”转换成“比上一页最后一条记录更大/更小的前 N 条”。关键在于 WHERE 条件构造和 ORDER BY 保持一致。
假设表 orders 按 created_at DESC, id DESC 排序(最新订单在前),且 id 是主键:
- 第一页:
SELECT id, user_id, total_amount, created_at FROM orders ORDER BY created_at DESC, id DESC LIMIT 10; - 第二页(假设上一页最后一条是
created_at = '2024-06-01 10:23:45',id = 987654):SELECT id, user_id, total_amount, created_at FROM orders WHERE (created_at - 升序场景(如按
id ASC):条件改为WHERE id > :last_id,ORDER BY 保持id ASC
注意:WHERE 中的括号和逻辑运算符顺序不能错,否则可能漏数据或重复;:last_id 和 :last_time 必须来自上一页查询结果的最后一行,不能用前端传入的任意值。
哪些索引能让游标分页真正生效
没有对应索引,游标分页只是“看起来快”,实际仍会触发全表扫描或 filesort。
- 必须为排序字段建索引,且顺序与 ORDER BY 一致:比如
ORDER BY created_at DESC, id DESC→ 索引应为INDEX idx_created_id (created_at DESC, id DESC) - 如果 WHERE 还带其他条件(如
user_id = ?),要把过滤字段放在索引最左:例如INDEX idx_user_created_id (user_id, created_at DESC, id DESC) - 避免
SELECT *:只查需要的字段,若都在索引中(覆盖索引),可彻底避免回表 - 不要用函数包裹排序字段:比如
ORDER BY DATE(created_at)会让索引失效
用 EXPLAIN 验证:理想情况是 type = range 或 ref,Extra 中不含 Using filesort 或 Using temporary。
容易被忽略的边界问题和坑
游标分页看似简单,但线上出问题往往卡在细节。
- 时间字段精度问题:
created_at如果是DATETIME(秒级),并发插入可能产生相同值,必须补id;如果是DATETIME(6)(微秒级),仍建议加id防万一 - NULL 值干扰:如果排序字段允许 NULL,
ORDER BY created_at DESC会把 NULL 排在最前(MySQL 默认),但WHERE created_at 不会匹配 NULL,导致首条数据丢失 - 数据变更风险:翻页过程中,新插入或删除记录可能造成“漏行”或“重复行”(幻读),业务需接受最终一致性,或加应用层锁(不推荐)
- 前端传参校验缺失:如果用户篡改游标值(如把
last_id改成极小值),可能返回大量数据甚至拖垮 DB,后端必须校验游标格式及合理性
真正的难点不在写第一条游标 SQL,而在处理排序字段重复、时区差异、索引失效、以及如何向产品解释“为什么不能跳页”。这些地方不提前对齐,上线后反而比 LIMIT offset 更难排查。











