2.4 慢查询与参数调优:首页从 2 秒到 200 毫秒

社区上线一段时间后,陆续有用户在群里反馈首页越来越慢。我自己定时测了一遍:首屏接口平均耗时 2 秒,赶上访问高峰直奔 3 秒。一个主打图片分享的网站,首屏转半天圈,新用户点开就走了,留存数据自然难看。这篇记录完整的排查和优化过程——慢查询怎么开、问题怎么定位、索引怎么加、参数怎么调,最后把接口压到 200 毫秒以内。全程没用任何黑魔法,全是可照着做的常规手段,核心就六个字:先测量、后优化,别瞎折腾。

一、第一步:让慢查询自己浮出水面

排查数据库性能问题,第一步永远是打开慢查询日志,而不是凭感觉猜哪里慢。我一开始就是凭感觉改了几处代码,耗时纹丝不动,这才老老实实回去开日志——这个弯路本身值得记住:性能优化的第一原则是先测量、后优化,没有数据支撑的改动多半是白费功夫。云数据库 MySQL 自带慢查询记录,在实例的日志管理页可以直接查看,也能下载下来慢慢分析。

配合 long_query_time 参数可以控制记录阈值。我一开始设成 1 秒先抓大鱼,抓到几条后又调到 0.5 秒再细筛:

-- 查看当前阈值
SHOW VARIABLES LIKE 'long_query_time';

-- 临时调整为 1 秒
SET GLOBAL long_query_time = 1;

提示:在控制台的参数设置里修改会持久生效,实例重启后依然保留;命令行里的 SET GLOBAL 只对实例当前运行期有效。生产环境建议统一走控制台,避免重启后配置悄悄丢失。

开关打开没多一会儿,慢查询日志里就抓到三条常客:图片列表查询、用户头像批量加载、首页图片计数。当时我心里大概有数,但具体慢在哪、慢到什么程度、优化后能快多少,还是 EXPLAIN 说了算。这里插一句方法论:慢查询日志告诉你哪些查询慢,EXPLAIN 告诉你它为什么慢,两者要配合使用——只看慢日志容易治标不治本,只看执行计划又不知道该先治哪条。确认目标查询之后,我对着日志和执行计划做了一张清单,按耗时从高到低逐条处理,改一条测一条。

二、第二步:三个性能黑洞

逐条用 EXPLAIN 分析执行计划后,首页慢的原因水落石出,一共三个:

慢查询 症状 EXPLAIN 上的特征
图片列表 按作者 ID 过滤,扫描行数巨大 type 列显示 ALL,即全表扫描
首页计数 每次都 COUNT 整张表 无可用索引,扫描行数等于全表行数
头像加载 列表每行都单独查一次用户表 一次列表渲染触发 21 次查询,即 N+1

简单解释一下表里的术语:EXPLAIN 输出的 type 列表示访问方式,从好到坏大致是 const、eq_ref、ref、range、index、ALL,看到 ALL 就意味着整表逐行扫描,基本就是要补索引的信号。完整演示一下加索引前后的对比效果:

-- 加索引前:type 为 ALL,扫描行数十几万
EXPLAIN SELECT * FROM image WHERE user_id = 123;

-- 加索引后:type 变为 ref,扫描行数降为几十
EXPLAIN SELECT * FROM image WHERE user_id = 123;

字段不同输出也不同,但判断套路一样:先看 type,再看扫描行数,最后看额外信息里有没有临时表和文件排序。

第一个黑洞:缺索引。 图片表按作者 ID 过滤是最高频的访问路径,但建表时只给主键和短链字段建了索引,作者 ID 这个字段被我忽略了。补上之后扫描行数直接从十几万掉到几十:

ALTER TABLE image ADD INDEX idx_user_id (user_id);

第二个黑洞:N+1 查询。 首页取出 20 张图后,代码里对每一行循环查作者的头像和昵称,一次列表渲染就是 21 次数据库往返。ORM 用起来顺手,但不留神就会写出这种循环查库的代码,而且本地开发数据少、完全感觉不到慢,一上线用户多了才暴露。改成一次批量查询,再在内存里按 ID 拼接:

SELECT id, nickname, avatar FROM user
WHERE id IN (1, 2, 3, 45, 67);

