深分页变慢是因为数据库必须逐行扫描并丢弃前n行,无法跳过计数过程;即使有索引,重复排序值或大offset仍导致全索引扫描、回表激增和i/o雪崩,性能随offset线性下降。

深分页(比如 OFFSET 100000)在任何主流数据库里都会变慢,这不是配置或索引能根治的问题——它本质是数据库必须读、计数、丢弃前 N 行。真要高效,得绕开 OFFSET。
为什么 LIMIT ... OFFSET 在大数据量下越来越慢
数据库执行 LIMIT 10 OFFSET 100000 时,并不会“跳到第 100001 行”,而是从头扫描、逐行计数,直到凑够 100000 行才开始取数据。哪怕你只想要 1 条,它也得把前面 10 万行全读出来。
- 索引能加速排序和定位,但无法跳过“计数”动作;
- 当
ORDER BY字段有重复值(如多个记录created_at相同),优化器常放弃索引跳转,退化为全索引扫描; -
OFFSET每增加一倍,耗时几乎线性增长,实测从几毫秒升到 2 秒以上很常见; - MySQL 8.0 和 PostgreSQL 13+ 都没解决这个底层机制问题,只是微调了执行计划选择。
键集分页(Keyset Pagination)怎么写才不漏数据
核心是用上一页最后一条记录的完整排序键作为下一页的查询起点,彻底消除 OFFSET。但它要求排序字段组合具备确定性——不能只靠时间戳,必须补主键或唯一字段。
- 错误写法:
WHERE created_at —— 多条记录时间相同就会漏或重; - 正确写法:先确保
ORDER BY created_at DESC, id DESC,再用WHERE (created_at, id) (PostgreSQL); - MySQL 不支持带
NULL的行构造器,得拆解:WHERE created_at ; - 必须建联合索引
INDEX (created_at, id),顺序与ORDER BY完全一致,否则索引失效。
哪些场景还不得不硬扛 OFFSET
跳页(比如直接输入页码 87)、后台导出、管理后台“总页数”展示——这些需求天然依赖全局偏移量,键集分页无法满足。
- 限制最大可访问页数(如
OFFSET不超过 10000),配合提示“数据量过大,建议按条件筛选”; - 用子查询先定位起始 ID:
SELECT * FROM orders WHERE id > (SELECT id FROM orders ORDER BY id LIMIT 9999, 1) ORDER BY id LIMIT 10,比直写OFFSET快不少; - SQL Server 或 Oracle 可用
ROW_NUMBER()窗口函数,但要注意它仍需扫描全部候选行,只是避免了多次排序; - 如果业务允许,把“总页数”改成“下一页是否存在”,用键集分页 +
FETCH NEXT 11 ROWS判断,更可靠。
最易被忽略的一点:前端传来的“上一页末尾值”必须是完整排序键(比如 (created_at, id) 两个字段),不能只记一个时间戳就以为够了。少传一个字段,翻页就断层。










