MySQL 如何解决深度分页问题?
一、面试题在电商后台的订单列表中用户可以按下单时间倒序分页查询。当翻到第 10000 页时SQL 查询明显变慢如何分析和优化二、真实业务场景订单表约有 5000 万条数据后台查询最近订单SELECTid,order_no,user_id,amount,status,created_atFROMordersWHEREstatus1ORDERBYidDESCLIMIT999900,10;LIMIT 999900, 10并不是直接定位到第 999901 条数据而是先扫描、排序并跳过前 999900 条记录再返回 10 条数据。页码越大扫描的数据越多性能越差这就是深度分页问题。三、方案一子查询优化先利用覆盖索引查询出当前页的主键再根据主键回表查询完整数据SELECTid,order_no,user_id,amount,status,created_atFROMordersWHEREidIN(SELECTidFROMordersWHEREstatus1ORDERBYidDESCLIMIT999900,10)ORDERBYidDESC;创建联合索引CREATEINDEXidx_status_idONorders(status,id);子查询只查询id可以尽量利用覆盖索引减少回表次数。适合页码分页、需要跳转到指定页码的场景但深度很大时子查询仍然需要扫描前面的数据。四、方案二基于游标或最大 ID 分页第一次查询SELECTid,order_no,user_id,amount,status,created_atFROMordersWHEREstatus1ORDERBYidDESCLIMIT10;假设本次返回的最小 ID 是985000下一页查询SELECTid,order_no,user_id,amount,status,created_atFROMordersWHEREstatus1ANDid985000ORDERBYidDESCLIMIT10;创建索引CREATEINDEXidx_status_idONorders(status,id);这种方式可以直接从索引位置继续读取不需要扫描并丢弃前面的数据性能基本不受页码影响。但它只适合连续翻页不适合用户直接跳转到第 10000 页。五、排序字段不唯一时的写法如果按照created_at倒序排序多个订单可能拥有相同的创建时间需要增加id作为唯一排序条件SELECTid,order_no,user_id,amount,status,created_atFROMordersWHEREstatus1AND(created_at2026-07-16 10:00:00OR(created_at2026-07-16 10:00:00ANDid985000))ORDERBYcreated_atDESC,idDESCLIMIT10;对应索引CREATEINDEXidx_status_created_idONorders(status,created_at,id);其中created_at和id组成稳定的排序游标避免数据重复或遗漏。六、方案三使用 Elasticsearch如果业务是商品搜索、订单搜索、日志检索等复杂查询可以使用 Elasticsearch。传统分页{from:999900,size:10}深度分页时仍然需要维护大量搜索结果性能会下降。推荐使用search_after{size:10,query:{term:{status:1}},sort:[{created_at:desc},{id:desc}],search_after:[2026-07-16T10:00:00,985000]}search_after需要携带上一页最后一条记录的排序值适合连续翻页。七、方案对比方案优点缺点适用场景LIMIT offset,size写法简单页码越大越慢数据量小子查询减少回表数据深度很大时仍需扫描需要页码跳转游标分页性能稳定不支持随机跳页无限滚动、连续翻页Elasticsearch支持复杂搜索需要维护数据同步搜索、日志、订单检索八、面试总结解决 MySQL 深度分页问题核心不是简单修改LIMIT而是减少数据库需要扫描和丢弃的数据量。实际项目中通常这样选择数据量较小直接使用LIMIT必须支持页码跳转使用子查询和覆盖索引连续翻页或无限滚动使用基于 ID 或时间的游标分页复杂搜索场景使用 Elasticsearch 的search_after排序字段不唯一使用“排序字段 主键”构造稳定游标