ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

列表翻到第 50 页就卡:深分页为什么慢,延迟关联 + 游标分页实测对比

列表翻到第 50 页就卡:深分页为什么慢,延迟关联 + 游标分页实测对比 职位列表分页前 20 页嗖嗖的翻到 50 页以后明显卡顿接口从 90ms 涨到 900ms。同样的 SQLLIMIT 0, 20和LIMIT 980, 20差距能到十倍。以前觉得不就是 limit 吗直到线上被打脸才认真把深分页的账算明白。这篇用实测数据说话。先看慢在哪LIMIT 大偏移的真实开销表还是那张 300 万行的job_post列表查询长这样SELECTid,company_id,salary_min,post_timeFROMjob_postWHEREcity_code440300ANDstatus1ORDERBYpost_timeDESCLIMIT980,20;执行计划看着还行——有索引、没全表扫。但LIMIT 980, 20的实际动作是InnoDB 先把前 980 行全部扫描出来丢掉再返回最后 20 行。偏移越大白白扫描和丢弃的行越多行数是指数级上涨的LIMIT 0, 20 → 扫描 20 行 LIMIT 980, 20 → 扫描 1000 行 LIMIT 99980, 20 → 扫描 100000 行更狠的是排序。ORDER BY post_time DESC如果没吃到索引MySQL 要把符合条件的所有行都放进 filesort 排序排完再切分页——数据量大起来filesort 直接吃掉几秒。深分页慢的本质不是查 20 行慢是排序 扫描丢弃前 N 行慢。方案一延迟关联先取 id 再回表思路先只查主键跳过回表等把该返回的 20 个 id 定下来再 join 回原表取完整字段。分页子查询里的扫描依然存在但扫描的是覆盖索引只读 id 和排序列不碰行数据省掉大量回表 IOSELECTt.id,t.company_id,t.salary_min,t.post_timeFROMjob_post tINNERJOIN(SELECTidFROMjob_postWHEREcity_code440300ANDstatus1ORDERBYpost_timeDESCLIMIT980,20)tmpONt.idtmp.idORDERBYt.post_timeDESC;实测同样LIMIT 980, 20延迟关联把耗时从 900ms 压到 210ms 左右。代价是写法丑MyBatis-Plus 里要写自定义 SQLPageHelper 帮不了你。适合偏移不是特别大但回表量明显的场景是性价比最高的第一步。方案二游标分页把偏移换成起点列表是按时间倒序往下翻的话可以不用页码改用上次最后一条的位置继续翻。MySQL 里最朴素的实现就是带 WHERE 的 LIMIT-- 第一页SELECTid,company_id,salary_min,post_timeFROMjob_postWHEREcity_code440300ANDstatus1ORDERBYpost_timeDESCLIMIT20;-- 下一页传上次最后一条的 post_time 和 idSELECTid,company_id,salary_min,post_timeFROMjob_postWHEREcity_code440300ANDstatus1AND(post_time2026-10-08 12:00:00OR(post_time2026-10-08 12:00:00ANDid102456))ORDERBYpost_timeDESCLIMIT20;关键在post_time相同的情况——时间可能重复所以必须加 id 做第二排序键否则同一秒发布的职位会翻页丢失或重复。游标分页不管翻多少页扫描量恒等于本页该读的行100 页和 1 页耗时基本一样这才是根治。踩坑记录游标分页把倒序翻成了正序现象游标分页上线后用户反馈列表越翻越乱翻几页后出现重复数据而且新发布的职位不出现。排查过程先查接口入参前端传的 lastPostTime 和 lastId 都正常。再看 SQL发现问题在排序方向——列表要post_time DESC新的在前游标条件写的却是post_time lastPostTime继续找更新的倒序列表配正序游标永远找不全。定位思路游标条件和排序方向必须配套。DESC排序要往下翻游标条件是比上一条更小或相等且 id 更小写成更大就整个反了。代码里两个地方不一致——接口层排序写 DESCSQL 拼接游标条件时复制错了模板方向没跟着改。最终解决游标条件生成收敛成一个方法方向参数化杜绝手拼// direction -1 表示倒序新的在前游标取更小值publicstaticStringbuildCursorCondition(intdirection,StringtimeCol,StringidCol,ObjectlastTime,ObjectlastId){if(lastTimenull){return;// 第一页无游标}if(direction0){return(timeCol ? OR (timeCol ? AND idCol ?));}return(timeCol ? OR (timeCol ? AND idCol ?));}方案三别翻那么深——产品侧限深说到最后还有个大实话业务上没几个用户真会翻到第 100 页。列表 搜索场景产品侧把翻页上限卡在 50 页超出提示条件太窄换个筛选配合搜索条件本身就能过滤大部分深分页流量。技术方案做得再好也不如产品上不让它发生。可直接复用的清单深分页慢的本质排序 扫描丢弃前 N 行不是查 20 行慢。中浅偏移先上延迟关联子查询先取 id 再回表改动小见效快。无限滚动列表直接游标分页排序键 唯一键双条件扫描量恒定。游标方向必须和排序方向配套DESC 配小于ASC 配大于条件生成统一收口。时间戳可能重复游标必带 id 第二排序键否则翻页丢数据。产品侧限深分页上限 50 页技术 产品双管齐下。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进