主键游标分页是最高效稳定的千万级数据分页方式,它通过记录上一页末尾主键值作为下一页查询边界(如 WHERE id > 12345),避免 OFFSET 深分页导致的全表扫描性能坍塌。

千万级数据分页,用 主键游标分页(也叫“游标分页”或“keyset分页”)是最高效、最稳定的方式,它不依赖 OFFSET,避免深分页性能坍塌。
为什么不能用 OFFSET LIMIT?
当偏移量很大(比如 OFFSET 1000000),MySQL/PostgreSQL 仍需扫描前100万行才能定位到结果,I/O 和 CPU 开销剧增,响应可能从毫秒级变成秒级甚至超时。这不是索引能完全解决的问题。
主键游标分页绕过这个问题:它不数行,而是“记住上一页最后一条的主键值”,下一页直接从这个值之后查。
核心写法:用主键做边界条件
假设表结构如下,id 是自增主键(有序、唯一、有索引):
第1页(取前20条):
SELECT * FROM orders ORDER BY id ASC LIMIT 20;第2页(已知第1页最后一条 id = 12345):
关键点:
- 必须有确定排序字段(通常是主键,或带索引的唯一时间字段如
created_at, id组合) - WHERE 条件严格大于(或小于)上一页末尾值,不能用 >=,否则会重复或漏数据
- ORDER BY 字段必须和 WHERE 中用于游标的字段一致,且方向一致(都 ASC 或都 DESC)
- 前端需把末尾主键值(如
12345)作为参数传给下一页请求,而不是页码
处理复合排序与非连续主键
如果主键不是自增(如 UUID)、或需要按时间倒序展示(最新在前),需用多字段游标:
按 created_at DESC, id DESC 分页,第1页最后一条是 created_at='2024-05-01 10:20:30', id='abc123',则第2页写法为:
WHERE (created_at, id) < ('2024-05-01 10:20:30', 'abc123')
ORDER BY created_at DESC, id DESC
LIMIT 20;
注意:
• PostgreSQL 支持元组比较 (a,b) ,MySQL 8.0+ 也支持;<br>• 低版本 MySQL 可改写为:<code>created_at ;<br>• 复合条件务必建联合索引:<code>INDEX(created_at, id)。
注意事项与避坑点
- 禁止跳页:游标分页只支持“下一页/上一页”,不支持直接跳转到第100页(因为没有中间游标值)。如需跳页,可先用游标快速定位到近似位置,再微调
- 数据实时变动要小心:分页过程中若新记录插入到当前游标范围内(如按时间倒序,新订单不断产生),会导致“幻读”——某条记录可能被跳过或重复出现。业务上可接受轻微偏差,或加版本号/时间戳快照隔离
- 主键必须有索引且高选择性:如果用非主键字段(如 status)做游标,性能会退化成范围扫描,失去意义
-
前端需缓存并传递游标值:API 返回应包含下一页游标(如
"next_cursor": "12345"),而不是"page": 2










