OFFSET LIMIT 深分页变慢是因为数据库必须扫描并跳过前N行,无法利用索引直接定位,导致I/O和CPU开销剧增;游标分页通过记录上一页末尾值做范围查询,可完全走索引,但要求严格有序无重复字段、前后端协同及安全编码游标。

为什么 OFFSET LIMIT 在深分页时变慢
数据库用 OFFSET 100000 LIMIT 20 查第 5001 页时,MySQL 或 PostgreSQL 仍要扫描前 10 万行才能跳到目标位置。索引无法跳过中间数据,I/O 和 CPU 开销陡增,响应可能从毫秒级升至秒级。
- 深分页本质是「跳过大量已排序结果」,而 B+ 树索引不支持 O(1) 跳转
- 即使有复合索引,
OFFSET仍需逐行计数,无法利用索引直接定位 - 分页参数被用户篡改(如手动改 URL 中的
page=9999)时,更易触发慢查询
游标分页的核心:用上一页最后一条记录的排序字段值做条件
不是“我要第 N 页”,而是“给我比上一页最后一条记录更新/更大的下一批”。这把分页变成范围查询,能完全走索引。
- 必须依赖一个严格有序且无重复的排序字段,首选
id(自增主键)或created_at, id组合 - 查询语句形如:
WHERE created_at > '2024-05-01 10:00:00' OR (created_at = '2024-05-01 10:00:00' AND id > 12345) -
前端不再传
page,而是传上一页返回的cursor(通常是 base64 编码的上一页末条记录关键字段)
PostgreSQL 和 MySQL 游标构造的细微差异
PostgreSQL 支持行构造器语法,写法更简洁:
正序分页(取下一页):
WHERE (created_at, id) > ('2024-05-01 10:00:00', 12345) ORDER BY created_at, id LIMIT 20MySQL 不支持多列元组比较,必须拆成逻辑或:
WHERE created_at > '2024-05-01 10:00:00' OR (created_at = '2024-05-01 10:00:00' AND id > 12345)两者都要求
ORDER BY字段顺序与WHERE条件严格一致,否则可能漏数据或重复如果排序字段允许 NULL,必须显式处理(如
created_at IS NOT NULL),否则游标行为不可靠
游标编码与安全边界容易被忽略的点
游标不是随便拼字符串,它承载的是精确的断点位置:
- 直接暴露
id和时间戳有风险(比如泄露业务增长量),建议用 base64 编码结构化 JSON:{"created_at":"2024-05-01T10:00:00Z","id":12345} -
后端解码后必须校验字段类型和范围,防止注入恶意时间或超大
id - 首次请求没有上一页游标,应设默认起点(如
created_at > '1970-01-01'),但注意时区和精度(微秒 vs 秒) - 游标分页天然不支持“跳转到任意页”,也不支持统计总页数——这点必须在产品设计初期就对齐
游标分页真正难的不是 SQL 写法,而是前后端对「游标即状态」的一致理解,以及所有排序路径都得收敛到同一套不可变字段组合。









