OFFSET FETCH 越翻越慢是因为OFFSET必须顺序扫描并跳过前N行,无法直接定位,导致大数据量时性能骤降;仅适用于前端下拉加载前几页或极小分页场景。

为什么 OFFSET FETCH 越翻越慢?
因为 OFFSET 会强制数据库跳过前面所有行,哪怕你只要第 10001 条,它也得先扫描、计数前 10000 条——数据量一上十万,OFFSET 100000 ROWS FETCH NEXT 20 ROWS ONLY 就明显卡顿。
这不是 SQL Server 或 PostgreSQL 的 bug,是语义决定的:OFFSET 不知道“跳到哪”,只能顺序扫。索引再好,也救不了这种全表位移式跳转。
- 适用场景:仅适合前端「下拉加载前几页」,或后台任务中分页量极小(
- 不适用场景:后台导出、报表翻页、用户手动输入大页码(如 page=5000)
- 性能影响:每多跳 10 倍行数,耗时几乎线性增长;配合
ORDER BY非索引字段时更糟
用游标分页(Keyset Pagination)绕过 OFFSET
核心思路:不用“第 N 页”,改用“从某条记录之后取 20 条”。依赖排序字段(如 id 或 created_at)必须有唯一、非空、有索引的组合。
比如按 id 降序分页,上一页最后一条是 id = 12345,那下一页就是:
SELECT TOP 20 * FROM orders WHERE id
- 必须确保
ORDER BY字段有索引,且 WHERE 条件能利用该索引(例如WHERE id 是索引范围扫描) - 如果排序依据是复合字段(如
status, created_at),WHERE 条件要写成WHERE (status, created_at) —— 注意括号和比较逻辑 - 不能跳页,只能“下一页/上一页”;用户点 page=100 时,得老老实实从头翻 99 次(所以前端禁掉页码输入)
ROW_NUMBER() + 覆盖索引能提速,但别滥用
ROW_NUMBER() 本身不解决性能问题,它只是把排序+编号一起做。真正起作用的是:你是否让这个编号过程只访问索引,而不回表。
例如这张表:
CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id INT, status VARCHAR(20), created_at DATETIME );
如果常按 user_id + created_at 分页查订单,建覆盖索引:
CREATE INDEX IX_orders_user_created ON orders (user_id, created_at DESC) INCLUDE (id, status);
- 这样
SELECT ROW_NUMBER() OVER (ORDER BY user_id, created_at DESC) AS rn, ...可以完全走索引,避免排序溢出 tempdb - 但一旦加上
WHERE status = 'paid',而status不在索引最左列,这个索引就大概率失效——ROW_NUMBER()还是得全扫 - SQL Server 中,
ROW_NUMBER()窗口函数无法被索引直接优化;它依赖底层数据扫描是否高效,不是“加了索引就快”
什么时候该放弃分页,改用搜索或归档?
当用户真实需求其实是“找某条记录”,却被迫用翻页实现时,分页就成了技术债。
- 典型信号:日志表分页查
WHERE level = 'ERROR',但错误只占 0.1%,翻到第 500 页才出现第一条——这本质是低效过滤,不是分页问题 - 解决方案优先级:加
level字段的筛选索引 > 改用全文检索(如 PostgreSQL 的tsvector) > 导出后本地查 - 归档冷数据:把一年前的
orders移到orders_archive,主表变小,OFFSET自然不卡——比调优ROW_NUMBER()更治本
分页优化最容易被忽略的一点:它从来不是纯技术问题。先确认用户到底要不要翻到第 1000 页,还是只是接口没提供搜索能力,才逼着人一页页点下去。










