ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

一条SQL查询语句的完整执行链路与优化实践

一条SQL查询语句的完整执行链路与优化实践 很多朋友问过我同一个问题我明明就是执行了一条selectMySQL 到底在里面做了什么为什么同样的 SQL数据量一上来就慢得离谱为什么索引明明建了执行计划里却看不到这三个问题如果只看 SQL 本身永远找不到答案。我的建议是静下心把一条查询语句的完整执行链路扒一遍。这篇分享就围绕“一条 SQL 查询语句是如何执行的”展开从客户端敲下回车到服务端把结果集返回把每个环节的职责、原理、常见坑都拆开讲透。适合刚学 MySQL 的开发者、写业务代码的后端工程师以及准备数据库面试的朋友。1. 一条查询语句的完整执行链路从建立连接到返回结果这里先给一个整体地图客户端工具/应用驱动发送 SQL 文本到 MySQL Server 后SQL 会依次经过连接器、解析器、预处理、优化器、执行器最后落到 InnoDB 存储引擎身上。MySQL 8.0 里已经没有查询缓存这个环节了但 5.7 及以下版本还有我会单独拿出来说明。先把这个链路拆开看每一步都搞清楚后面分析慢查询和面试题时才能顺手拈来。1.1 连接器客户端是怎么“进门”的连接器听着不起眼却是很多生产事故的第一现场。凌晨两点 DBA 被叫醒十有八九是max_connections满了新连接进不来。连接器干的事情就是建立 TCP 连接、校验用户名和密码、读取当前账号的权限信息。这一步只做“身份认证”也就是证明“你是谁”但还没有真正去验证“你能查这张表吗”这种细粒度权限。权限的精细校验发生在后面的预处理阶段也就是 SQL 真正执行之前。这里经常有人误解以为连接成功后权限就固定了。实际上如果 DBA 在你连接期间改了你的权限MySQL 不会实时通知你新权限要等下次重新连接才生效。这也是为什么很多团队改完权限要求业务方重连不是 MySQL 偷懒而是连接器在设计上就是为了减少频繁读权限表的开销。连接器还牵扯到几个关键参数wait_timeout和interactive_timeout控制非交互连接和交互式连接的空闲超时默认 8 小时。max_connections默认 151很多云厂商给的是几百上千连接数打满时直接报Too many connections。我建议应用侧一定要用连接池比如 HikariCP池大小不要贪多通常 10 到 20 个连接足够撑起几千 QPS 的业务。每个连接在 MySQL 服务端都是一个线程加上会话级内存连接越多不代表越快反而会拖垮整个实例。1.2 解析与预处理把 SQL 变成数据库能懂的语法树连接器放行之后SQL 文本进入解析器。解析器做的事情可以类比“中文分词”先做词法分析把SELECT * FROM user WHERE id 1拆成 token比如SELECT是关键字user是表名id是列名然后做语法分析检查这些 token 组合出来的句子是否符合 MySQL 的 SQL 语法规则。如果语法不对你会看到经典的报错You have an error in your SQL syntax near ...这个报错永远是解析器抛出来的而不是优化器或执行器。解析器只负责生成语法树不关心表是否存在、列是否存在。真正做语义检查的是预处理器。预处理器拿到语法树之后会去数据字典里查表名、列名、别名顺便做权限检查。比如执行select * from not_exist_table报错Table xxx.not_exist_table doesnt exist这就是预处理器阶段发现的。这个阶段的性能开销通常很小但有一个问题值得注意SQL 文本越长、join 的表越多、子查询越复杂解析耗时会成比例上升。尤其是一些 ORM 框架生成的几百行大 SQL解析阶段就已经开始变慢。我见过一个项目用 MyBatis 拼出上万字符的 SQL解析加预处理就花了 20ms完全是浪费。日常开发里能把一条 300 行的 SQL 拆成几条简单的 SQL执行效率通常反而更高。1.3 优化器决定“怎么查最快”的大脑解析和预处理结束后MySQL 进入最核心的优化器阶段。优化器要决定这条 SQL 用哪个索引、表连接顺序是什么、子查询怎么改写、排序能不能走索引等。它不看真实数据而是基于表的统计信息估算“代价”选择它认为代价最小的执行计划。统计信息里最关键的是索引基数 cardinality也就是索引列上有多少个不同值。MySQL 通过采样来估算这个值并不是实时精确的。如果数据变化剧烈但统计信息还没更新优化器就可能做出错误判断明明有索引却选择全表扫描或者选错索引。解决方法很简单执行ANALYZE TABLE user_order;手动更新统计信息。很多经验不足的同学一遇到慢查询就FORCE INDEX我建议先跑一下ANALYZE TABLE也许问题就解决了。优化器还会考虑回表代价。举个例子SELECT * FROM user WHERE name 张三如果name索引选择性很差比如 100 万行里有 20 万行叫“张三”那么通过索引找到 20 万个主键 id再回表 20 万次代价非常高。这种时候优化器宁可选择全表扫描因为全表扫描是顺序读而每次回表是随机读随机读的成本远高于顺序读。这个道理明白了也就明白了为什么“明明有索引却不用”不一定是指标坏了有时候是优化器的理性选择。另一个容易被忽略的是索引下推。MySQL 5.6 之后引入的 Index Condition Pushdown可以把WHERE中部分条件判断下推到存储引擎层在索引遍历过程中直接过滤减少回表次数。比如联合索引(a, b)查询WHERE a 100 AND b 1没有 ICP 时InnoDB 会把所有a 100的记录都回表再用b条件过滤有 ICP 时InnoDB 在引擎层就用b 1过滤索引项回表数量骤降。EXPLAIN中 Extra 列出现Using index condition就代表 ICP 生效了。1.4 执行器与存储引擎最后一段路如何落地优化器生成执行计划之后执行器开始真正干活。执行器会向存储引擎要数据。以全表扫描为例执行器调用 InnoDB 的接口读取第一行判断WHERE条件是否满足满足就放入结果集不满足就跳过然后继续读下一行直到读完为止。如果走索引InnoDB 会沿着 B 树找到第一条满足条件的记录再通过叶子节点上的链表顺序读取后续记录。这里有一个很多人混淆的点MySQL 的 Server 层和存储引擎层是分离的。执行器在 Server 层负责调用引擎接口、过滤条件、计算表达式、排序、分组等存储引擎层负责真正读写磁盘数据、维护索引、处理事务锁。EXPLAIN里看到的Using where通常意味着 Server 层还需要对引擎返回的记录做条件过滤Using filesort意味着 Server 层需要额外做排序而不是依赖索引顺序。InnoDB 在这条链路里还有一道重要缓存Buffer Pool。如果数据页已经在内存里存储引擎直接返回数据如果不在则需要从磁盘读页到 Buffer Pool。所以同样的 SQL第一次执行可能几十毫秒第二次执行可能不到一毫秒就是因为数据页被缓存了。查询语句本身不会产生 redo log 或 binlog因为它是只读的但查询过程中读取到的数据版本是由 undo log 和 MVCC 机制来保证一致性的这部分在讲事务时再展开。1.5 查询缓存为什么从 MySQL 8.0 开始被移除如果是 MySQL 5.7 及以下版本SQL 在执行前还会先经过查询缓存。它的逻辑很简单以 SQL 文本作为 key把查询结果缓存起来下次执行同样的 SQL 时直接返回缓存结果跳过解析、优化、执行全流程。听上去很美好实际却是鸡肋。因为只要涉及的表有任何一条数据被更新这张表的所有查询缓存都会被清空。对于写多读少的业务缓存刚生成就被失效命中率极低对于读多写少的业务每次写入清理缓存的成本也很高。加上查询缓存需要额外加锁保护并发稍高反而成为瓶颈。MySQL 8.0 直接把这个组件删了也说明官方彻底放弃了这条路。现在你在 8.0 里执行SHOW VARIABLES LIKE query_cache%是查不到任何结果的。如果你还在用 5.7建议直接关闭查询缓存SET GLOBAL query_cache_type OFF;。不要指望它帮你提速把 Buffer Pool 调大、索引建对比什么缓存都靠谱。2. 实操让一条真实 SQL “开口说话”理论讲完必须上手跑一遍。我本地用的是 MySQL 8.0 版本准备一张简单的订单表插入十几万条数据用EXPLAIN、profiling、慢查询日志把这套链路量化出来。这些操作你在自己电脑上也能复现不需要特别高配置普通笔记本就能跑。2.1 准备测试环境建表、造数据、装好 MySQL先建一张用户订单表字段不要太复杂够演示就行。CREATE TABLE user_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_no VARCHAR(50) NOT NULL, amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL, KEY idx_user_id (user_id), KEY idx_order_no (order_no), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;接下来用存储过程批量插入 20 万行数据。如果你是在自己机器上实验10 万行足够看出效果。这里顺便提一句MySQL 默认不允许存储函数里写带RAND()的 SQL会报错需要用SET GLOBAL log_bin_trust_function_creators 1;放行。这是新人最容易踩的坑我当年第一次跑类似脚本被这个报错卡了半小时。DELIMITER $$ CREATE PROCEDURE insert_test_data() BEGIN DECLARE i INT DEFAULT 1; WHILE i 200000 DO INSERT INTO user_order (user_id, order_no, amount, status, create_time) VALUES ( FLOOR(RAND() * 10000), CONCAT(NO_, LPAD(i, 10, 0)), RAND() * 1000, FLOOR(RAND() * 3), DATE_ADD(2024-01-01, INTERVAL FLOOR(RAND() * 365) DAY) ); SET i i 1; END WHILE; END$$ DELIMITER ; CALL insert_test_data();造完数据后我习惯先跑ANALYZE TABLE user_order;更新统计信息。这一步很关键要不然优化器拿到的可能是刚建表时的采样数据。2.2 EXPLAIN读懂优化器的执行计划EXPLAIN是分析查询语句执行流程的最直接工具。执行EXPLAIN SELECT * FROM user_order WHERE user_id 9527;输出里会有这样几列关键信息列名值说明typeref使用非唯一索引等值匹配possible_keysidx_user_id优化器认为可能用到的索引keyidx_user_id最终选中的索引rows约 20预估扫描行数Extra空无明显附加操作type是执行计划里最值得关注的字段从好到差大致是system、const、eq_ref、ref、range、index、ALL。system和const意味着通过主键或唯一索引精确匹配单行ref是普通索引等值匹配range是索引范围扫描index是全索引扫描比全表稍好ALL就是全表扫描通常意味着这条 SQL 有大问题。再看一个稍微复杂的例子EXPLAIN SELECT * FROM user_order WHERE status 1 ORDER BY create_time DESC LIMIT 20;由于status没有索引执行计划里type是ALLExtra里大概率出现Using filesort。这意味着存储引擎全表扫描 20 万行Server 层再对这 20 万行排序。很多人以为 MySQL 的filesort是磁盘排序其实在数据量不大时是内存排序但不管怎么样扫描 20 万行再排序性能都不会好。这说明一个重要的索引设计原则ORDER BY字段尽量与WHERE条件字段组成联合索引。比如这张表经常按status筛选、按create_time排序就应该建一个(status, create_time)联合索引。建完索引后再看EXPLAINtype会变成refExtra里的Using filesort也会消失。还有一个非常实用的技巧EXPLAIN里key_len可以用来判断联合索引实际用了几个字段。比如索引(user_id, create_time)user_id是INT在 MySQL 里是 4 字节如果列允许为空则要再加 1 字节所以key_len至少是 5。如果key_len只有 5说明优化器只用到了user_id这一列create_time那部分没有参与索引查找。这个细节在排查联合索引失效时非常有用。2.3 profiling 与慢查询日志量化每个阶段耗时EXPLAIN告诉我们执行计划但从每个阶段耗时来看还需要 profiling。MySQL 8.0 中SHOW PROFILE已经标记为 deprecated不过很多老版本仍然能用。做法也很简单SET profiling 1; SELECT * FROM user_order WHERE user_id 9527; SHOW PROFILES; SHOW PROFILE CPU, BLOCK IO FOR QUERY 1;SHOW PROFILES会列出最近执行过 SQL 的总耗时SHOW PROFILE可以看到这条 SQL 在不同状态下的耗时分布比如starting、Executing hook on plugin、Sending data等。很多新人对Sending data这个名字有误解以为纯指网络传输其实这个阶段包含了从存储引擎读取数据、Server 层过滤和拼接结果集的过程。在大量回表或全表扫描时Sending data的耗时往往是最高的。如果你用的 MySQL 8.0官方更推荐查performance_schema或sys库。比如sys.statement_analysis视图会按 SQL 模板聚合出平均耗时、扫描行数、排序次数等指标用起来非常直观。开启慢查询日志是另一条重要的观测路径SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 0;把long_query_time设为 0意味着所有 SQL 都会记录到慢日志里生产环境一般设 1 秒以上。慢日志里的每一行都包含执行时间、锁等待时间、扫描行数、返回行数。结合这些信息就能判断一条查询语句到底慢在“扫描太多行”还是“锁等待太久”还是“排序太慢”。我自己的排查习惯是先EXPLAIN看执行计划再用sys.statement_analysis看统计趋势最后针对单条 SQL 打开optimizer_trace。optimizer_trace是优化器的心跳记录能告诉你它为什么选择这个索引、为什么认为这个计划代价最低SET optimizer_trace enabledon; SELECT * FROM user_order WHERE user_id 9527; SELECT * FROM information_schema.optimizer_trace\G很多时候你以为优化器疯了看完 trace 才发现它计算出来的代价确实就是这样。这时候你不是去怪优化器而是要想办法让代价模型更准确比如更新统计信息、调整索引结构甚至在必要时用FORCE INDEX。3. 执行链路中的常见问题与排查实录理解了正常链路就等于拿到了排查异常的工具。这一节我把平时被问得最多、热搜也最集中的几类问题整理出来每个都配上排查思路和实际案例争取你遇到相似场景时能直接照着操作。3.1 慢 SQL 优化为什么有索引却不用“有索引却不用”是慢 SQL 问题里最经典的一种。我见过很多新人第一反应就是FORCE INDEX但更多时候问题的根源在 SQL 写法上。常见场景有四个第一隐式类型转换。比如order_no列是VARCHAR查询写WHERE order_no 12345MySQL 会把列值转成数字导致索引失效。解决方法是保证参数类型与列类型一致。第二对索引列使用函数。比如WHERE DATE(create_time) 2024-01-01此时create_time索引失效正确写法是范围查询WHERE create_time 2024-01-01 00:00:00 AND create_time 2024-01-02 00:00:00。第三前导模糊查询。LIKE %abc无法使用索引但LIKE abc%可以。第四OR条件中部分列没有索引优化器可能放弃整条索引路径改成全表扫描。UNION往往比OR更利于索引。大分页问题也值得单独说。LIMIT 100000, 20这种写法MySQL 需要先扫描前 100020 行再丢弃前 100000 行代价很高。优化思路是改成“先查主键再回表”先SELECT id FROM user_order WHERE ... ORDER BY id LIMIT 100000, 20拿到 20 个主键后再用JOIN或WHERE id IN (...)去取完整行。实测下来数据量越大这种改写越明显。3.2 锁与事务查询卡住的幕后黑手有时候一条查询语句本身很简单但就是执行不过去大概率是卡在锁等待。先看锁的分类我整理了一张简表锁类型作用范围典型场景全局锁整个实例FLUSH TABLES WITH READ LOCK全库只读表级锁整张表MDL 锁、LOCK TABLES行锁索引记录UPDATE、DELETEInnoDB 事务中生效间隙锁索引记录之间的区间RR 隔离级别下防止幻读意向锁表级标记行锁与表锁的协同最常见的“查询卡住”其实是 MDL 锁。MySQL 5.6 之后执行 DDL 语句比如ALTER TABLE需要拿 MDL 写锁而已经存在的查询持有 MDL 读锁两边互相等待。更麻烦的是一个长事务一直不提交DDL 就排在后面后续所有查询全部被阻塞。遇到这种情况先查information_schema.innodb_trx看有没有长时间未提交的事务再决定是KILL这个事务还是调整 DDL 的执行窗口。生产环境建议用pt-online-schema-change或gh-ost这类工具来做在线 DDL避免长时间锁表。行锁方面InnoDB 的行锁是建立在索引上的。如果UPDATE的WHERE条件没有索引InnoDB 找不到目标记录只能锁全表。所以“给更新语句的WHERE列加索引”不只是性能优化更是锁粒度优化。排查锁等待可以查sys.innodb_lock_waits它会直接告诉你哪个事务阻塞了哪个事务以及阻塞了多久。innodb_lock_wait_timeout默认 50 秒超过就报ERROR 1205。实际调优时不要盲目调高这个参数更关键的是把事情做对事务要短、索引要准、批量更新要分批。3.3 几个常踩的 SQL 写法坑IN、去重与 NULLIN查询报错是搜索热词但“报错”和“变慢”是两回事。真正的执行流程中IN本身是可以走索引的问题通常出在类型不一致或数据量过大。比如列user_id是INT却传入IN (1, 2)在某些版本里优化器可能放弃索引扫描。更隐蔽的情况是NOT IN子查询中包含了NULL这个经典坑极其推荐大家记住只要子查询结果里包含NULLNOT IN的最终结果一定是空集因为它等价于“不等于NULL”而任何值与NULL比较都是未知。改成NOT EXISTS就安全了。去重查询也是高频热搜。DISTINCT与GROUP BY本质区别不大依赖临时表或排序。真正的问题是很多项目习惯直接SELECT DISTINCT *把一整行的所有列都拿去比较去重。完全没有必要应该只SELECT DISTINCT需要去重的列。如果只是某列基数很低比如状态值只有几个那么建一个二级索引利用索引本身的有序性就能更快速去重。另外COUNT(DISTINCT col)在数据量大时也慢日常统计可以改用近似函数或者离线数仓。空值处理方面NULL不等于空字符串。WHERE col 查不到NULL的列统计COUNT(*)会包含NULLCOUNT(col)不会IFNULL(col, 0)可以做兜底。如果表中来源复杂空值既可能是NULL又是空字符串建议清洗数据的时候统一规则否则每条查询都要写WHERE col IS NOT NULL AND col 又丑又慢。3.4 SQL 注入万能密码是如何绕过执行流程的SQL 注入之所以能得逞问题出在“SQL 语句的拼接时机”上。拿经典万能密码来说用户输入 OR 11 --如果代码把输入直接拼进 SQLSELECT * FROM user WHERE username admin AND password OR 11 -- 这里OR 11恒真--把后面的引号注释掉整个条件直接被绕过。从执行链路角度看用户输入变成了 SQL 语法的一部分解析器会忠实地按这段文本生成语法树之后一切优化和执行都在正常走但逻辑已经错了。解决办法不是靠过滤关键词而是从根本上把“数据”和“代码”分开。使用预编译语句PreparedStatementSQL 模板先被解析一次参数通过占位符传入MySQL 把参数当作纯粹的数据不会再参与语法解析。ORM 框架大多默认使用参数绑定但如果你习惯自己拼 SQL 字符串特别是ORDER BY后面的字段名、表名这种不能参数化的位置一定要做白名单校验否则还是会有注入风险。排查线上问题时可以用慢日志看有没有异常的OR 11或UNION SELECT等特征语句。3.5 从 MySQL 到 ClickHouse 的同步一条查询背后的数据流转既然聊到执行链路就不能不提 binlog。SELECT不写 binlog但所有能让查询结果发生变化的写入操作都会在事务提交时记录 binlog。binlog 是 MySQL Server 层的二进制日志记录了“数据变成了什么样”。因此把一个 MySQL 实例的数据实时同步到 ClickHouse业界标准做法就是基于 binlog 做 CDCChange Data Capture。Flink CDC 是目前最常用的方案之一。它的 MySqlSource 会伪装成一个 MySQL 从节点向主节点请求 binlog然后解析、清洗、写入 ClickHouse。使用时需要保证 MySQL 开了log_bin、binlog_format ROW、binlog_row_image FULL并且为每个同步任务设置唯一且不等于主库 server_id 的server_id。这里有个经典坑多个同步任务如果server_id相同主库会误判为同一个从节点把后连接的节点踢掉导致同步中断。排查这类问题看 MySQL 错误日志里的Got fatal error 1236就有线索。不只是 Flink CDCCanal 也是同样的思路伪装成从库拉取 binlog再投递到消息队列或 ClickHouse。理解这条链路你其实就理解了“一条 SQL 执行后它的影响如何传递到其他系统”这对做实时数仓、缓存同步、搜索索引同步都很有帮助。3.6 部署经验从 Windows 到 Docker 的那些坑如果连 MySQL 都装不起来前面说的执行流程全都没法验证。这里把我在 Windows 和 Docker 上安装 MySQL 的踩坑合集写一下方便你快速搭环境。Windows 10 上安装 MySQL 8.x最省事的办法是下载官方 zip 包解压后创建my.ini写上basedir和datadir再用管理员权限执行mysqld --initialize-insecure初始化数据目录最后注册 Windows 服务并启动。最容易踩的坑是目录名里带中文或空格导致初始化时报错另一个是没装 VC 运行库mysqld启动闪退。Docker 安装 MySQL 失败的常见原因则集中在端口被占用、容器名冲突、数据目录权限不足。建议用docker logs 容器名看具体日志大部分问题都能在这里找到答案。MySQL 5.7.44 和 8.4 LTS 这些版本我都有实测过。5.7.44 相比 5.7.43 主要是安全修复安装流程不变8.4 默认的认证插件是caching_sha2_password老版本 Navicat 或旧 JDBC 驱动会连不上更新驱动或把用户改成mysql_native_password就能解决。安装配置的重点永远是my.ini里的字符集、时区和 innodb_buffer_pool_size这三项前期不配好后面数据多了再改成本极高。4. 把这条链路“变现”面试里能引出哪些考点最后把这个主题放到面试场景里看看。很多后端岗位面试都会问数据库基础知识而“一条 SQL 是怎么执行的”就是一个绝佳的母题能够轻松引出一连串问题。把这条链路真正理解透面试时就不需要死记硬背了。4.1 执行流程连环问第一个问题通常就是“描述一条查询语句的执行过程”。标准答案在 MySQL 8.0 里是连接器认证身份 → 解析器做词法和语法分析 → 预处理器检查语义和权限 → 优化器生成执行计划 → 执行器调用存储引擎接口返回结果。如果答到这一步面试官大概率会追问连接器校验的是什么东西解析和预处理的区别是什么优化器到底优化了哪些东西这些细节在第一章里都有答案核心是要把“Server 层做逻辑处理存储引擎层做物理存储和索引操作”这句话反复强调。第二个高频追问是“为什么 MySQL 8.0 删掉了查询缓存”。如果从缓存失效机制、并发控制成本、命中率低三个角度来回答基本就能过关。这题考察的不只是记忆而是你是否理解缓存组件的适用边界。第三个问题往往围绕索引“为什么 InnoDB 用 B 树而不是 B 树或哈希”。B 树只有叶子节点存数据非叶子节点能存更多索引项三层就能容纳千万级数据叶子节点用链表串联天然支持范围查询每个节点大小对应一个数据页减少磁盘 I/O。哈希索引只适合等值查询范围查询退化严重。这个问题的答案本质上也在解释执行器为什么能沿着索引快速定位数据。4.2 索引、日志与事务的追问再往下面试官会把问题延伸到 SQL 执行背后的“保障机制”。比如回表和覆盖索引有什么区别回表是拿到主键后再去聚簇索引查完整行记录覆盖索引则是在二级索引中已经拿到了查询所需的全部列EXPLAIN的Extra列显示Using index。什么是索引下推就是存储引擎在索引遍历过程中提前过滤部分条件减少回表次数Using index condition对应的就是它。日志三兄弟也是重点redo log 负责崩溃恢复是物理日志记录“数据页改成了什么”undo log 负责事务回滚和 MVCC记录数据版本链binlog 负责主从复制和同步是逻辑日志记录 SQL 或行变更。redo log 是 InnoDB 引擎层的binlog 是 Server 层的两者在事务提交时通过两阶段提交保证一致性。这个“两阶段提交”的考点本质上是 MySQL 在主从环境下避免崩溃恢复时数据不一致的关键设计。MVCC 的考点也可以从这里接下去每条记录隐藏了trx_id和roll_pointerSELECT时根据 ReadView 对照可见版本。RC 隔离级别下每条语句生成新的 ReadViewRR 隔离级别下整个事务复用同一个 ReadView所以可重复读。这就是“为什么普通SELECT不会被其他事务的UPDATE阻塞”的底层原因也是前面锁问题的最佳补充。我个人带新人的经验是每次遇到慢查询不要急着加索引先跑一遍EXPLAIN再看ANALYZE TABLE后的统计信息最后必要时开optimizer_trace看优化器决策。这条执行链路理解透了你会发现很多数据库问题都变成了连锁反应索引写错了执行计划就偏了执行计划偏了扫描行数多了扫描行数多了锁持有时间长了锁冲突多了整个实例就卡了。排查问题时顺着链路找基本不会有盲区。下一篇如果有机会可以继续聊聊优化器源码层面的实现或者从 binlog 出发把主从复制和 CDC 同步的整套玩法讲透。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进