☰
LIMIT 100000, 20 为什么还是慢:深分页三件套,游标 / 延迟关联 / 覆盖索引
2026/11/3 10:44:43 网站建设 项目流程

LIMIT 100000, 20 为什么还是慢:深分页三件套,游标 / 延迟关联 / 覆盖索引

“第 1 页 20ms,第 5000 页 3 秒”,列表接口越翻越慢,这种体验在后台管理系统里几乎人人撞过。LIMIT 100000, 20看着只取 20 条,MySQL 却要先把前 100000 行全扫出来再丢掉。这篇把深分页的代价算清楚,再给三个能落地的优化。

先看现象:EXPLAIN 里 rows 从 20 涨到 10 万

招聘系统的简历列表,翻页翻到后面:

SELECT*FROMresumeWHEREcity_id=440700ORDERBYupdate_timeDESCLIMIT100000,20;

EXPLAIN 出来type=ALL,rows 显示 100020——MySQL 把满足条件的行一路扫到第 100020 行,然后只留下最后 20 条返回。扫描的行数和 offset 成正比,这就是深分页慢的根源。offset 越大,扫的行越多,跟"只取 20 条"的直觉完全相反。

优化一:游标分页,用 id 或时间戳当游标

最常用的解法是把"翻页"改成"往后取":记住上一页最后一条的位置,用 WHERE 条件定位而不是 LIMIT 跳过。简历列表用update_time + id做游标:

-- 第一页:id = 0 哨兵SELECTid,name,phone,update_timeFROMresumeWHEREcity_id=440700AND(update_time,id)<(:lastTime,:lastId)ORDERBYupdate_timeDESC,idDESCLIMIT20;-- 下一页:把上一页最后一条的 update_time / id 传进来

要点:排序字段必须有唯一性(时间可能重复,所以拼上 id),索引要建(city_id, update_time, id)复合索引,让 MySQL 走索引直接定位游标位置,而不是全表扫。游标分页的代价是不能随机跳页(只能上一页下一页),但对"瀑布流加载""订单列表"这类场景完全够用。

优化二:延迟关联,先查 id 再回表

业务上确实要跳页、要LIMIT 50000, 20,那也别让大表全字段参与排序。先在索引里把 id 捞出来,再 join 回原表取完整行,回表只发生在最终 20 条上:

SELECTr.*FROMresume rINNERJOIN(SELECTidFROMresumeWHEREcity_id=440700ORDERBYupdate_timeDESCLIMIT50000,20)tONr.id=t.id;

子查询里只 select id,走覆盖索引(city_id, update_time, id),MySQL 扫 50020 个 id 的代价远小于扫 50020 行完整记录(行里有大文本字段时差距是数量级的)。实测简历表(含自荐信大字段)从 2.8s 降到 0.4s。

优化三:覆盖索引,能不出表就不出表

如果列表页只需要展示几个字段(id、姓名、岗位、更新时间),干脆把展示列全部塞进索引,连回表都省了:

CREATEINDEXidx_city_time_coverONresume(city_id,update_timeDESC,id,name,position);

注意:别把大字段(自荐信、简历附件 URL)塞进索引,索引会膨胀,写入变慢,得不偿失。覆盖索引只服务"高频、字段少"的列表查询。

踩坑实录:游标分页翻页漏数据

现象:用游标分页后,用户反馈"下一页少了条数据",对账发现第 3 页和第 4 页之间有条记录消失了。

排查:游标条件是update_time < :lastTime,而翻页间隙正好有一条记录的update_time被业务更新成更晚的时间(简历被编辑,更新时间刷新)。

定位:游标分页对"排序字段动态变化"的数据天然敏感——上一页的最后一条在翻页瞬间被改时间,排序位置变了,下一页的游标条件就把它漏掉了。这不是 bug,是方案特性。

解决:列表排序字段选只增不改的字段(如创建时间create_time),或把"更新时间"改成"创建时间 + 编辑次数"组合排序;业务上接受"编辑中的简历位置可能轻微漂移"的现实。要严格稳定,就用只读快照或加版本号重排。

三个方案怎么选:先治源头,再谈优化

优化手段是一方面,为什么业务会翻到第 5000 页更值得先问。招聘系统里"全部简历"这种查询,用户根本翻不到那么深,他会改筛选条件。深分页优化的第一步,是确认这个分页真的被高频使用——如果只是运营导出全量数据,那就该走异步导出任务,别让用户在页面上翻 10 万条;如果列表确实要支持海量浏览,砍掉"跳到任意页"的需求改游标。先想清楚交互,再优化 SQL,顺序反了容易白忙活。

方案也不是三选一,经常组合用,我的选择顺序:瀑布流/下拉加载/订单列表走游标分页(天然往后取,不需要跳页);后台管理要跳页(第 5000 页)用延迟关联(保留 LIMIT 语义,只优化回表);展示字段少且固定上覆盖索引(连回表都省了);海量数据导出直接异步任务。生产环境最常见的组合是"App 端列表用游标 + 后台管理用延迟关联",各配各的复合索引,互不干扰。

还有一点:三个方案的效果都建立在索引命中基础上,city_id这种过滤条件必须排在排序字段前面((city_id, update_time, id)),索引顺序错了,EXPLAIN 直接给你看Using filesort,再好的分页方案也白搭。用 EXPLAIN 验证rows和key,是每次优化的第一动作。

可以直接抄的清单

  • 深分页慢在"扫 offset 行",不是"取 limit 行",offset 越大越慢
  • 瀑布流/翻页用游标分页:(sort_col, id) < (:last)+ 复合索引,不能随机跳页
  • 必须跳页时用延迟关联:子查询只查 id 走覆盖索引,再 join 回表
  • 展示字段少且固定时上覆盖索引,别放大字段进索引
  • 游标字段选只增不改的列,排序字段可变的场景谨慎用游标
  • 上线后看慢查询日志,rows_examined超过 10 万就该动手了

深分页没有银弹,三个方案按场景选:能游标就游标,不能游标就延迟关联,字段少就覆盖索引。全都要,MySQL 也扛不住瞎翻。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询