开车见真章:为什么64核服务器也会卡在慢查询上
先说句得罪人的大实话:很多团队排查慢查询,第一步就把方向搞错了。一遇到数据库响应慢,第一反应是怀疑服务器配置不够,接着就扩容、升CPU、加内存。我曾经接手过一个线上案例,64核的处理器、512G内存、全闪NVMe硬盘,这个配置放在今天的大多数公司里绝对算得上豪华了,结果一个报表页面照样卡到超时。后来我把慢查询日志拉出来一看,一条SQL裸扫了三千多万行,连一个像样的索引都没用上。这就像开着法拉利送外卖——车是顶级性能,但每单都导航到城中村小巷里绕路,发动机越猛,跑偏得越远。
这个案例让我意识到一个非常普遍的问题:慢查询不等于服务器不够好,绝大多数慢查询其实是数据库自己“不会走路”。这篇内容适合所有跟数据库打交道的同学,包括后端开发、DBA、运维,也包括刚入行没几年、正被“服务器性能焦虑”困扰的人。我会把整个排障思路、日志分析方法、执行计划解读和实际优化方案串成一条完整的链路,希望你看完之后,再遇到慢查询时能先冷静下来问一句:到底是车不行,还是路线不行?
1. 开着法拉利送外卖:一场由硬件升级引发的幻觉
1.1 服务器配置拉满,却被一条SQL打回原形
那次线上故障发生后,业务方的第一封邮件就是要求“紧急扩容数据库服务器”。当时我顶着压力先把扩容量暂缓了一周,这不是拍脑袋,而是因为我看了一眼监控:CPU使用率只有个位数,IO延迟也不高,内存还有大量富余。一台几乎在“怠速”的服务器,怎么可能会因为算力不够而跑不动查询?
问题出在另一面。数据库的连接数一直很高,大量会话卡在“Sending data”状态。这个状态对老手来说是第一个警报:它不是CPU算不过来,而是在一条一条地扫描和发送结果,整个查询过程被拖成了蚂蚁搬家。我在慢查询日志里找到元凶后,当场做了一个测试——把这个查询用到的耗时逻辑拆成两部分,一部分走主键,一部分走一个存在但选错列的普通索引,耗时立刻从12秒降到了0.3秒。
硬件一点都没动,查询速度差距却有40倍。这像极了把一辆法拉利放在早高峰的市中心,驾驶技术再高、发动机马力再大,也只能跟着电动车一步一挪。增配硬件解决的是“车的极限速度”,解决不了“路线规划错误”。
1.2 硬件升级为什么经常“看似有效”
这里必须说清楚一个关键点:为什么有时候升级硬件确实有用?因为它掩盖的是另一类瓶颈。当数据库的buffer pool太小、热点数据频繁被淘汰、磁盘每秒都在做随机读时,你加内存、换SSD,效果立竿见影。这种情况本质是“油箱太小、路面太烂”,提升硬件当然有效。
但很多慢查询根本不是这种问题。一条SQL扫描了上亿行,最终只返回20行,这时候数据库的工作大头在“一行一行地判断是否符合条件”,属于纯CPU逻辑操作,加多少核都无济于事。如果SQL里还带了强制类型转换、函数包裹列、或者关联字段没索引,服务器配置再高也帮不上忙,因为它浪费在无用功上的资源远大于有效计算。
所以遇到慢查询,先别盯着CPU和内存。判断到底是哪一类瓶颈,是排障的第一道分水岭。我见过太多团队加完硬件之后问题暂时“变好”,其实是把并发请求拖到了更长的超时时间之外,该慢的还是慢,只是没触发报警而已。这种幻觉比慢查询本身更危险。我后面的所有方法,都是围绕“先把问题定位到路线层,而不是车本身”这个原则展开的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 第一现场:慢查询日志里藏着的完整真相
2.1 开启慢查询日志,别让证据提前消失
排查慢查询,第一件事是确认数据库有没有留下“现场记录”。MySQL的慢查询日志是最常用的入口,但很多生产库默认并不开启,或者阈值设得太高,导致故障发生时什么证据都没留下。我建议在核心业务库上至少这样配置:
ini复制slow_query_log = ON
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = ON
long_query_time设置成1秒,而不是默认的10秒。理由很简单:线上很多查询在正常状态下都是几十毫秒级别,一旦出现网络抖动、锁等待或者缓存失效,单条查询就会被拖到几百毫秒,如果等到超过10秒才记录,说明故障已经非常严重,你反而丢失了从“开始变慢”到“彻底超时”的中间过程。
log_queries_not_using_indexes这个参数很容易被忽略,但我强烈建议打开。它的作用是记录那些“虽然没超过阈值、但没有走索引”的查询。全表扫描的危害往往是积累出来的,单次可能只要200毫秒,可一旦并发一大,CPU立刻被打满。把这类查询记录下来,等于提前排雷。
2.2 慢查询日志的聚合:不要一条一条人工翻
生产环境运行一小时后,慢查询日志可能积累几千条记录,逐条肉眼看是不现实的。首先你用文本工具打开日志,确认基本格式,看懂字段含义:
text复制# Query_time: 12.345678 Lock_time: 0.000234 Rows_sent: 20 Rows_examined: 31234567
SET timestamp=1699999999;
SELECT ... FROM order_info o LEFT JOIN user_info u ON o.user_id = u.id WHERE u.status = 1 ORDER BY o.pay_time DESC LIMIT 20;
Query_time是总耗时,Lock_time是锁等待时间,Rows_sent是最终返回的行数,Rows_examined是实际扫描的行数。如果Rows_examined远大于Rows_sent,说明这条路绕了远路。
但如果日志很多,建议直接用pt-query-digest这样的工具做聚合,它能自动把结构相似的SQL归类,并按总耗时排序。我当时用它跑完的结果显示,某一条订单查询占总慢查询时间的75%以上。看到这个比例,就可以把注意力从“服务器负载高不高”转移到“这条SQL的路线图到底怎么回事”上了。
2.3 手工复现,锁定真正消耗时间的那一环
定位到具体SQL后,接下来我会做一次手工复现。复现环境不一定非要和生产环境一模一样,但表结构、数据量级和索引状态最好接近,否则得出的结论没有参考意义。
在MySQL命令行里执行这条SQL,同时用另一个会话观察状态。常用的做法是执行前先FLUSH STATUS,执行后查看Handler_read_first、Handler_read_next等状态变量。如果Handler_read_next的数值很大,基本可以断定数据库做了大量的逐行扫描。另一个更直接的办法是打开profiling:
sql复制SET profiling = 1;
SELECT ...;
SHOW PROFILES;
SHOW PROFILE FOR QUERY 7;
这一步能拆分出执行过程中各个阶段的时间占比:语法分析、表打开、查询优化、存储引擎读取、排序、发送结果等。大多数全表扫描型慢查询,时间都会集中在Sending data或executing阶段,而不是网络传输。看到这种结果,基本可以认定问题不在服务器硬件,而在执行路径本身。
3. 执行计划就是行车路线:EXPLAIN到底应该看什么
3.1 只跑一遍SQL看不出它为什么慢
一条慢SQL手工执行可能要好几秒,你能直观感受到它慢,但看不出它慢的具体原因。EXPLAIN就是用来把数据库的执行路线图摊开给你看的工具。很多初学者看到EXPLAIN的结果就懵了,十几列字段不知道重点在哪。我的习惯是先抓四个核心:type、key、rows、Extra。
拿前面那条订单查询举例,执行EXPLAIN后大概率会看到如下输出片段:
text复制+----------+-------+------+---------------+------+---------+------+----------+-----------------+
| table | type | key | rows | Extra |
+----------+-------+------+---------------+------+---------+------+----------+-----------------+
| o | ALL | NULL | 26000000 | Using filesort |
| u | ALL | NULL | 500000 | Using where; Using join buffer |
+----------+-------+------+---------------+------+---------+------+----------+-----------------+
type为ALL,意思是全表扫描;key为NULL,说明这个表上没有用到任何索引;Extra里出现Using filesort,表示排序没法走索引,只能在临时内存中再排一次。这套组合拳看下来,你立刻明白为什么服务器那么高级还是卡——数据库把两千六百万行数据全扫了一遍,再对结果做文件排序,相当于让外卖员把全城每条路都走一遍,回站点重新排序后再挨个配送。
3.2 type字段的正确阅读顺序
type字段的常见值从好到坏依次是:system、const、eq_ref、ref、range、index、ALL。其中range及以上基本都可以接受,index虽然扫的是索引树,但通常比全表扫描好一些,ALL是最差的情况。
看到eq_ref或ref,说明关联查询用了索引等值匹配,效率高;看到range,说明用到了范围查询,比如BETWEEN、>、<等,只要选择性够好问题不大。最需要警惕的是ALL和index配合大rows值的组合,它们代表“数据访问方式是遍历”,而不是“精确定位”。
我一直强调一个观点:EXPLAIN里的rows字段代表预估值,不是精确值,但它能反映优化器认为要扫描多少行。如果预估扫描行数远离目标行数,你要思考两个原因,一是索引没建对,二是统计信息过期了。后者可以执行ANALYZE TABLE来刷新,前者就需要走索引设计流程了。
3.3 Extra列的隐藏信息:filesort和temporary不是小事
发生Using filesort不代表真的在磁盘上排序,内存排序区不足时才会落到磁盘。但它仍然意味着排序操作无法利用索引的有序性,需要数据库额外开工。你可以想象成,外卖路线明明可以按门店顺序一键排好,却非要先把所有订单收集起来,再手动按距离重新排序。
我的建议是,只要SQL中包含ORDER BY,就要看看排序字段是否和WHERE条件中的字段构成了联合索引。比如这个查询按pay_time排序,同时按user_id等值关联,那么(user_id, pay_time)联合索引就非常有价值。原因是索引本身有序,数据库遍历时按索引顺序读取,性能会好很多。
另一种常见情况是Using temporary,通常出现在GROUP BY、DISTINCT或某些关联查询中。它意味着数据库需要创建临时表来辅助计算,涉及大量内存和磁盘交换,一旦数据量大,性能直线下滑。所以看到Extra里有Using temporary,优先考虑改写SQL或调整索引,而不是给服务器加内存。
在用EXPLAIN判断索引是否使用时,还可以顺手查看possible_keys和key的区别。possible_keys表示可能使用的索引,key表示实际使用的索引。有时候possible_keys里有索引但key为NULL,说明优化器判断走索引还不如全表扫描快。这种判断不一定正确,常和数据分布、统计信息有关。可以通过FORCE INDEX做一次对比测试,但最终还是要靠合理的索引设计解决问题,而不是长期留着一个强制索引的补丁。
4. 路线优化实操:让同一台服务器跑出不同效果
4.1 先处理“深分页”这个热点问题
与服务器性能相关的热搜词里,经常能看到“分页查询慢怎么用redis优化”,可见深分页是全网共性痛点。这里先解释一句:深分页本身不慢,慢的是它背后那条“先把前100万行扫描出来再丢弃”的执行路径。
典型的慢SQL长这样:
sql复制SELECT id, order_no, user_id, amount, status, pay_time
FROM order_info
ORDER BY pay_time DESC
LIMIT 200000, 20;
假设order_info表有两千万行,它的执行步骤是:先按索引把200020行找出来,然后扫描这200020行对应的完整数据,最后丢弃前200000行,只返回最后20行。大量时间浪费在获取、回表、丢弃这些过程上。
改进方案之一是延迟关联,先只查主键ID,再通过主键去关联完整的行:
sql复制SELECT t.id, t.order_no, t.user_id, t.amount, t.status, t.pay_time
FROM (
SELECT id
FROM order_info
ORDER BY pay_time DESC
LIMIT 200000, 20
) d
JOIN order_info t ON t.id = d.id;
子查询先走覆盖索引,只返回id,再通过主键关联回原表取完整数据。回表次数从20万次降到了20次,效果立竿见影。
如果业务能接受“下一页”模式而非“跳页”模式,还能更进一步用游标分页。因为跳页本身是反数据库直觉的:数据库处理的是位置无关的记录流,它并不知道“第200001行”有什么特殊含义。改成基于上次查询中的动态条件,一次性精确定位到一个更小的数据窗口。不同数据量和不同查询复杂度下,需要根据实际EXPLAIN结果选择方案。
4.2 索引设计:别把所有字段逐一加索引
很多人以为加索引就是给WHERE后面的每个字段各建一个索引,这是严重误区。单独索引对等值查询有效,但对组合查询往往只有一个会被优化器采用,其他索引白白占用空间,还会拖慢写入性能。
正确的联合索引设计要遵循几个基本原则:等值条件字段放前面,排序字段紧随其后,范围条件靠后。以这个查询继续举例,如果它同时要过滤status和pay_time:
sql复制SELECT ... FROM order_info WHERE status IN (1, 2) ORDER BY pay_time DESC LIMIT 20;
可以设计索引(status, pay_time),让等值过滤和排序都走索引树。但如果再加一个范围条件pay_time >某个值,就需要注意排序字段一旦本身是范围条件,索引顺序就要重新掂量。
实际设计时,还要考虑索引的选择性。选择性是指列中不同值的比例,如果某列90%都是同一个值,索引几乎不会带来过滤效果,反而增加回表成本。我经常看到线上有一个state字段,状态只有三种,但很多人给它建了单列索引,结果优化器看了统计信息后依然选择全表扫描。这种没被用上的索引,除了浪费磁盘,没有其他价值。
覆盖索引是另一个值得好好利用的工具。当SQL涉及的列全部包含在同一个索引中时,数据库不需要回表就能直接拿到数据。比如我的延迟关联子查询,实际上就是在利用覆盖索引把回表次数降到最低。日常设计索引时,如果能根据SELECT列、WHERE条件、ORDER BY字段综合出一个覆盖索引,对高频小查询的收益非常明显。
4.3 缓存兜底:Redis适合哪一层
热搜词里提到“分页查询慢怎么用redis优化”,这里把边界讲透。Redis做缓存确实能极大缓解部分慢查询,但它不是万能药,更不是让你去扛深分页的。
合理的做法是缓存那些“固定条件下的高频查询结果”。比如首页的排行榜、热点新闻列表、最近订单前20条,这类查询变动不频繁,且响应结果会被大量用户重复访问,非常适合放在Redis中。设置了合理的过期时间后,数据库压力大幅下降。
对于深分页的场景,缓存反而容易踩坑。如果用户反复翻到第几万页,你不可能把每一页结果都提前生成并缓存。比较聪明的做法是只缓存前几页或者用户最容易访问的热点页,深层页继续走数据库的游标分页或延迟关联方案。也就是说,Redis负责挡住热流量,SQL优化负责照顾长尾,两条腿走路才走得稳。
缓存还会带来数据一致性问题。订单状态一旦变更,缓存中的列表可能过期了,还是要及时清理对应的分页缓存。真要把缓存用顺,建议在更新操作发生后,主动删除相关缓存键,让下一次查询重建结果,而不是依赖超长过期时间,否则用户迟早会看到脏数据。这里有两层考虑,一层是技术上的性能取舍,另一层是业务上的体验一致。
5. 长效防线:慢查询的监控、提交与排除干扰
5.1 建立慢查询的自动分析与告警机制
一次慢查询排查结束,并不意味着可以高枕无忧。长期有效的手段是让慢查询日志自动进入分析流程。我所在团队的做法是写一个定时任务,每小时分析一次慢日志,把结果写入一个单独的性能库,并按哈希值聚合相同结构的SQL;如果某条SQL出现频率过高或累计耗时超过阈值,就自动告警。这套机制等于给服务器装了一个“行车记录仪”,而不是等外卖超时了才看监控回放。
告警阈值不要拍脑袋定,需要根据业务差异做区分。交易核心链路可能要求主流程查询小于100毫秒,后台报表允许1秒以内。用同一条慢查询阈值覆盖所有业务并不合理,真要细化的话,尽量把报表库和在线事务库拆开或区别对待。这比给单台服务器配置顶级硬件更有效,成本也更低。
5.2 慢查询日志的趋势分析带来的反思
在技术细节之外,还有一类“慢查询”经常被误伤,那就是服务器本身的问题:比如rsyslog日志服务器配置不当导致系统日志堆积、服务器时区配错导致时间统计混乱、vscode连接远程服务器调试时网络波动,这些因素可能反映在连接层而不是查询层。排查的时候,要先排除这种干扰。
一个很典型的例子是,我排查一个慢查询时,发现SQL本身很快,但连接建立环节异常缓慢。后续调查发现服务器的时钟同步服务没有开启,安全认证阶段产生额外开销。这类问题如果只看SQL,永远找不到答案。所以慢查询定位要分两层:第一层是数据库内部是否在高效地工作,第二层是数据库外部环境是否在稳定地提供支撑。整体排查链路里,先看库内执行计划,再看系统层监控,顺序不能反。
5.3 把经验沉淀为团队SOP和SQL审查单
最后一个心得,是尽可能把这次排查经验固化成团队的发布流程。我们现在的做法是,凡是涉及核心表结构变更、新增索引或复杂SQL,上线前都要过一遍EXPLAIN,并在工单里附上执行计划截图。DDL检查项会关注是否用了覆盖索引、是否避免函数包裹索引列、是否隐式类型转换,以及深分页是否改成了游标或延迟关联。
这套SOP能拦截掉大量潜在慢查询。比如刚入职的同学很可能写出WHERE user_id = '12345'这样的SQL,user_id是整型,但字符串会被隐式转换成数值,索引仍然有可能失效;或者对create_time列使用DATE(create_time) = '2025-01-01',函数包裹索引列之后数据库就没办法高效走索引了。审查单的存在就是早期拦住这些低级错误。
定期清理冗余索引也很重要。每次发布索引都增加,长期下来索引攒了一大堆,数量过多的索引会让优化器选择起来更加耗时,写入时需要维护的索引树也跟着变多。我会用performance_schema或sys库中的schema_unused_indexes视图,找出那些从未被使用过的索引,评估后删掉。这一系列优化动作做完,通常不需要动硬件,就足以让现有服务器重新回到健康水位。
只要顺着慢查询日志、执行计划、索引设计、监控沉淀这条链路走一遍,你会慢慢发现,“拿着顶级服务器跑慢查询”这件事完全可以避免。法拉利的说明书不会教你送外卖怎么规划路线,数据库的硬件配置同样不会替你优化SQL。真正解决问题的,永远是把每条查询的运行路线图看清楚的那个人。
