资讯动态

MySQL 大数据量分页查询优化:深分页为什么慢,有没有办法?

发布时间:2026/9/30 17:22:46 来源:尧图企业网站定制
hello 我是逆境一张订单表有几千万条数据同样查询 20 条记录前几页很快越往后却越慢。原因往往在于返回的数据虽然只有 20 条但数据库为了找到这 20 条可能已经处理了上百万条记录。本文以 MySQL 的 InnoDB 引擎为例讲清楚深分页为什么慢、常见优化方式以及面试时应该如何回答。先从一条常见的分页 SQL 看起。假设订单表orders包含订单主键id、订单状态status、创建时间created_at、金额amount等字段。查询已支付订单按创建时间和主键升序排列SELECTid,amount,created_atFROMordersWHEREstatus1ORDERBYcreated_atASC,idASCLIMIT1000000,20;LIMIT 1000000, 20的含义是跳过前 100 万条符合条件的记录再返回 20 条。在可以沿索引顺序读取、且符合条件的记录足够多时可以理解为依次取得前1000020个符合条件的条目跳过前1000000个返回最后20个。MySQL 的 LIMIT/OFFSET 执行器也体现了这种读取并跳过的方式。:chatgpt-content-reference{index“0”}如果还需要过滤更多数据或者额外排序实际工作量可能更大。这就是深分页的核心问题OFFSET 越大为了跳过前面的记录数据库做的无用工作就越多。有人可能会问有索引为什么不能直接跳到第 100 万条因为普通 B 树索引擅长的是按键值定位例如查找id 500的起点它并没有为任意查询结果维护一个“第几条”的位置目录。因此“找到某个 ID”和“跳过某个数量的记录”是两回事。优化的第一步是让过滤和排序用上合适的索引。针对前面的查询可以考虑建立联合索引CREATEINDEXidx_status_time_idONorders(status,created_at,id);这个顺序对应查询的处理需求通过status 1限定订单状态。在状态相同的范围内按照created_at、id读取。索引顺序与ORDER BY一致有机会避免额外排序。优化器是否实际选择这条索引还需要看执行计划。:chatgpt-content-reference{index“1”}这里额外按照id排序是因为多笔订单可能具有相同的创建时间。加入唯一主键后可以让排序结果确定只按时间排序相同时间的记录之间没有确定顺序。:chatgpt-content-reference{index“2”}不过加索引之后前面 100 万条记录仍然需要被跳过。索引可以降低过滤和排序成本但不能自动消除 OFFSET 的扫描成本。另外列表只查询需要展示的字段避免无意义的SELECT *。但少查字段不代表一定不用回表本例中的amount不在联合索引里仍需要读取对应的数据行。如果必须按页码查询可以使用“覆盖索引 延迟关联”。先理解两个概念回表先从二级索引找到主键再通过主键到聚簇索引读取需要的数据。InnoDB 的二级索引记录包含主键值。:chatgpt-content-reference{index“3”}覆盖索引查询需要的列都能从同一条索引中取得不必为了补齐其他列再读取数据行。:chatgpt-content-reference{index“4”}如果原查询沿着非覆盖的二级索引读取大量候选记录可能带来大量回表。最终虽然只返回 20 条前面的读取工作却已经发生了。根据这个原理可以推导出一种优化方式先利用覆盖索引找到这一页的 20 个 ID再通过主键读取这 20 条订单的详情。SQL 如下SELECTo.id,o.amount,o.created_atFROMordersASoJOIN(-- 先在索引中完成分页取出这一页的主键和排序字段SELECTid,created_atFROMordersWHEREstatus1ORDERBYcreated_atASC,idASCLIMIT1000000,20)ASpageONo.idpage.id-- 外层也要明确排序ORDERBYpage.created_atASC,page.idASC;这就是延迟关联。内层查询需要的status、created_at、id都在索引中可以先确定这一页的 ID外层再读取对应订单的金额等字段。在上述执行计划成立时优化点是把读取详情的工作推迟到选好这一页之后让最终这 20 条记录再去读取详情。但它有一个重要限制内层仍然存在LIMIT 1000000, 20仍需遍历大量索引条目。所以面试时不能说“延迟关联把扫描量从 100 万条降到了 20 条”。准确的说法是延迟关联减少了无用的回表工作OFFSET 带来的扫描成本仍然存在。如果原查询本来就被索引覆盖额外增加一次关联通常也没有这个收益。如果业务允许连续翻页游标分页可以进一步减少扫描。普通分页记录的是“跳过多少条”游标分页记录的是“上次查到了哪里”。例如业务本来就按主键id升序排序上一页最后一条记录的 ID 是500SELECTid,amountFROMordersWHEREid500-- 上一页实际返回的最后一个 IDORDERBYidASCLIMIT20;数据库可以根据id定位起点再向后读取无须从头跳过此前所有记录。主键不需要连续。如果500后面是503就从503开始读取。但是不能把 OFFSET 直接当成 ID。删除记录、筛选条件等都可能让记录位置与 ID 失去对应关系。回到前面的订单查询我们按created_at、id排序就需要同时保存这两个值。假设上一页最后一条订单是created_at 2026-09-01 10:00:00.000000 id 12345下一页可以这样查询SELECTid,amount,created_atFROMordersWHEREstatus1AND(-- 时间更晚排在上一页最后一条之后created_at2026-09-01 10:00:00.000000OR(-- 时间相同继续比较主键created_at2026-09-01 10:00:00.000000ANDid12345))ORDERBYcreated_atASC,idASCLIMIT20;这个条件就是把“排在上一条之后”翻译成 SQL先比较时间时间相同再比较 ID。如果只写created_at 上次时间与上一页最后一条时间相同、但尚未展示的订单就会被漏掉。示例假设时间字段非空。保存游标时应保留时间精度并保持相同的筛选条件。如果改成两个字段都降序排列下一页的两个比较符也相应改成。在索引能够支持范围定位的情况下读取工作通常接近当前一页所需的数据量。不过仍存在索引定位、过滤和读取详情的成本不能说“任何情况下都只扫描 20 条”。游标分页还有两个限制不能仅凭页码直接定位。用户要求跳到第 5000 页时我们并不知道这一页的起点。不会自动固定数据快照。并发增删或者排序、筛选字段发生变化时多次请求看到的数据集仍可能变化。实际选择方案时要先看业务如何翻页。业务需求常用方案主要限制普通后台列表页数不深合适的索引 LIMIT深页的跳过成本上升需要按页码跳转并读取详情覆盖索引分页 延迟关联仍有 OFFSET 扫描成本下一页、加载更多、分批读取游标分页不能直接根据任意页码定位对于很深的随机跳页可以先引导用户缩小时间范围、增加筛选条件或者限制可翻页的深度。如果确实需要高效随机跳转再考虑预计算页锚点或固定结果集但这些方案会增加维护和一致性成本。分页接口还有一个容易忽略的瓶颈查询总条数。很多分页接口实际执行了两条 SQL一条查当前页一条查总数。SELECTCOUNT(*)FROMordersWHEREstatus1;即使当前页已经查得很快统计总数仍可能很慢。InnoDB 不会直接保存一个对所有事务都适用的精确行数COUNT(*)需要统计当前事务可见的记录。:chatgpt-content-reference{index“5”}如果业务只需要知道“还有没有下一页”可以查询pageSize 1条。例如每页展示 20 条就查询 21 条查到 21 条返回前 20 条并设置hasNext true。不足 21 条则没有下一页。使用游标分页时下次游标取实际返回的第 20 条不能取用于判断的第 21 条否则会漏掉它。如果确实需要总数可以根据允许的数据延迟考虑缓存或异步统计。要求实时、精确就需要承担相应查询成本。优化是否有效最终要通过执行计划和实际耗时判断。先用EXPLAIN查看key实际选择了哪条索引。type采用什么访问方式。index可能是全索引扫描不能直接理解成“很快”。rows预估需要检查的行数不是实际测量值。ExtraUsing index表示覆盖索引Using index condition表示索引条件下推两者不同。:chatgpt-content-reference{index“6”}Using filesort表示需要额外排序但不一定使用磁盘也可能在内存中完成。:chatgpt-content-reference{index“7”}还可以使用EXPLAIN ANALYZE查看实际执行信息包括耗时、返回行数和循环次数。注意它会真正执行 SQL。:chatgpt-content-reference{index“8”}验证时应关注读取阶段处理了多少记录、是否减少了大量详情读取以及整体耗时是否改善。只看到最终返回 20 条并不能证明优化成功。总结大数据量分页主要关注深分页问题。LIMIT offset, size的 offset 很大时数据库仍需要处理并跳过前面的记录如果还有大量回表或额外排序成本会进一步增加。我会先用 EXPLAIN 检查执行计划根据过滤条件和排序条件设计联合索引并且只查询需要的字段。如果必须支持按页码跳转可以先用覆盖索引查出这一页的 ID再关联查询详情减少无用的回表但它仍有 OFFSET 扫描成本。如果业务允许连续翻页我会使用游标分页保存上一页最后一条记录的排序值下一次从该位置做范围查询。排序字段不唯一时需要加主键保证顺序确定。最后还要检查 COUNT 总数查询并结合实际执行信息验证收益。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