ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MySQL SQL执行链路全拆解:从连接到存储引擎的每一步

MySQL SQL执行链路全拆解:从连接到存储引擎的每一步

一条 SQL 丢给 MySQL,到底要经过多少道工序才把结果还给你?这个问题我回答过不少次,很多同学第一反应是“查询嘛,打进去就出来了”,等到排查慢查询的时候才发现,自己对执行链路的理解,直接决定了能不能精准定位问题。今天这篇就把这个链路完整拆开,把连接、解析、优化、执行、存储引擎这几个环节逐个讲清楚,也会带一个真实的表结构和查询示例,把每个阶段怎么观察、怎么排查串起来。无论是刚入门想搞懂 MySQL 的开发者,还是有经验的 DBA,都可以对照自己的场景找到值得深挖的点。

我先给一个结论:SQL 只是任务描述,不是执行指令。MySQL 必须把这段文本翻译成内部执行计划,再按计划逐层访问数据,这个翻译和落地的过程,就是它的核心价值所在。理解了这条链路,你才能真正看懂慢查询日志、读懂 EXPLAIN、知道什么时候该改 SQL,什么时候该加索引,什么时候其实应该调整参数。

1. 先建立整体认知:SQL 执行生命周期有四个主要阶段

边看慢日志边看进程状态的时候,我经常跟同事说,SQL 不是凭空执行的,它像一条流水线:客户端把 SQL 原文扔进来,服务端一层层接住、拆解、编排,最后才能操作数据。整个生命周期大致是连接管理、解析检查、查询优化、执行返回四大段,每一段的职责完全不一样。

1.1 为什么 MySQL 要把 SQL 拆成这么多阶段

你要理解这件事,先想一个生活化的场景:你跟一个只会按流程办事的窗口打交道,递进去一张申请表,窗口先看你有没有资格,再把表内容逐项录入,然后系统帮你生成最优审批路径,最后各环节按路径盖章返回。SQL 的执行就是这么一回事。连接管理相当于资格审查,解析检查相当于逐项录入和校验,查询优化相当于生成最优审批路径,执行返回相当于逐级盖章和回执。

拆成多阶段的好处,首先是职责清晰,每层只干一件事,出了任何问题都能定位到具体环节。其次是存储引擎可以替换,同样的 SQL 语法,在 InnoDB、MyISAM 甚至其他存储引擎上都能执行,就是因为解析和优化不直接触碰数据,真正碰数据的是最后一层的存储引擎接口。这个松耦合的设计,也是 MySQL 能长期适应不同业务负载的基础。

1.2 每个阶段的失败点和排查入口对应关系

了解分层之后,最重要的事情是把“故障现象”和“阶段”对上号,这样排查慢查询和报错的思路才有方向。我在实际工作中经常看到两种情况:一种是语法报错,结果有人去调索引;一种是执行超时,结果有人反复改 SQL 格式。

各阶段的典型现象大致是这样:如果报错信息是语法错误,比如关键字拼错、括号不匹配,那是解析阶段的问题;如果报错说某个库表或者字段不存在,那是语义检查阶段的问题;如果报错说没有权限,那是连接后或解析后的权限检查拦截了;如果 SQL 能正常返回但是特别慢,多半是优化器选错了执行计划,或者执行器访问存储引擎时走了低效路径;如果卡住不动且报锁等待超时,那就是执行阶段在跟 InnoDB 的行锁打交道时阻塞了。

这么一梳理你就发现,每个现象背后都有一块对应的机制。下面我会按这个顺序,把每个阶段的原理、参数、排查手段一点点铺开。

2. 连接与会话层:影响 SQL 执行的第一道门

很多人研究慢 SQL 只盯着优化器和索引,却忽略了连接层。实际上 SQL 能否顺利开始执行,第一步就是能不能建立起一条可用的会话通道。连接建立的成本比你想象中高,尤其是使用短连接的业务,握手过程中的认证、权限读取、网络往返都是开销。

