MySQL SQL执行全链路解析:从连接到存储引擎的优化指南

发布时间:2026/10/6 9:07:40
MySQL SQL执行全链路解析:从连接到存储引擎的优化指南 1. 一条 SELECT 从客户端发起MySQL 内部要过哪些关卡1.1 整条链路先看一眼全貌很多刚开始接触 MySQL 的同学对“执行一条 SQL”的理解就是把语句丢给数据库数据库查找数据返回结果。这么说没错但真实情况远不止这么简单。一条SELECT * FROM user WHERE id 123从发出到拿到结果中间至少经过连接器、解析器、优化器、执行器、存储引擎这几层每一层都有自己明确的职责和边界。这里先给一个全局视角后面每一层我们再拆开讲客户端/驱动层负责建立网络连接、发送 SQL 文本、接收结果集。连接管理与鉴权校验用户名密码确认你有权限登录并把连接纳入 MySQL 的会话管理体系。解析与预处理把 SQL 文本拆成 MySQL 能理解的数据结构检查语法、表名、列名、权限。优化器从多种可能的执行路径中挑一个代价最小的生成执行计划。执行器根据执行计划一步步调用存储引擎接口处理返回的行数据。存储引擎层真正和磁盘、缓冲池打交道负责数据的读取和返回。这其实很像一次外卖下单的流程你客户端打电话连接器下单接单员记录需求解析器并确认你能点这个菜权限校验后厨会根据订单决定先做哪个菜、怎么做最优优化器最后炒菜师傅执行器用锅具存储引擎真正把菜做出来再由配送员送回你手上。这个类比虽然简单但能帮你记住一个关键点MySQL 不是一个“整体执行 SQL”的怪物它是一套分工明确的流水线。你排查 SQL 慢的问题本质上就是在排查流水线上哪一环出了问题。1.2 为什么很多人以为“执行 SQL”就只是执行引擎在干活我在带团队做数据库优化时经常问一个问题“SELECT 慢你觉得是哪个环节慢”大部分人会脱口而出“表数据太多了索引没走”。这个回答没错但不完整。索引选择是优化器的事数据读取是执行器和存储引擎的事而连接器如果出问题SQL 甚至还没走到“查询”这一步就被卡住了。举个很常见的例子线上突然出现大量的Too many connections你第一反应是谁在跑大查询结果一查发现是某个应用端连接池配置出错把 MySQL 连接数打满了。这时候你优化 SQL 一点用都没有因为请求根本没到达优化器。再比如一个 SQL 语法本身有错误或者访问了一个不存在的列名它在解析器阶段就会直接报错压根不会进入后面任何环节。所以理解一条 SQL 的执行过程不是让你背面试题而是让你具备一个能力任何一次 SQL 异常你能在脑海里快速定位到“这是哪一层的问题”。这句话是我这篇文章的核心目的。2. 连接器先证明你是谁再谈执行2.1 TCP 握手与鉴权细节客户端要执行 SQL第一步是建立连接。这里说的连接底层是 TCP 连接加上 MySQL 自定义的应用层协议。默认端口 3306如果是本机 socket 连接走的是/tmp/mysql.sock这类 Unix socket。在这个阶段完成三件事TCP 三次握手建立网络通道。MySQL 协议握手服务端发送握手包包含协议版本、服务端版本、认证插件类型等客户端回送认证响应。身份与权限验证MySQL 校验用户名、密码、来源 IP并从权限系统里加载这个账号的全局权限、库权限、表权限、列权限放到会话里留着后面用。这里有个细节容易被忽略认证插件。老版本 MySQL 默认是mysql_native_password新版本从 8.0 开始默认成了caching_sha2_password。如果你用的是老驱动连接 8.0 的库出现Authentication plugin caching_sha2_password cannot be loaded这种报错问题就出在握手阶段和你的 SQL 半毛钱关系都没有。解决办法是升级驱动或者给对应账号指定兼容的认证插件但后者属于临时方案。2.2 长连接与连接池的坑MySQL 的连接是典型的“短连接成本高、长连接有隐患”。每次新建连接都要经过 TCP 握手、MySQL 握手、权限读取这三步。权限数据如果数据量大加上网络 RTT一个连接建下来几十毫秒很常见。所以业务层普遍使用连接池比如 HikariCP、Druid、Spring 自带的池目的就是复用连接降低建连开销。但长连接有一个非常经典的坑连接状态一直挂在Sleep占着服务器资源却没有真正干活。我见过一个生产事故应用连接池配置了minIdle50、maxActive200数据库max_connections300。正常情况下没问题有一天某个接口被刷大量线程从池里拿连接执行慢 SQL慢 SQL 不结束池里的连接就不释放新请求继续建新连接很快把 300 个连接全占满。后面的请求排队等池子里归还连接前端超时于是重试又带来更多请求最后数据库连接被占满整个应用不可用。排查的时候第一眼看到的不是慢 SQL 本身而是满屏的Too many connections。所以连接层给你的经验是连接数不是越大越好池子大小、超时时间、连接验证策略三者必须一起调。池里的连接如果长期不用MySQL 那边wait_timeout会自动断开但连接池不知道下次拿到一个死连接就会报Communications link failure。解决办法是在连接池配置里开test-on-borrow或者定时ping验证连接。2.3 连接数限制和常见报错SHOW VARIABLES LIKE max_connections; SHOW STATUS LIKE Threads_connected;max_connections是 MySQL 允许的最大连接数Threads_connected是当前已用连接数。如果两者接近饱和说明连接层已经告急。常见的连接层报错有这几类报错信息含义常见原因Too many connections连接数超过上限连接池配置过大、慢查询占住连接不释放Access denied for user鉴权失败密码错误、账号不存在、Host 范围不匹配Communications link failure连接中断网络抖动、wait_timeout超时断开、连接池未验证连接Connection reset by peer连接被重置应用端提前断开或 MySQL 主动 kill 连接排查连接问题时优先用SHOW PROCESSLIST看每个连接在干什么mysql SHOW PROCESSLIST; ---------------------------------------------------------------------- | Id | User | Host | db | Command | Time | State | Info | ---------------------------------------------------------------------- | 5 | app | 10.0.0.1 | shop | Query | 120 | Sending data | SELECT * FROM ... | | 6 | app | 10.0.0.2 | shop | Sleep | 300 | | NULL | ----------------------------------------------------------------------Command是Query说明正在执行 SQL是Sleep说明空闲。Time越大越要警惕Query时间过长是慢 SQL 占用资源Sleep时间过长是连接泄漏不释放。看到这两种情况首先确认是否存在连接池泄漏、事务未提交等情况然后再看 SQL 本身。3. 解析器与预处理器SQL 从文本变成内部结构3.1 词法分析和语法分析到底在干什么连接建立之后你发过来的是一条字符串例如SELECT id, name FROM user WHERE age 18 ORDER BY create_time DESC LIMIT 10;MySQL 的解析器要做两件事。第一件事词法分析。把字符串拆成一个个 token识别出哪些是关键字SELECT、FROM、WHERE、ORDER BY、LIMIT哪些是表名、列名、数值、字符串字面量。这一步是通过正则和状态机扫描实现的。你可以把它理解为把一整句话拆成一个个单词并给每个单词标上词性。第二件事语法分析。根据 MySQL 的语法规则把 token 流组合成一棵抽象语法树AST。这一步会检查语句结构是否合法SELECT后面是否跟了列表达式FROM后面是否跟了表名WHERE后面是否跟了条件表达式LIMIT后面参数是否合法ORDER BY字段是否和SELECT列表冲突等。语法分析阶段是最容易出传统报错的地方ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ...这个 1064 报错本质上就是解析器在这里告诉你语法树构建失败我读不懂你的 SQL。常见的触发原因包括字符串引号不闭合、关键字拼写错误、语法升级前后写法不兼容比如 8.0 之后窗口函数没写好、括号不匹配等。3.2 预处理器做的权限检查和元数据解析语法树构建成功不代表万事大吉接下来是预处理阶段它做三件重要的事情检查表是否存在FROM user中的user表在库中是否存在。检查列是否存在SELECT id, name中的id、name列是否存在于user表。扩充权限验证确认当前用户对user表有SELECT(id, name)的权限。这一步用的是连接建立时加载好的权限数据。这里就要讨论一个非常容易混淆的问题什么时候做权限校验很多人以为权限校验发生在连接阶段其实不是。连接阶段只校验你能不能登录 MySQL。你登录成功后能不能读某张表、能不能查某个列要到预处理阶段查表结构时逐一校验。所以一个用户即使能连接到 MySQL如果没被授权查询某张表在执行SELECT * FROM secret_table时会在这一步直接报错ERROR 1142 (42000): SELECT command denied to user testlocalhost for table secret_table这个报错出现的位置已经通过了连接器和语法检查说明问题不是“连不上”而是“没权限”。排查时需要检查授权SHOW GRANTS FOR testlocalhost;关于权限校验有个细节值得注意列权限的校验发生在预处理阶段但表权限的校验可能在优化阶段再次发生。特别是涉及视图的子查询时MySQL 会递归校验视图对底层表的访问权限。如果你把一张表字段分列授权给某个账号SELECT * FROM table会把没有权限的列也暴露出来吗实测会发现它报错而不是自动过滤。也就是说MySQL 不会因为你没权限查某些列就只返回你有权限的列它会在预处理阶段直接拒绝整条查询避免“部分可见”带来的安全隐患。3.3 常见解析阶段报错与排查解析阶段报错的特征是SQL 还没执行直接返回。通过SHOW GLOBAL STATUS LIKE Queries可以看到这一类请求其实也被计入 Queries 总量但它们在SHOW PROCESSLIST中存活时间极短很难被抓到。排查这类报错我建议直接从几个方向入手用EXPLAIN SELECT ...跑一遍如果报语法错误说明解析阶段就挂了。检查字符串转义和\在拼接 SQL 时特别容易出问题。检查是否用了 8.0 才有的语法跑到 5.7 的库上比如WITH ... AS在 5.7 需要特定版本WINDOW函数在 5.7 完全不可用。检查表名/列名大小写敏感问题Linux 上表名大小写敏感列名不敏感但代码里混用不同大小写风格容易埋坑。解析器有个常被忽略的性能点如果 SQL 文本非常长比如批量拼接几千条 INSERT VALUES解析器的 CPU 开销会明显上升。这也是为什么批量插入要控制单条 SQL 的 size而不是无限拼接。解析再快也是纯 CPU 工作长文本的 token 化、AST 构建、元数据校验都会变成执行链路里的固定成本。4. 优化器决定执行计划的“幕后黑手”4.1 为什么同名 SQL 会有不同表现解析器把 SQL 变成语法树之后MySQL 知道你要干什么了但“干什么”不等于“怎么干”。同一个查询可能有多种执行路径全表扫描把user表从头扫到尾。走age列的索引。走id主键索引后再回表。如果有多个索引可以选是走 A 索引还是 B 索引ORDER BY create_time DESC是要排序还是直接走create_time索引天然有序避免 filesortLIMIT 10是否可以提前终止扫描只取 10 行就返回优化器就是一个“决策者”它在多个执行方案里挑一个它认为代价最小的。注意是**“它认为”**不是“绝对最优”。这是理解优化器最重要的一句话。我见过一个真实案例有个报表表里有idx_status和idx_create_time两个索引SQL 是SELECT * FROM report WHERE status 1 AND create_time 2024-01-01 ORDER BY create_time DESC LIMIT 20;优化器选的是idx_status先过滤status 1结果status 1的行有几十万回表再过滤时间再排序最终执行时间 15 秒。但如果我们强制走idx_create_time倒序扫索引最早命中 20 条记录就够了执行时间不到 50ms。这就是典型的优化器方案选择与预期不符。原因在于优化器的“神经”它根据表的统计信息估算每个方案的代价如果status 1的区分度不准确统计信息过期或均匀度假设被打破它就会算错代价选错路径。4.2 优化器如何算成本、选索引MySQL 的优化器是基于成本的优化器简称CBO。它给每个执行方案算一个总代价包含I/O 代价读取数据页的数量。CPU 代价过滤、排序、比较等操作的开销。通信代价数据传输回客户端的开销。计算公式可以简化理解成总代价 ≈ 全表扫描代价 vs 索引扫描代价 回表代价 排序代价全表扫描的代价取决于表的行数和页数索引选择的代价则取决于索引的区分度INDEX A每个值平均对应多少行越少越好。回表成本走二级索引查出的主键还要再回聚簇索引查完整行。是否覆盖如果SELECT的列都在索引中可以直接走覆盖索引减少回表。排序列是否能借助索引天然有序避免 filesort。为了“猜”这些数字优化器依赖存储引擎给出的统计信息。InnoDB 通过随机采样估算索引的基数cardinality如果采样时机不对统计信息就会“过期”导致优化器“误判”。你可以用ANALYZE TABLE强制重新统计ANALYZE TABLE user;注意ANALYZE TABLE有锁的开销别在业务高峰期频繁跑。4.3 EXPLAIN 和 optimizer trace 实操排查 SQL 执行计划最常用的工具是EXPLAINEXPLAIN SELECT id, name FROM user WHERE age 18 ORDER BY create_time DESC LIMIT 10;它的输出是执行计划的核心摘要。重点看几列列名含义关键经验type访问类型从ALL到index到range到ref到eq_ref到const扫描范围逐步收窄key实际用的索引如果是 NULL代表没有用索引rows预估扫描行数偏差大时说明统计信息可能不准Extra额外信息Using filesort、Using temporary通常意味着需要优化filtered过滤比例越小说明 where 条件能过滤掉越多的行看EXPLAIN的第一原则不要只看有没有走索引要看估算扫描行数和实际行数是否匹配。我自己的习惯是先看type。ALL是最坏情况全表扫描index说明扫描了整个索引也不一定好range说明走了索引范围扫描通常是可控的ref和const是较理想的等值匹配。再看Extra。Using filesort说明 MySQL 不得不额外做一次排序如果排序列能走索引往往可以消掉这一项。Using temporary说明中间结果建了临时表常见于GROUP BY非索引列、DISTINCT复杂查询等场景。当EXPLAIN只给了执行计划结果但没告诉你“为什么”这时候用Optimizer TraceSET optimizer_traceenabledon; SELECT * FROM user WHERE age 18 ORDER BY create_time DESC LIMIT 10; SELECT * FROM information_schema.OPTIMIZER_TRACE\G SET optimizer_traceenabledoff;OPTIMIZER_TRACE会把优化器的思考过程全部打印出来它考虑了哪些索引估算的成本是多少为什么选了某一个。这个工具在排查“优化器选错索引”这类疑难问题时非常有用比单纯看EXPLAIN更能定位原因。4.4 索引失效的优化器视角关于索引失效网上有大量帖子讲“避免在索引列上进行函数运算”“避免隐式类型转换”等等。这里我从优化器的角度说一下为什么会这样。索引本身是 B 树节点是有序排列的。如果查询条件能转换成“索引列和一个常量做比较”B 树就能用二分查找定位到某个范围。但如果你在索引列上套了函数比如SELECT * FROM user WHERE DATE(create_time) 2024-01-01;优化器无法直接把DATE(create_time)这个表达式映射成create_time在 B 树上的有序范围因为函数改变了值的排序规则。所以它只能放弃索引的范围查询能力改而扫描整个索引或全表行数一下子暴增。优化器当然也可以尝试“改写”这类表达式来保留索引但不是所有函数都支持改写。MySQL 8.0 在这方面有了改进但核心原则仍然是尽量让索引列保持原样让函数作用在参数上。正确写法是SELECT * FROM user WHERE create_time 2024-01-01 00:00:00 AND create_time 2024-01-02 00:00:00;再比如隐式类型转换。假设user.id是 VARCHAR你写WHERE id 100数字 100 会被转成字符串再比较还是字符串被转成数字分情况。MySQL 规则是如果比较的双方类型不一致通常会把“字符串”转成“数字”再进行比较。如果索引列是字符串转换后索引列的原本顺序就被打破了优化器可能又只能放弃索引。优化器在这里扮演的角色不是“故意坑你”它只是在“无法可靠判断索引有效性”的时候做一个最保守的选择扫描。所以排查索引失效不要只看EXPLAIN结果更要想清楚这个查询条件到底能不能被 B 树利用上。5. 执行器与存储引擎数据到底从哪搬出来的5.1 Server 层与存储引擎层的分工优化器生成执行计划后执行器开始“跑”这个计划。执行器在 Server 层它本身不知道数据在磁盘还是缓冲池不知道索引里的 key 怎么组织它只知道“我要按执行计划去调用存储引擎的接口拿回行记录然后做 Server 层的后续处理”。举个例子执行计划告诉执行器第一步从user表的idx_age索引读取age 18的第一行第二步通过主键回表取整行记录第三步看是否符合其他过滤条件第四步取下一行直到满足LIMIT 10。执行器就会一次次调用存储引擎接口handler::ha_index_read()按索引读取第一行。handler::ha_index_next()按索引顺序读取下一行。handler::ha_rnd_pos()按主键位置读取行。存储引擎InnoDB、MyISAM 等才是真正干脏活累活的那一层它决定数据页怎么缓存、索引怎么查找、行锁怎么加、事务怎么控制。这个分层设计最有趣的代价是Server 层和存储引擎层之间会有反复交互。MySQL 没有像某些数据库那样直接把执行器编译成存储引擎的原生指令而是通过 handler 接口层的虚函数实现“通用化调用”。每读一行就要从存储引擎跨越到 Server 层一次。如果扫描行数很多这个“跨层”本身的调用开销也会叠加。所以优化的一个重要思路就是减少从存储引擎返回给 Server 层的行数。走索引、加过滤条件、控制LIMIT本质都是在减少这个“跨层搬砖”的次数。5.2 InnoDB 与 MyISAM 等引擎的执行差异执行器是通用逻辑但底层的存储引擎可以各显神通。InnoDB默认引擎的特点是聚簇索引组织表主键和数据行存在一起二级索引的叶子节点存的是主键值。支持事务、行锁、MVCC。有缓冲池Buffer Pool读过的数据页缓存在内存里下次读直接走内存。数据页默认大小 16K一次读取最少读一页。MyISAM老引擎的特点是数据和索引分开存储索引叶子节点存放指向数据行的物理地址。不支持事务锁表崩溃恢复能力差。在只读场景下扫描速度曾经有优势但现在基本被 InnoDB 取代。如果你在 5.7 之后的 MySQL 里新建普通表默认就是 InnoDB。所以我们要聊的执行过程严格说是“InnoDB 引擎下的执行过程”。InnoDB 读数据的时候有个原则叫预读它不会一次只读一个数据页而是按照顺序预测要读的下一页批量读入缓冲池。比如你全表扫描一张大表执行器让 InnoDB 读“第一行”InnoDB 会一口气预读一批数据页到缓冲池后续的行读取大多直接命中内存。这也是为什么全表扫描有时候看起来“并不慢”——因为它在内存里连续搬数据I/O 反而不是瓶颈。但一旦数据量超过缓冲池大小全表扫描就会频繁触发磁盘 I/O表现就是Sending data状态持续很久。5.3 没索引的行扫描到底发生了什么假设你执行SELECT * FROM user WHERE age 18;如果user表没有age索引也没有覆盖索引执行器的计划就是全表扫描。InnoDB 会沿着聚簇索引也就是主键顺序从头开始扫描整张表把每一行数据都返回给 Server 层由 Server 层判断age 18是否成立。这里有个很容易忽略的点即便age 18一眼看过去过滤性很好全表扫描也一点都“省事”。因为它必须把每一行读出来看一眼哪怕只有 1% 的行满足条件它也要扫 100% 的行。优化器在全表扫描和索引扫描之间算成本时会拿预估行数、页数、过滤比例综合算账。有时它选择全表扫描不是因为“笨”而是因为表太小走索引的额外开销磁盘随机读、回表反而更大。判断走索引是否划算可以参考rows估算。如果优化器预估要扫 100000 行但type还是ALL说明它认为全表扫更便宜。这时候你想人工干预可以用FORCE INDEXSELECT * FROM user FORCE INDEX (idx_age) WHERE age 18;但注意FORCE INDEX是让优化器优先考虑指定索引而不是“强制使用”如果该索引对该 SQL 完全不可用MySQL 还是有可能选择其他方案。遇到这种情况最好用IGNORE INDEX观察对比而不是直接下狠手。5.4 从执行器回 Query 结果到客户端的完整回路最后一步是把结果集返回给客户端。这个环节有不少细节会影响体验很多人忽略。执行器拿到满足条件的行后会按SELECT列表的列名组装数据。然后MySQL 会把结果数据写进网络发送缓冲区通过 MySQL 协议包分批传输给客户端。你可以用max_allowed_packet控制单次传输的最大包大小。这里有几个实践要点大结果集不要一次性全捞出来。应用端做分页查询LIMIT/OFFSET数据库不仅扫描时减少返回行数网络传输也减少。LIMIT 1000000, 20这种深分页是很差的做法。它要先把前 100 万行扫描出来丢掉再取第 100 万行后的 20 行。优化方法是改用游标分页或记录上次最大 id。结果集的组装和传输也是执行器“忙碌”的时间。有时候你用SHOW PROCESSLIST看到一个 SQL 一直处于Sending data状态其实就是执行器一边扫描、一边在往客户端推数据可能卡在网络上了。执行器这一步还会做一件事记录慢查询日志。如果这条 SQL 的实际执行时间超过了long_query_time默认 10 秒执行器会在执行结束后写一条慢查询日志记录时间、SQL 文本、扫描行数、返回行数等。注意long_query_time的单位是秒但判断标准是“实际执行时间”不包括连接建立和等待锁的时间。6. 查询慢真凶的排查思路从一次实战说起6.1 慢查询日志和分析前面说了一大堆原理最终都是为排查和优化服务的。我实际带团队排查慢 SQL第一步永远是开慢查询日志或者直接查 MySQL 里的slow log表。SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 2; -- 设置超过2秒就记录 SET GLOBAL slow_query_log_file /var/log/mysql/slow.log;然后可以用mysqldumpslow汇总慢日志找到 Top N SQLmysqldumpslow -s t -t 10 /var/log/mysql/slow.log这条命令按耗时排序展示前 10 条慢 SQL。也可以直接在表里查SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 20;注意mysql.slow_log这张表不是默认就维护的需要在配置文件里打开输出到表比较麻烦。生产环境一般直接看日志文件。拿到慢 SQL 之后我的标准动作是这样的EXPLAIN看执行计划确认是否全表扫描、是否 filesort、临时表。看rows估算行数和实际数据量对比判断统计信息是否需要更新。看status字段里的Sending data状态持续时间。用OPTIMIZER_TRACE看优化器到底为什么选择当前计划。结合业务场景决定是加索引、改写 SQL还是调整参数innodb_buffer_pool_size、max_allowed_packet等。6.2 同一张表、同一条 SQL 的不同环境表现很多人问我同一个问题同样的 SQL 在测试环境毫秒级返回在线上却跑好几秒。这里最常被忽略的是环境差异。我碰到过一个例子测试库的表只有 5 万行线上表有 800 万行。同一套 SQLSELECT order_id, user_id, amount FROM order_table WHERE status 0 ORDER BY create_time DESC LIMIT 20;测试环境走了idx_status特别快线上也走了idx_status但 status0 的行有 600 万回表之后还要再排序又因为create_time不是索引前缀排序只能用 filesort线上直接慢成狗。这时候加一个联合索引就能解决ALTER TABLE order_table ADD INDEX idx_status_create (status, create_time);为什么这个索引有效因为status作为等值条件create_time作为排序条件联合索引天然让数据按 status 分组、组内按 create_time 有序。执行器扫描idx_status_create时每一组的 create_time 已经排好序了直接倒序取前 20 行即可连 filesort 都省了。这个案例说明同一套 SQL 在不同数据分布下执行计划完全可能不同。优化器是根据统计信息来决策的统计信息又依赖实际数据所以“原理够懂实际看数据分布”才是排查慢 SQL 的正道。6.3 我实际排查过的一个案例再分享一个我自己遇到过的、不那么常规的案例。某个系统的用户表user有 2000 万行线上某条 SQLSELECT id, name, phone FROM user WHERE phone 13800138000 LIMIT 1;明明phone上有唯一索引uk_phoneEXPLAIN显示的type却是refrows估算只有 1实际执行竟然要 800ms。一开始我不知道为什么后来查OPTIMIZER_TRACE发现优化器认为走uk_phone的代价比全表扫描高原因在于统计信息显示该索引的 cardinality 特别低——也就是说优化器认为这个索引区分度很差每个 phone 值对应很多行。可实际上 phone 基本是唯一的。为什么统计信息会这么离谱因为那张表之前经历过大批量DELETE INSERTInnoDB 的采样统计还没跟上。解决办法ANALYZE TABLE user;跑了之后执行计划自动改成constSQL 瞬间回到 10ms 内。这个案例有两点值得记到笔记里统计信息过期会骗过优化器ANALYZE TABLE是重要的“纠正手段”。排查执行计划不要只信EXPLAIN的第一眼要看rows是否符合预期不符合时优先查统计信息。MySQL 的统计信息更新机制是自动的但更新时机依赖“变更行数超过一定阈值”。大批量操作后主动ANALYZE是最可靠的做法。7. 一些从执行链路延伸出来的优化习惯前面讲清楚了一条 SQL 的完整执行路径最后我想说的是懂原理不是终点能把原理用到日常工作中才是真正的收益。这里整理几条我从实践中沉淀下来的优化习惯供你参考。先确认瓶颈在哪一层再动手优化。连接层问题调连接参数解析层问题改 SQL 拼装方式优化器问题更新统计或加索引引擎层问题调 Buffer Pool 和 I/O 配置。不分层排查很容易白费功夫。慢 SQL 排查从执行计划入手但执行计划只是“结果”。你要往前推一步想想为什么优化器给出这个计划这样才能根治问题。索引不是越多越好联合索引的顺序、选择性、回表成本都要算。很多团队为了“索引覆盖”疯狂加索引最后写入变慢、存储膨胀反而得不偿失。LIMIT深分页非常伤分布式系统里提倡用 cursor 分页或异步流式拉取。定期ANALYZE TABLE特别是大批量导入、删除、更新之后。所有 SQL 上线前用EXPLAIN过一遍并把rows和实际数据量做个对比。这个习惯几乎可以帮你避开 80% 的线上慢查询事故。回到开头那句话一条 SQL 的执行过程听着是个偏原理和概念的话题但它和我们每天遇到报错、排查慢查询、设计索引、评估性能是同一件事。你能在脑子里清晰画出从连接器到存储引擎的这条流水线遇到问题时的直觉就会准得多也不会再被“一条 SQL 慢”这个笼统现象带着跑偏。希望这篇分享对你有实际帮助。