目标你能把“分页慢”讲成一个可解释的 IO 模型并掌握可落地的改造seek 分页、覆盖索引、延迟关联。1. offset 分页的本质扫描 丢弃经典写法select*fromtwhereuser_id?orderbycreate_timedesclimit100000,20;直觉MySQL 必须先找到前 100000 行满足 where order 的结果然后丢弃再返回 20 行这意味着页码越大被丢弃的行越多扫描成本线性增长1.1 一个可复现的最小例子同一张表对比 offset 与 seek 的 EXPLAIN准备表createtablet_order(idbigintprimarykey,user_idbigintnotnull,create_timedatetimenotnull,titlevarchar(64)notnull,contentvarchar(2000)notnull,keyidx_user_time(user_id,create_time,id,title));对比两条查询。对照 1offset 分页越翻越慢explainselect*fromt_orderwhereuser_id1orderbycreate_timedesc,iddesclimit10000,20;你要重点观察rows是否显著变大是否发生大量回表select *且Extra不会Using index对照 2seek 分页稳定explainselectid,title,create_timefromt_orderwhereuser_id1and(create_time?or(create_time?andid?))orderbycreate_timedesc,iddesclimit20;你要重点观察rows不随页码线性增长更容易出现Using index覆盖2. 即使走索引也会慢因为“走的是长距离顺扫”如果索引是(user_id, create_time)能按顺序找到记录但仍需要向后移动 100000 步才能到达起点如果还select *会回表 100020 次或接近随机 IO/缓存失效进一步放大3. seek 分页把“跳过”变成“从游标继续”思路用上一页最后一条记录的排序键作为游标下一页从游标之后继续取示例select*fromtwhereuser_id?and(create_time?)orderbycreate_timedesclimit20;如果存在同时间戳并发写入建议加 tie-breakerwhereuser_id?and(create_time?or(create_time?andid?))orderbycreate_timedesc,iddesclimit20;对应索引(user_id, create_time, id)优势不随页码变慢能稳定利用索引有序性3.1 对照组只用 create_time 做游标可能不稳定如果你的数据里存在相同时间戳错只用create_time lastTime可能出现漏数据/重复数据对加 tie-breakercreate_time相等时用id4. 覆盖索引 延迟关联解决回表放大4.1 列表页优先覆盖索引如果列表只需要少量列让索引覆盖返回列Extra: Using index4.2 必须返回全字段用延迟关联压缩回表次数select*fromtwhereidin(selectidfromtwhereuser_id?orderbycreate_timedesclimit20);目的内层只拿 20 个 id外层只回表 20 次对照点延迟关联只解决“回表次数”不解决 offset 的“扫描丢弃”。页码很深时优先 seek 分页页码不深但select *很重延迟关联收益明显5. 排序为什么会慢filesort 与临时表当where用的索引与order by不一致MySQL 可能先过滤再排序触发Using filesort优化让联合索引同时满足 where order让排序字段方向一致desc/asc6. 线上排查 checklist是否大 offsetlimit 100000, 20EXPLAINrows是否随页码增长Extra是否Using filesort/temporary是否Using index覆盖是否select *导致回表放大6.1 更流程化的排查顺序从现象到动作确认现象是否“页码越大越慢”用 EXPLAIN 看 3 个指标rows是否随 offset 增长Extra是否Using filesort/temporary是否覆盖索引Using index按根因选方案offset 导致的扫描丢弃改 seek 分页回表放大覆盖索引或延迟关联排序代价调整联合索引让 whereorder 同索引