2.1 连接建立的成本和一个我踩过的连接坑

MySQL 的连接过程涉及 TCP 三次握手,然后是 MySQL 协议层的握手认证,客户端和服务端要交换版本信息、认证插件、密码校验结果,这一来一回在低延迟内网里可能就几毫秒,但在跨机房或者公网环境里,一轮 RTT 可能就是几十毫秒。如果业务每次请求都新建连接,这部分开销会被放大得非常明显。

所以我特别建议使用连接池,让长连接复用。但要小心一个坑:连接池里的连接如果长时间没有活动,会被服务端的 wait_timeout 或 interactive_timeout 参数断开。这时候你以为连接还是好的,发出去第一条 SQL 直接报“Lost connection to MySQL server during query”,或者连接池拿到一个已经失效的连接,表现为偶发性报错。解决办法是连接池一定要配置探活机制,比如定期执行一次轻量查询,或者在拿到连接时先做一次有效性校验。

另外,服务端能支撑的连接数不是无限的。默认的 max_connections 通常较小,生产环境如果出现大量短连接堆积,很容易触发“Too many connections”错误。我之前处理过的一个案例,就是某个服务把连接池 maxSize 配得过大,同时并发压力上来后每个连接都占着线程资源,直接打满连接数,其他正常业务也连不上。后来调整了服务端 max_connections,同时压住了客户端连接池上限,才把问题解决。调大 max_connections 不是不行,但每增加一个连接,都会占用线程栈、内存和可能的事务资源,盲目调大反而会让系统进入崩溃边缘。

2.2 会话级变量会改变同一个 SQL 的命运

连接建立之后,每个连接就是一个会话,会话里有一堆系统变量,它们决定了这条 SQL 在后续阶段怎么跑。这里我不展开所有变量,只挑几个影响面大的说。

sql_mode 是一个典型的例子。它控制 SQL 的宽松程度,比如是否允许字符串转数字时产生警告,是否把零日期当成非法值,是否开启 ONLY_FULL_GROUP_BY。如果 sql_mode 设置得太严格,一些原本能跑的 SQL 可能直接报错或者执行结果跟预期不一样;如果设置得太宽松,又容易把不规范的数据悄悄写进去。所以同一个 SQL,在不同的 sql_mode 下可能有不同的命运,这一点排查异常时要优先确认。

还有一类影响性能的会话变量,比如 sort_buffer_size、join_buffer_size。这些缓冲区是每个会话需要用的时候临时分配的。你以为把 sort_buffer_size 调大能加快排序,但如果并发会话很多,每个会话都分配一大块内存,MySQL 的内存压力瞬间就上去了,甚至可能触发内存交换。我的经验是这类参数优先保持默认或小容量,遇到真实排序慢的场景,先去验证是不是真的需要用临时文件排序,再谨慎调整。

3. 解析与检查:从字符串到 MySQL 能理解的结构

连接建立后,SQL 原文开始进入解析流程。这一阶段 MySQL 做的事情,是把文本翻译成内部结构。很多人以为这里只是简单拆一下字符串,实际上它分了好几小步:词法分析、语法分析、语义检查和权限检查。

3.1 词法分析、语法分析、语义检查各干什么

第一步是词法分析,也可以叫分词。MySQL 把 SQL 字符串拆成一个一个的 token,也就是关键字、表名、字段名、字符串常量、数字、运算符这些元素。比如 SELECT id, name FROM user WHERE age > 18,会被拆成 SELECT、id、逗号、name、FROM、user、WHERE、age、>、18。这一步本身一般不会报错,因为任何字符串都能被拆开。

