深分页查询越来越慢如何优化 LIMIT OFFSET订单列表第一页打开很快翻到第 5000 页却要好几秒。SQL 看起来只取了 20 条LIMIT 100000, 20里的 20 也很小为什么还会慢因为数据库并不是直接跳到第 100000 条它要先数过去。1.LIMIT 100000, 20到底做了什么很多人把LIMIT 100000, 20理解成“从第 100000 条开始取 20 条”。这个理解在结果上没错但执行过程不是瞬移。数据库会先按照ORDER BY的顺序找到符合条件的行然后跳过前 100000 行最后把接下来的 20 行返回。被跳过的行并不是免费消失的它们仍然要经过索引扫描、条件判断很多时候还要回表读取整行数据。SELECT*FROMordersORDERBYidLIMIT100000,20;如果id是主键数据库可以沿着主键顺序扫描。但即使如此它也要走过前 100020 个索引项才能拿到最后那 20 行。页码越深走过的路越长。如果排序字段是普通索引查询又要SELECT *情况会更重数据库先在二级索引里找到一批主键再逐个回表取整行。深分页时这种随机读取会被放大很多倍。图 1OFFSET 要先扫过并丢弃前面十万行游标从上一页的位置继续只读需要的 20 行。2. 为什么第一页快第五百页慢第一页的OFFSET是 0数据库找到开头就能返回成本很小。第二页OFFSET 20只多扫 20 行。到第 5000 页OFFSET变成了 100000扫描量也跟着涨到了十万级。这就是深分页的核心成本大致和OFFSET成正比而不是和LIMIT的大小成正比。第 1 页 OFFSET 0 扫描约 20 行 第 2 页 OFFSET 20 扫描约 40 行 第 5000 页 OFFSET 100000 扫描约 100020 行如果表再大一点或者每行数据比较宽回表次数增加差距会更明显。再加上ORDER BY需要排序、COUNT(*)需要统计总数一个列表接口很容易从几十毫秒变成几秒。3. 游标分页从上一页的位置继续如果页面是“按时间倒序、不断往下翻”的形态最适合的不是OFFSET而是游标分页。不要告诉数据库“跳过前 100000 行”而是告诉它“从上一页最后一条记录之后继续”SELECTid,user_id,amount,created_atFROMordersWHEREid1000000ORDERBYidLIMIT20;这里假设上一页最后一条记录的id是1000000。数据库可以直接从id 1000000的位置开始沿着主键索引取 20 条不需要数前面有多少行。如果排序不是主键而是时间时间又可能重复就要把“时间 唯一键”一起作为游标SELECTid,user_id,amount,created_atFROMordersWHERE(created_at,id)(2026-10-01 12:00:00,1000)ORDERBYcreated_atDESC,idDESCLIMIT20;这里的(created_at, id)是一个组合位置。时间相同的记录再用id区分下一页就不会漏掉或重复。游标分页的代价是不能随机跳到第 5000 页。它适合“下一页”“加载更多”这种连续浏览不适合后台管理系统里“直接跳到最后一页”的需求。4. 延迟关联先拿 ID再回表如果产品确实需要OFFSET分页又不想让每次深翻都回表十万次可以用延迟关联。思路是先在索引里只查本页需要的 20 个主键再用这 20 个主键去取完整数据。SELECTo.*FROMorders oJOIN(SELECTidFROMordersORDERBYidLIMIT100000,20)tONt.ido.idORDERBYo.id;内层查询只取id如果id上有索引它可以只扫描索引不需要为前十万行逐行回表。外层再拿这 20 个id去关联完整数据回表次数就从十万级降到了 20 次。但要说清楚延迟关联减少的是回表成本不是扫描成本。内层仍然要跳过前十万个索引项。对于特别深的页码它能把几秒降到几百毫秒但不一定能像游标分页那样稳定。如果内层查询用的是覆盖索引效果会更明显。比如按created_at排序可以建立(created_at, id)索引让内层只在这个索引里完成排序和取 ID。5. 还有几种降低深分页成本的办法如果业务允许可以直接限制最大页码。搜索引擎通常只展示前几十页不会让用户翻到第一百万条。后台管理系统也可以改成“按条件筛选”而不是无限翻页。对于数据量特别大的列表可以预计算分页边界。比如按时间分区先定位到某个月再在小区间内分页。这样OFFSET始终在一个很小的范围内成本就可控了。如果列表需要复杂的搜索、排序和高亮继续在 MySQL 上做深分页往往不划算。把数据同步到 Elasticsearch 这类搜索引擎用search_after做游标分页通常更合适。另外列表页的总数统计也要注意。SELECT COUNT(*)在千万级表上可能比列表查询还慢。可以缓存总数、使用近似统计或者只显示“还有更多”不必每次请求都精确统计。6.OFFSET分页还有一个一致性问题深分页不只是慢还可能“翻着翻着就乱了”。假设你正在看第 2 页这时有人插入了一条排在前面的新订单。原本第 2 页的第一条会被挤到第 3 页你翻到下一页时可能又看到它一次。反过来如果前面的记录被删除也可能有一条记录被你直接跳过。游标分页基于“上一页最后一条的位置”对插入和删除没那么敏感。它更适合不断有新数据进入的列表。OFFSET分页则更像“按当前排名取第 N 名”数据一变排名就会跟着变。总结LIMIT 100000, 20慢不是那 20 条数据难取而是数据库要先扫过并丢弃前面的十万条。游标分页用“上一页最后一条的位置”代替OFFSET适合连续浏览延迟关联先用覆盖索引取 ID再回表取 20 条能减少深分页的回表成本。选择哪种方式要看页面是否允许随机跳页、排序字段是否稳定以及数据会不会在翻页过程中变化。参考资料MySQL Reference Manual: LIMIT OptimizationMySQL Reference Manual: Range Optimization