程序里先收集列表涉及的作者 ID 去重,一次查回再组装,把 N+1 压成 1+1。数据库往返次数下来了,耗时立刻掉一大截。怎么快速确认代码里有没有 N+1?在测试环境开一下数据库的 SQL 全量记录,看一次接口请求触发了几条查询就门儿清了。

第三个黑洞:全表计数。 首页要显示图片总数,COUNT 每次都扫全表。短期靠覆盖索引让扫描代价降下来;中期把总数挪进缓存层定时刷新,页面上显示的数字有点延迟完全无所谓——首页要的是个接近数,不是精确到个位的实时值。缓存怎么配合使用,后面的 CDN 篇还会再讲一次,本篇先专注把数据库这一侧的事情做到位。

三、加索引前后:数据差距

优化前后用同样的查询逐条实测,差距非常直观:

查询 优化前 优化后
图片列表 20 条 1300 毫秒 90 毫秒
头像批量加载 600 毫秒(21 次查询) 40 毫秒(1 次查询)
首页图片计数 800 毫秒 25 毫秒(走缓存)
首屏接口总耗时 约 2 秒 约 200 毫秒

加索引是性价比最高的优化手段,但不是免费的午餐:索引占存储空间,也会拖慢每次写入。我的原则是只给真正高频的查询路径建索引,每加一条都先看 EXPLAIN 判断收益,不盲目堆索引。索引建得太多,写入代价和优化器的选择成本都会变成新的负担——有时候索引太多反而让优化器选错执行计划,慢得更隐蔽。

压测方法也顺便交代一下:我用同一个接口地址,在更新前后各请求几十次取平均,同时避开缓存干扰(首次请求和缓存命中后的耗时完全不是一码事)。条件允许的话,最好在访问低峰期测,数据才有可比性。测完把结果记在工单里存档,方便下次怀疑性能回退时翻出来对比。

四、第四步:参数层面的调整

索引和 SQL 都改完之后,参数层面我只动了两个地方:

参数方向 怎么处理 心态
InnoDB 缓冲池 云数据库随规格自动调整,升规格等于扩缓冲池 先把规格用满,再谈参数优化
连接相关参数 与应用侧连接池对齐,避免连接数震荡 两侧配合调才有意义,单边调等于白调

缓冲池那条道理很简单:内存的大头给最热的数据做缓存,剩下的交给查询本身。云数据库把缓冲池随规格管理好了,我手工能调的空间其实很有限,所以与其纠结参数,不如把规格选对。这也是云数据库和自建的一个区别:自建时期你能调的参数很多,也更容易调出事;云数据库帮你管住大头,剩下的难点反而集中在表结构设计和查询写法上。

参数调优的一大忌是贪多:今天改这个、明天改那个,出问题都说不清是哪一个的锅。我的做法是每改一个参数、观察一天监控曲线再决定下一步。数据库的 CPU、内存、连接数指标就是最好的反馈仪表盘,配合云监控配上告警阈值,异常自己会找上门,不用人盯屏幕。说得功利一点:一个正确的告警策略,顶你每天花半小时人工巡检。

分析 EXPLAIN 和改写 SQL 时,我经常把执行计划丢给 AI 帮忙一起看,比自己啃文档快不少,工具实践整理在DeepSeek 工具链实战。数据库之外,首页体验还涉及静态资源加速,那是后面 CDN 篇的内容;更通用的站点性能思路可以参考地图与站点优化,里头对站点层面的优化有系统的整理。

五、小结

  • 排查从慢查询日志开始:先开开关,再调阈值收网
  • EXPLAIN 是照妖镜:type 为 ALL 就是全表扫描,额外信息里出现临时表和文件排序也要警惕
  • 三板斧:索引解决单表扫描,批量查询解决 N+1,缓存解决聚合计数
  • 参数调优抓大放小:缓冲池和连接数优先,一次只动一个、观察一天再继续

回头看,这次优化最有价值的不是某一条索引,而是形成了一套可复用的排查节奏:监控发现问题、慢查询定位、EXPLAIN 分析、最小改动验证。后面再遇到性能问题,照着这个流程走,心里一点都不慌。关键在于每次只做一个变量并立即验证——这套方法论迁移到缓存设计、代码层面乃至服务器层面的调优,都同样好用,属于一次学会、终身受用的手艺。

延伸阅读