第二步是语法分析。MySQL 根据 SQL 语法规则,把这些 token 组装成一棵解析树。这棵树的每一个节点都对应一种语法成分,比如投影列表、表引用、条件表达式。如果你写的 SQL 不符合语法规则,比如 SELECT 后面什么都不写,或者 WHERE 后面跟了一个不完整的表达式,MySQL 在这个阶段就会直接报语法错误,错误信息里会给出语法附近的内容。这就是你看到 1064 语法错误时的来源。

第三步是语义检查,也叫做解析后的绑定。MySQL 会拿着解析树去查数据字典,确认 FROM 后面的表确实存在,SELECT 后面的字段确实属于这张表,函数名是否存在,类型是否兼容。如果表不存在或字段不存在,就是在这里报出来的。这里有一个容易被忽略的细节:字段名如果带引号,MySQL 可能会把它当成字符串而不是列名,这也属于语义层的歧义问题。

3.2 权限检查到底拦在哪一步

权限检查也是一个关键环节。MySQL 在解析阶段拿到表和字段信息之后,会检查当前用户对这个表有没有相应的权限,比如 SELECT 需要 SELECT 权限,UPDATE 需要 UPDATE 权限。如果权限不足,直接抛“SELECT command denied to user”之类的错误。

有人可能会问:权限检查放在这么靠前的位置合理吗?如果某个用户没有权限查这张表,提前拦截既能省去后面优化和执行的开销,也符合安全直觉。但同时你要意识到,权限检查也是开销的一部分,特别是涉及视图、存储过程这种需要层层解析对象依赖关系的场景。一个存储过程内部的 SQL,可能要依赖调用者权限或定义者权限规则来做额外检查。

如果你在生产环境碰到一条 SQL 执行时快时慢,有时候也可以留意一下是不是权限模式差异导致的缓存影响。MySQL 的权限加载和缓存机制,在一些复杂权限模型下会让查询计划表现不稳定。虽然这类问题不常有,但一旦出现,你要知道它属于连接和检查阶段,不要一股脑去做 SQL 改写。

3.3 prepared statement 能省掉一部分解析开销

解析阶段是要消耗 CPU 的,而且 MySQL 在传统模式下,对同一条 SQL 的每次执行都会重新做解析。虽然 8.0 以后内部有了一些改进,但一条新的 SQL 文本进入,至少词法和语法分析是少不了的。

如果你在写代码时发现某种 SQL 会被反复执行上万次,只是参数不同,比如用户中心里按用户 ID 查资料,那么可以考虑用 prepared statement。它把 SQL 的固定部分预先解析成内部结构,后续执行只传入参数值变化,可以减少一部分重复解析开销。在 MySQL 客户端里也有 PREPARE/EXECUTE 语法可以使用,JDBC 驱动里如果启用了预编译,本质上也是在走这个链路。

这里要补充提醒:预编译并不是“优化器会提前固定死执行计划”。MySQL 的预编译重点在防注入和减少解析,优化器依然会在执行时根据当时统计信息选择执行计划。所以别以为用了 prepared statement 就能解决所有性能问题,它解决的是解析开销和注入风险。

4. 优化器如何挑出执行计划以及它的“小心思”

解析完成之后,SQL 已经变成一棵语义正确的结构化树。接下来就是整个执行链路中最充满变数的环节:优化器决策。优化器的工作是输入一条逻辑上等价的查询,输出多个候选执行计划,再估算每个计划的成本,选一个它认为代价最低的来执行。

4.1 优化器的内核逻辑是估算成本

MySQL 优化器用的是一种基于成本的模型。每个计划会被拆成一条条原子操作,比如走主键、走二级索引、顺序全表扫描、临时表排序、连接时驱动表访问方式等,每种操作都有成本常数和估算行数参与计算。行数估算依赖表的统计信息,也就是每个索引列的“基数”和数据页分布情况。

用生活化的方式理解:你要从 A 点去 B 点,优化器不只是看直线距离,它还要看路况、红绿灯、车速。索引就像是已经修好的快速路,全表扫描就像是在普通小路上挨家挨户敲门。但它手里的地图可能是旧版本的,实际路况已经变了,这就可能导致它选了一条自以为很快的路。

