资讯文章 资讯 · NEWS
列表页翻到后面就卡:目录站那三类慢查询是怎么来的,该怎么改
2026-10-23 约 1,793 字 预计阅读 4 分钟 yiyunkj
列
一个典型的目录站性能现象:分类页前几页都是毫秒级,翻到后面开始一秒、两秒、偶尔超时。加索引的人不少,加完发现一点用没有——因为这类问题的根子不在索引,在 SQL 的写法本身。

图片来源:网络,版权归原作者所有,侵权请联系删除
第一类:深分页为什么越翻越慢
LIMIT 100000, 20 这句话看起来只取 20 条,实际执行逻辑是先扫出前 100020 行,再把前 100000 行丢掉。它的代价和偏移量成正比,跟你最终要几条无关。偏移越大,白白扫描和回表的行就越多。
对应到目录站:一个有两千条条目的分类,每页 30 条,就有六七十页。用户翻得到第 70 页的可能性不大,但爬虫会——分页 URL 只要在站点结构里存在,就会被逐个走到。深分页慢的结果不是用户抱怨,而是爬虫在某个深页码上超时,然后那个区间的页面长时间不被更新。
| 方案 | 写法要点 | 适用 | 代价 |
|---|---|---|---|
| 游标分页 | 用上一页最后的排序键做 WHERE 条件 | 顺序浏览、无限下拉 | 不能随机跳页 |
| 延迟关联 | 子查询只取主键,外层再回表 | 必须支持跳页 | 写法复杂一点 |
| 限制页数 | 最多 100 页,更深用细分项 | 任何场景 | 需要额外的信息架构 |
游标分页是最彻底的解:WHERE id < 上次最小 id ORDER BY id DESC LIMIT 30,无论翻多深都只扫三十行。它的短板很明显——不支持「直接跳到第 N 页」。对目录站来说这其实不是硬伤,因为深层的随机跳页几乎没人用,而它的收益是恒定的毫秒级。
需要保留跳页能力时,用延迟关联:子查询在覆盖索引上只取主键,外层再拿这三十个主键回表取完整字段。同样是 100000 的偏移,从一两秒降到几十毫秒常见。
用游标分页时有一个约定要守:排序字段必须唯一。拿创建时间做游标会漏数据或者重复数据,因为同一毫秒可能有好几条。正确做法是用组合游标,(create_time, id)。
第二类:那个跟着每页走的 count
列表页要显示「共 1987 条」,得先算总数。这个 COUNT(*) 在 InnoDB 里基本是全表或全索引扫描,条目越多越慢,而且是每翻一页都算一次。
三种处理方式:一是缓存总数,五分钟或十分钟更新一次,目录站的总数少几条多几条没人看得出来;二是用估算值,MySQL 的统计信息里就有近似行数,快几个数量级;三是把它从关键路径里摘出来,页面骨架先出,总数用异步请求补。
最不划算的做法是把精确计数放在每一页的渲染里同步执行,因为它是整个页面里唯一一个随着数据增长一定会变慢、并且分布在每个列表页上的查询。早点处理,省掉的是后期那种整体卡顿。
第三类:LIKE %关键词% 为什么一定全表扫
搜索框用 WHERE name LIKE '%设计%',前置通配符会让索引失效,变成逐行比对。条目表一万行时还撑得住,十万行时每次搜索都是一次全表扫描,高峰几个并发就能把数据库连接池占满。
可选的三条路:数据量小(一两万以内)时接受它,加上查询缓存和限流就够了;需要中文分词级别的效果时,用外部检索(MySQL 自身的全文索引对中文分词支持有限,通常需要额外处理);或者退一步,把高频查询的结果预先物化成一张表,由定时任务维护。
选择依据很简单:先看站内搜索的实际并发量。大部分目录站的搜索 QPS 是个位数,这时候过早引入一套检索系统是灾难。定时任务维护的物化表加热词纠错词典,往往就够了。
怎么确认到底慢在哪
不要猜。开启慢查询日志,阈值设成一秒,跑一周拿到实际的慢查询样本。
拿到疑似语句后第一步永远是 EXPLAIN,重点看两列:rows 那一列的数字是不是远大于你实际需要的行数(远大于就说明扫描了太多无用数据),Extra 里有没有 Using filesort 或 Using temporary(有说明排序没能走索引)。
第二步看联合索引的顺序。WHERE 分类 =? ORDER BY 更新时间 DESC 这种组合,索引得建成 (分类, 更新时间) 这个顺序。顺序错了,MySQL 要么放弃索引去排序,要么只用一半。
这些改动跟 SEO 的关系
慢查询导致的结果是 TTFB 上升,具体变现为:抓取工具单位时间内拿到的页面变少;部分深页码请求超时,长期不被重抓;极端情况下数据库连接被占满,全站 5xx。
而它们的改进方式其实很便宜——大部分是改 SQL 写法,不需要加机器。把这份投入跟「把整站搬到更高配服务器」对比一下就知道,先解决写法是划算的那一档。
做完之后留一个习惯:每次改动用同一个深页码做基准测试,把响应时间记下来。下次再有性能问题,这份基线表能帮你判断是数据变多了还是代码变慢了。