☰
职位列表翻到第 10000 页接口超时:深分页优化,从 3 秒到 30ms
2026/9/29 4:54:43 网站建设 项目流程

导读:招聘平台职位列表页,用户翻到第几百页后接口开始变慢,第 10000 页直接超时。慢查询日志里躺着一条LIMIT 200000, 20的 SQL,执行要 3 秒多。这个案例很典型,深分页的优化套路也就那么几个,今天用一次真实排查把它讲透。

职位列表翻到第 10000 页接口超时:深分页优化,从 3 秒到 30ms

先说业务。求职招聘系统的职位搜索页,支持按城市、职位类型、薪资范围筛选,分页大小 20。用户猛翻页,翻到后面(比如第 10000 页,即 offset=200000)的时候,接口响应从几十毫秒涨到 3 秒,再往后直接超时。

慢 SQL 长什么样

最开始的 SQL 是这么写的:

SELECTid,title,salary_min,salary_max,city_code,company_name,updated_atFROMjob_positionWHEREcity_code='330100'ANDstatus=1ORDERBYupdated_atDESCLIMIT200000,20;

字段看着不复杂,city_code和status也有索引。用EXPLAIN看一眼:

EXPLAINSELECTid,title,salary_min,salary_max,city_code,company_name,updated_atFROMjob_positionWHEREcity_code='330100'ANDstatus=1ORDERBYupdated_atDESCLIMIT200000,20;

结果里type=ref,key=idx_city_status,乍一看索引用上了。但注意Extra列:

Using index condition; Using filesort

Using filesort出来了。排序字段updated_at不在联合索引里,MySQL 只能把 20 万行先捞出来,在内存里排完序,再丢掉前 20 万行。这就是慢的根源。

为什么 offset 越大越慢

很多人以为 LIMIT 优化是"只取 20 条",其实 MySQL 是先扫描 offset+limit 行,再丢弃前 offset 行。offset=200000 时,即使每条都命中索引,也要读 200020 行。更要命的是 SELECT 里查了company_name、salary_max这些不在索引里的字段,每行都要回表查一次聚簇索引,200020 次回表,不慢才怪。

优化一:延迟关联(最通用)

核心思路:先在索引里把 id 定位出来,再回表拿完整数据,回表次数从 20 万降到 20 次。

SELECTp.id,p.title,p.salary_min,p.salary_max,p.city_code,p.company_name,p.updated_atFROMjob_position pINNERJOIN(SELECTidFROMjob_positionWHEREcity_code='330100'ANDstatus=1ORDERBYupdated_atDESCLIMIT200000,20)tONp.id=t.idORDERBYp.updated_atDESC;

子查询里只查id,走覆盖索引(如果建了(city_code, status, updated_at)联合索引,连 filesort 都省了,直接按索引顺序扫)。这样子查询扫 20 万行但不回表,快了不是一点半点。压测结果:同一页数据,从 3.2s 降到 31ms,100 倍。

配合的索引 DDL:

ALTERTABLEjob_positionADDINDEXidx_city_status_time(city_code,status,updated_at);

ORDER BY updated_at DESC能直接走这个索引的顺序,Using filesort消失,Extra变成干净的Using index condition。

优化二:游标分页(翻到底的方案)

延迟关联能救 10000 页,但 offset 到 100 万行的时候,子查询本身也要扫 100 万行,还是会慢。更彻底的方案是不用 offset,改游标:记住上一页最后一条的updated_at,下一页只查比它小的。

SELECTid,title,salary_min,salary_max,city_code,company_name,updated_atFROMjob_positionWHEREcity_code='330100'ANDstatus=1ANDupdated_at<'2026-09-27 18:30:00'-- 上一页最后一条的时间ORDERBYupdated_atDESCLIMIT20;

这个方案每页都是 O(索引命中行数),不管翻多深都稳定。代价是不能用页码跳转,只能上一页下一页,且updated_at要有唯一性保证(同秒多条会漏数据,一般拼上id做二级条件)。

AND(updated_at<?OR(updated_at=?ANDid<?))

我在项目里是浅页数用延迟关联,超过 100 页提示用户用搜索/筛选缩小范围,没有做无限翻页的游标——招聘场景用户不会真翻到 1 万页,但接口不能因此超时。

踩坑记录:加了索引反而更慢?

说个翻车经历。我先给表加了(city_code, status, updated_at)联合索引,以为万事大吉,结果 EXPLAIN 一看,优化器还是走了老索引idx_city_status,filesort 还在。

排查过程:SHOW INDEX FROM job_position确认索引建上了,再 EXPLAIN,发现key还是旧的。查了 MySQL 版本和统计信息,ANALYZE TABLE job_position更新统计信息后,优化器才切到新索引。

这个坑的教训:加了索引不代表优化器会用,MySQL 的优化器基于统计信息做选择,表数据量变化后要ANALYZE TABLE刷新统计信息;实在不听话,可以用FORCE INDEX (idx_city_status_time)先顶上,但根因要查清楚。

可直接复用的要点

  • 慢查询日志开着,long_query_time=1,定期扫一眼。
  • 深分页三板斧:联合索引覆盖排序字段 → 延迟关联减少回表 → 游标分页根治翻页。
  • EXPLAIN看Extra:出现Using filesort就是排序没走索引。
  • 加了索引不生效,先ANALYZE TABLE刷新统计信息。
  • 业务层限制最大翻页深度,超深提示用户加筛选条件,比无限优化 SQL 更实在。

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

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

立即咨询