我经常用 EXPLAIN 看执行计划,重点看 type 列。如果看到 ALL,意味着全表扫描;如果看到 ref 或者 range,说明用到了索引等值或范围匹配;如果看到 const,那是主键或唯一索引的等值匹配,性能最好。这些扫描方式对应的成本估算差异很大,也是优化器决策的核心依据。

4.2 一张统计信息不准导致执行计划跑偏的实例

说一个我实际遇到的例子。某业务有一个订单表,订单状态字段 status,上面建了一个普通索引。平时 status=1 的数据量很小,优化器会选择走 status 索引。但某天大促状态数据暴涨,status=1 的记录占了全表 60% 以上,由于统计信息没有及时更新,优化器依然以为走 status 索引只需要扫极少行数,结果实际执行时用索引扫描了大半张表,再回表查,慢得离谱。

这个案例告诉两件事:第一,即使有索引,优化器也可能选错;第二,当数据分布发生极大变化时,ANALYZE TABLE 是有价值的,执行完以后统计信息被更新,优化器才可能改成全表扫描而不是傻傻回表。顺便说一句,全表扫描不一定比走索引慢,尤其是当返回数据量超过全表一定比例时,顺序读可能比随机回表快得多。很多新手一看没走索引就慌,其实优化器算过账的。

实操上,我建议在分析执行计划前先手动跑一次 ANALYZE TABLE,排除“统计信息过期”这个变量。如果你确实怀疑统计信息有问题,可以比较执行计划中的 rows 估算值和实际返回行数,差距如果超过一个数量级,基本就是统计不准。

4.3 优化器跑偏时的三个常规干预手段

干预手段我一般按优先级排列。第一优先是改写 SQL,让条件结构更清晰。比如把 OR 改成 UNION ALL,把函数包裹索引列的表达式改成能走索引的等价条件。总之让优化器更容易摸到正确的访问路径。

第二优先是更新统计信息。前面说了,ANALYZE TABLE 是轻量且安全的手段。如果表特别大,可以在低峰期执行,或者手动设置部分列的直方图信息。8.0 里 InnoDB 支持直方图,也给优化器提供了更细的分布信息。

第三优先才是使用索引提示,比如 FORCE INDEX、USE INDEX。我不太建议一上来就强制索引,因为它会让执行计划固化,一旦数据分布变化,这个强制可能越来越糟。它更适合先临时压住问题、验证猜想,确认有效后再优化 SQL 本身。

另外,很多人会忽略 optimizer_switch 这个参数,它控制优化器启用哪些特性,比如 semi-join、materialization、derived_merge 等。特定版本下某个优化特性可能带来回归,关闭对应开关就等于退回到更保守的路径。这些开关是调整执行计划的最后手段,要在理解特性的前提下使用。

5. 执行器与存储引擎的配合:读和写的不同路径

优化器输出一个执行计划,接下来就轮到执行器动手了。执行计划是一棵可迭代的算子树,执行器会按照算子的嵌套关系,一层层从存储引擎读取数据、做计算、过滤、投影聚合,最后把结果攒起来返回给客户端。

5.1 handler 接口把 MySQL 服务层和存储引擎层隔开

MySQL 服务层不直接知道 InnoDB 的 B+ 树长什么样,它通过 handler 接口来操作存储引擎。handler 这个接口屏蔽了不同引擎的实现差异,InnoDB、MyISAM、老版本的 Memory 引擎,都在这个接口下面提供自己的实现。所以执行器说“我要按主键查这一行”,InnoDB 就把自己的主键索引检索逻辑拿出来,MyISAM 就用自己的方式。

这就解释了为什么 MySQL 支持插件式存储引擎。正是因为有 handler 接口,你在建表时用 ENGINE=InnoDB 和 ENGINE=MyISAM,上层 SQL 语法完全一致,但底层数据访问、锁机制、崩溃恢复能力完全不同。

具体到 InnoDB,执行 SELECT 时,存储引擎会先把目标数据页从磁盘读入缓冲池,如果页已经缓存,直接走内存查找。索引定位靠的是 B+ 树,从根节点走到叶子节点,叶子节点上的数据按主键顺序排列。二级索引的叶子节点存储的是索引列值和主键值,如果查询列不在二级索引里,就需要拿着主键值再回主键索引查一次,这就是所谓的回表。

5.2 一致性读和行锁决定了 SELECT 与 UPDATE 的本质差异

SELECT 和 UPDATE 在存储引擎层的路径差异非常大。普通 SELECT 走一致性读,InnoDB 通过 MVCC 机制为事务建立一致性视图。在 REPEATABLE READ 隔离级别下,事务内多次普通读看到的是同一份快照,这个快照不是把数据复制一份,而是通过 undo log 和事务版本链来构造读视图。

而 UPDATE 操作不仅要把目标行找出来,还要对行加排他锁,防止其他事务同时修改。加锁成功后,InnoDB 会把这一行的旧版本写入 undo log,再修改数据页上的新版本,同时生成 redo log 来保证崩溃恢复。如果两个事务同时更新同一行,后到的事务会被阻塞,等待锁持有者提交或回滚,这就是我们常说的锁等待。

这里有个非常容易踩的坑:一条 UPDATE 虽然在语句层面看起来很简单,但执行器扫描路径可能是全表扫描,这样它会对扫描到的每一行都尝试加锁,直接锁住大量行,造成其他事务大面积阻塞。所以优化 UPDATE 的 WHERE 条件,让它精准命中索引,不光是性能问题,更关系到并发下锁范围的大小。

5.3 结果的返回不是一次性全给客户端

我在排查问题的时候还发现,有些同学会误解结果返回方式。MySQL 默认情况下,客户端是一次性接收查询结果,还是边查边取?这取决于客户端 API 和配置。在默认的 MySQL 协议里,服务端可以先把结果缓冲完再发给客户端,也可以使用流式游标边扫边发。

如果查询返回结果集特别大,一次性服务端缓存会把所有结果都攒在内存里,对内存和网络缓冲区都造成压力。某些客户端库里,你可以设置 fetchSize 来启动流式读取,比如 JDBC 中需要开启 useCursorFetch 并设置合理的 fetchSize。流式读取的好处是内存占用低,缺点是读取期间连接会被事务占住,不能做其他操作。因此,结果是“全量返回”还是“边取边返”,对执行性能和资源消耗影响很大,调优时也要放在考虑范围内。

6. 用一个完整示例把上面的链路串起来

纸上谈兵再多,不如直接拿一条 SQL 从头到尾看一遍。我在这里构造一个简单的业务场景,模拟一张用户订单表,用它在生产环境里最常见的查询方式走一遍链路。

6.1 场景和数据准备

假设业务上有这样一张订单表:

CREATE TABLE `t_order` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `order_no` VARCHAR(64) NOT NULL, `amount` DECIMAL(10,2) DEFAULT NULL, `status` TINYINT NOT NULL DEFAULT 0, `create_time` DATETIME NOT NULL, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

现在要执行这样一条查询:找出某个用户最近一段时间的大额订单。

SELECT id, order_no, amount FROM t_order WHERE user_id = 10001 AND create_time >= '2024-01-01' AND amount > 100 ORDER BY create_time DESC;

这条 SQL 拿到 MySQL 里,第一步就是连接层和解析层,这里不再重复。到优化器这里,它面对的选择是:走 idx_user_id 索引先过滤用户,再过滤时间;还是走 idx_create_time 索引先按时间范围过滤,再过滤用户;甚至直接全表扫描。它怎么选?依靠统计信息和成本估算。

6.2 用 EXPLAIN 观察优化器到底选了哪条路

我一般会先直接跑 EXPLAIN:

EXPLAIN SELECT id, order_no, amount FROM t_order WHERE user_id = 10001 AND create_time >= '2024-01-01' AND amount > 100 ORDER BY create_time DESC;

通常能看到执行计划里使用 idx_user_id 或者 idx_create_time。如果使用 idx_user_id,type 是 ref,rows 估算的是该用户订单数;然后还需要对这部分数据按 create_time 和 amount 过滤排序。如果该用户的订单数量非常大,而 create_time 范围其实只覆盖很少数据,那走 idx_create_time 可能更合适。

这里我经常建议再配合 EXPLAIN ANALYZE 看实际执行时间和实际行数。8.0.18 以后支持这个语法,比如:

EXPLAIN ANALYZE SELECT id, order_no, amount FROM t_order WHERE user_id = 10001 AND create_time >= '2024-01-01' AND amount > 100 ORDER BY create_time DESC;

它会返回一棵带实际耗时的执行树,里面能看到每一步实际读取行数和花费时间。这样对比优化器的 rows 估算和实际行数,你就能立刻发现统计信息是否失真。

6.3 加索引后全程变化对比

如果数据量到了百万级别,而业务里这个查询又高频出现,那么单索引方案可能不够。比如走 idx_user_id 后,还需要回表取 order_no 和 amount,回表的行数等于该用户符合条件的时间范围订单数。要减少回表和排序成本,可以考虑建立一个覆盖联合索引:

ALTER TABLE t_order ADD KEY idx_user_create_amount (`user_id`, `create_time`, `amount`);

这里把 user_id、create_time、amount 放在同一个二级索引里。优化器可以直接在这个索引上完成等值 user_id 过滤、范围 create_time 过滤、读取 amount 字段,而 order_no 和 id 可能还需要回表,不过 amount 已经在索引里,减少了部分数据页读取。如果你把 order_no 也加进去,就完全覆盖了查询列,成为真正的覆盖索引。

加完索引之后再看 EXPLAIN,type 可能变成 range,Extra 列会出现 Using index condition 或者 Using index 等信息。完整扫描行数明显下降,排序也可能因为在索引上已经有序而消失。这就是把底层存储组织方式和查询条件匹配起来之后带来的直接收益。

当然,索引不是越多越好。每个索引都会占用磁盘空间,并且拖慢写入速度。对于需要高频更新的表,多一个索引就多一份二级索引维护成本。你在生产环境做索引设计,一定要看真实的查询比例和写入压力,不要照着“覆盖索引能提速”这一点盲目堆索引。

7. 执行链路的问题排查工具速查

前几章已经把每个阶段都拆开讲了,最后我给你一套可以直接照着用的排查工具组合。这套组合我在定位线上问题的时候反复用过,思路是先看走到哪一步慢,再定位慢在哪类资源上,最后根据阶段特征做针对性优化。

7.1 常用排查手段与观察对象

排查需求推荐工具/视图主要观察点
找出最慢的 SQL慢查询日志 slow_query_logquery_time、rows_examined、rows_sent
查看执行计划EXPLAIN / EXPLAIN FORMAT=TREEtype、key、rows、Extra
比较估算与实际开销EXPLAIN ANALYZE实际耗时、实际行数
要看优化器当时怎么想的OPTIMIZER_TRACE索引选择、成本对比、最终决策原因
排查锁等待information_schema.innodb_trxtrx_state、锁等待时间、事务 SQL
定位等待事件performance_schema.events_waits_history_long等待类型、等待对象、耗时统计
看连接和会话状态SHOW PROCESSLISTState、Command、Time、Info

慢查询日志是最先看的。它会记录执行时间超过 long_query_time 阈值的 SQL,rows_examined 就是执行过程中真实扫描或者访问过的行数。我判断一条 SQL 是否高效,经常不看时延而看 rows_examined 和 rows_sent 的比值。如果扫描了 100 万行只返回 100 行,说明过滤条件或者索引利用存在大问题;如果扫描行数和返回行数接近,说明访问路径很精准。

EXPLAIN 是第二件武器。它给的是一个静态执行计划,不代表真实执行时间。所以我特别推荐在关键场景用 EXPLAIN ANALYZE 做真实采样,它是带实际执行反馈的,可以一次性看到每一步的执行时间、启动时间、总行数和循环次数。缺点是该语句会真实执行,如果是 UPDATE、DELETE 这类有副作用的语句,千万不能直接 EXPLAIN ANALYZE,实际数据会被改动。

7.2 几个典型的阶段问题与处理经验

我把线上最常见的几类“慢 SQL”问题整理成了一份经验清单,你可以收藏起来用于自查。

第一类:SQL 本身不复杂,但是执行时 CPU 飙高。这种情况先确认是不是大量 SQL 解析和短查询并发挤压。如果每秒有几千条 SQL 进来,每条都要重新解析,CPU 会被解析消耗掉。这时优先考虑 prepared statement 和连接池批量复用,减少短连接频繁建连解析。

第二类:用了索引但还是慢。常见原因是回表次数太多,或者索引列区分度太低。区分度太低意味着索引每个值对应的行数很多,走索引跟扫描没区别。处理办法是分析索引选择性和查询过滤条件,必要时用多列联合索引提高区分度。

第三类:SQL 偶尔慢一次,之后又正常。这个特征常见于冷数据页加载。第一次查询时目标数据页不在缓冲池,需要从磁盘读,耗时高;后续数据页已经缓存,所以变快。观察指标是 Innodb_buffer_pool_read_requests 和 Innodb_buffer_pool_reads 的对比,如果读磁盘请求占比高,说明缓冲池命中率不足。另一个偶尔慢的原因是统计信息更新后执行计划突然切换,这时候需要去对比执行计划变化。

第四类:UPDATE 和 DELETE 被锁卡住。这类问题通过 SHOW PROCESSLIST 往往能看到 Waiting for lock 之类的状态,或者从 innodb_trx 里找到锁等待记录。处理思路要分两层:第一层检查被更新的行是否落在长事务里,第二层检查更新条件的索引使用情况,尽量避免大范围扫描加锁。

第五类:排序和临时文件导致慢。EXPLAIN 里 Extra 如果出现 Using filesort,表示排序无法利用索引,需要在内存或磁盘临时文件里排。如果同时出现 Using temporary,说明有中间结果要落地到临时表。这类问题的优化思路是让 ORDER BY 和 GROUP BY 的字段配合 WHERE 条件组成合适索引,让数据天然有序,从而省掉排序环节。

7.3 我个人常用的一个笨办法:一步步缩小链路范围

最后分享一个很朴素的排查方法。遇到一条 SQL 不慢但整体系统被拖垮的情况,我习惯直接在监控侧看线程状态和等待事件。MySQL 提供的信息很丰富,先把 performance_schema 打开,再去 events_waits_history_long 里按等待时间倒序看,通常能看到当前系统到底卡在 IO、锁等待还是 CPU 调度上。

这个思路的笨拙在于没有捷径,但它非常可靠:优先确认“卡在哪个环节”,而不是一上来就改 SQL。因为有一类慢查询,其实不是 SQL 本身慢,而是整个数据库资源已经被其他查询挤占。你用 EXPLAIN 怎么分析都发现不了问题,这时候跳出 SQL,去看系统级等待事件才是正解。

我自己在踩过几次坑之后,已经形成了一套固定的优先级:先看连接有没有堆积,再看锁等待是不是普遍现象,然后才看单条 SQL 的执行计划,最后才是优化索引或者改写 SQL。这个顺序能过滤掉大量干扰项,也能帮你在汇报问题的时候拿出完整的证据链。希望这套方法对你也有用。

返回列表