MySQL中执行一条 SQL 查询语句的底层逻辑?
最近研究了MySQL的底层查阅了很多技术文档…
用通俗的话术简而易懂的给大家分享些有趣的东西,当然有理解不对的地方也感谢大家指出。
…
我们平时用 MySQL,总觉得它像个 “黑盒子”—— 输入一条select * from emp where ID=10;,就拿到了结果,却不知道这条语句在里面经历了什么。
其实要搞懂 MySQL并不难,不能一上来就扎进细节,得先 “鸟瞰” 它的整体架构。就像修电脑前要知道主板、CPU、内存的分工一样,理解 MySQL 的分层和组件,才能在遇到问题时直戳本质,比如慢查询、连接超时这些常见故障,都能从架构层面找到根因。
MySQL 的 “两层架构”
MySQL 整体分为Server 层和存储引擎层,两层各司其职,又协同工作。
你可以把它想象成一家餐厅:Server 层是前台(点单、确认需求、安排出餐),存储引擎层是后厨(做菜、存食材)—— 不管后厨用什么厨具(不同存储引擎),前台的服务流程是统一的。
1. Server 层
Server 层是 MySQL 的核心,涵盖了大多数核心服务功能,所有跨存储引擎的能力都在这里实现。它包含 5 个关键组件:
- 连接器:负责接待 客户端,建立连接、验证身份;
- 查询缓存:暂存之前的查询结果,能直接返回就不用 查询;
- 分析器: 理清需求,确认 “要查什么表、什么字段”;
- 优化器:制定 最优方案,比如选哪个索引、先查哪个表;
- 执行器:按方案执行,调用存储引擎的接口拿结果。
除此之外,Server 层还包含所有内置函数(比如DATE()、SUM()),以及存储过程、触发器、视图这些跨引擎功能 —— 不管你用 InnoDB 还是 MyISAM,这些功能的逻辑都是一样的。
2. 存储引擎层
存储引擎层只负责一件事:数据的存储和提取。它采用 “插件式架构”,你可以为不同的表选择不同的引擎,比如:
- InnoDB:现在的默认引擎(MySQL 5.5.5 起),支持事务、行锁,适合大多数业务;
- MyISAM:老引擎,不支持事务,适合只读场景(比如日志表);
- Memory:数据存内存,速度快但重启丢失,适合临时缓存数据。
你建表时如果不指定引擎,默认就是 InnoDB;要换引擎也简单,加个engine参数就行:
create table emp (ID int primary key, c int) engine=MyISAM; -- 指定MyISAM引擎
关键是:所有存储引擎共用一个 Server 层。不管你用 InnoDB 还是 Memory,一条 SQL 都会先经过连接器、分析器等组件,最后才到存储引擎层拿数据。
以上是MySQL的基本架构,那么一条查询SQL的执行会是怎么样的呢?
拆解流程:一条select * from emp where ID=10的执行流程
我们就以这条最简单的查询语句为例,一步步看它如何从输入到输出,经过 Server 层的每个组件。
…
这里我们把它看作“SQL闯关”,一共分为五个关卡:
第一关:连接器 —— 建立连接,确认 “身份”
第二关:查询缓存 ——“有没有现成的结果?”
第三关:分析器 ——“搞清楚要做什么”
第四关:优化器 ——“选最优的方案”
第五关:执行器 ——“按方案执行,拿结果”
…
第一关:连接器 —— 建立连接,确认 “身份”
要查数据,第一步—— 客户端和 MySQL 服务端建立连接,这个过程由连接器负责,就像餐厅前台确认客人身份、登记信息。
1. 怎么建立连接?
你在命令行输入的连接命令,就是给连接器发 “进门申请”:
mysql -h$ip -P$port -u$user -p
mysql:客户端工具,负责和服务端通信;- 输入命令后,会先完成 TCP 三次握手(和网络连接的基础流程一致);
- 接着输入密码,连接器会去 MySQL 的权限表(比如
mysql.user)验证用户名和密码。
2. 身份验证的 “坑”
- 如果用户名 / 密码错,会直接返回
Access denied for user错误,客户端直接退出; - 如果验证通过,连接器会把这个用户的权限(比如对表 T 有没有 SELECT 权限)“记下来”—— 注意:后续这个连接的所有操作,都用这次查到的权限。哪怕你中途用管理员账号改了这个用户的权限,已建立的连接也不受影响,只有新连接才会用新权限。
3. 连接后的 “闲置与断开”
连接建立后,如果没发 SQL,连接就会处于 “空闲状态”,你用show processlist就能看到,Command 列会显示Sleep:
mysql> show processlist;
+----+------+-----------+------+---------+------+-------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------+------+---------+------+-------+------------------+
| 2 | root | localhost | test | Sleep | 120 | | NULL |
+----+------+-----------+------+---------+------+-------+------------------+
- 空闲连接不会一直挂着,MySQL 用
wait_timeout参数控制超时时间,默认是 8 小时; - 超时后连接器会主动断开连接,这时再发 SQL,就会收到
Lost connection to MySQL server during query,得重新连接。
4. 长连接 vs 短连接:该怎么选?
- 短连接:每次查完就断开,下次查再重新连 —— 缺点是建立连接的过程(TCP 握手 + 权限验证)很耗时,要是每秒有上千次查询,连接开销会拖慢整体速度;
- 长连接:连接建立后一直用,后续查询复用这个连接 —— 优点是省掉连接开销,但有个问题:MySQL 会把连接过程中产生的临时内存(比如结果集缓存)存在连接对象里,只有断开连接才释放。如果长连接太多,内存会越用越多,甚至触发 OOM(内存溢出),导致 MySQL 异常重启。
解决长连接内存问题的两个方案:
- 定期断开长连接:比如程序里每执行 1000 次查询,就主动断开连接,下次查询再重连;
- 用 MySQL 5.7 + 的
mysql_reset_connection:执行完大查询(比如查 10 万行数据)后,调用这个命令重置连接 —— 不用重连,也不用重新验证权限,能清空连接里的临时内存,恢复到刚建立连接的状态。
第二关:查询缓存 ——“有没有现成的结果?”
连接建立后,MySQL 会先去查询缓存里 “翻一翻”:之前有没有执行过一模一样的 SQL?
查询缓存的逻辑很简单:以 “SQL 语句” 为 key,“查询结果” 为 value,存在内存里。如果这次的 SQL 能匹配到 key,就直接把 value 返回给客户端,不用走后面的分析、优化流程 —— 理论上很快。
但我几乎不建议用查询缓存,因为它弊大于利:
- 失效太频繁:只要这个表有更新(比如
update T set c=5 where ID=10),这个表上所有的查询缓存都会被清空。你刚缓存了 “查 ID=10” 的结果,一个更新过来,缓存就没了,命中率极低; - 只适合静态表:只有那种很久不更新的表(比如系统配置表,一个月更一次),用查询缓存才划算。
怎么 “按需使用” 查询缓存?
MySQL 提供了开关:把query_cache_type参数设为DEMAND,默认所有 SQL 都不查缓存;只有你明确加了SQL_CACHE关键字的 SQL,才会用缓存:
-- 明确使用查询缓存
mysql> select SQL_CACHE * from T where ID=10;
更彻底的是:MySQL 8.0 直接删掉了查询缓存功能。官方也意识到,在大多数更新频繁的业务里,查询缓存是个 “鸡肋”,不如直接去掉,减少维护成本。
第三关:分析器 ——“搞清楚要做什么”
如果没命中查询缓存,就该 “理清需求” 了 ——分析器负责把 SQL 语句 “翻译” 成 MySQL 能懂的指令,分两步:
1. 词法分析:拆分 “关键词”
SQL 是字符串,分析器先把字符串拆成有意义的 “零件”:
- 看到
select,知道这是查询语句; - 看到
emp,知道要查的表是emp; - 看到
ID,知道要查的列是ID; - 看到
where ID=10,知道过滤条件是ID=10。
就像你说 “我要一份番茄炒蛋”,服务员先拆成 “要 / 番茄炒蛋 / 一份”,明确需求。
2. 语法分析:检查 “语法对不对”
拆完零件后,分析器会根据 MySQL 的语法规则,检查语句有没有错:
- 比如少写了
select的t,写成selec * from T where ID=10; - 或者多写了逗号,比如
select ID, c, from T where ID=10。
如果有错,会直接返回You have an error in your SQL syntax错误,并且提示错误位置 —— 比如上面的elect错误,会告诉你 “near ‘elect * from t where ID=1’”,意思是 “从‘elect’这里开始错了”:
mysql> elect * from T where ID=10;
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 'elect * from T where ID=10' at line 1
那么这一步的核心是:确认 SQL 的 “语法正确” 和 “意图明确”,如果过不了,后续流程就不用走了。
第四关:优化器 ——“选最优的方案”
分析器知道了 “要做什么”,接下来优化器要决定 “怎么做最快”—— 比如选哪个索引,或者多表连接时先查哪个表。
举个例子:如果执行select * from e1 join e2 using(ID) where e1.p=10 and e2.d=20,有两种执行方案:
- 先查 e1 里
p=10的记录,拿到对应的 ID,再用 ID 去 e2 里查d=20的记录; - 先查 e2 里
d=20的记录,拿到对应的 ID,再用 ID 去 e1 里查p=10的记录。
这两种方案的结果完全一样,但效率可能差很多 —— 比如 t1 的c=10只有 10 条记录,t2 的d=20有 1000 条记录,显然方案 1 更快。优化器的作用就是 “算” 出哪个方案成本更低,然后确定执行计划。
再比如我们的例子select * from T where ID=10:如果表 T 的 ID 列有索引,优化器会选择走索引查(只查 1 行);如果没索引,就只能全表扫(查所有行)—— 这个 “选择” 就是优化器做的。
优化器的决策依据是 MySQL 的 “成本模型”,比如扫描行数、索引选择性等(后面会专门讲优化器的逻辑)。确定执行计划后,就该执行器上场了。
第五关:执行器 ——“按方案执行,拿结果”
优化器制定好计划,执行器就负责 “落地执行”,核心是 “调用存储引擎接口,拿数据”,但执行前还有个关键步骤:权限验证。
1. 先验权:“有没有资格查?”
执行器会先检查:当前用户对表 T 有没有SELECT权限?如果没有,直接返回权限错误:
mysql> select * from emp where ID=10;
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'
(补充:如果命中查询缓存,权限验证会在返回缓存结果前做,避免没权限的用户拿到数据)
2. 调用引擎接口:“按计划拿数据”
如果有权限,执行器会根据表的存储引擎(比如 InnoDB),调用对应的接口拿数据。分两种情况:
情况 1:ID 列没有索引
执行器的流程是:
- 调用 InnoDB 的 “取第一行” 接口,拿到一行数据;
- 判断这行的 ID 是不是 10:如果是,就加入结果集;如果不是,跳过;
- 调用 InnoDB 的 “取下一行” 接口,重复步骤 2,直到拿到表的最后一行;
- 把结果集返回给客户端。
情况 2:ID 列有索引
执行器的流程会更高效:
- 调用 InnoDB 的 “取 ID=10 的第一行” 接口(通过索引直接定位,不用扫全表);
- 因为 ID 是主键,只有一行数据,直接加入结果集;
- 调用 “取下一行” 接口时,InnoDB 会返回 “没有更多行”,流程结束;
- 返回结果集给客户端。
一个关键指标:rows_examined
执行器在调用引擎接口时,会累加 “扫描过的行数”,这个值就是慢查询日志里的rows_examined—— 它能帮你判断查询是不是 “扫了太多行”(比如没走索引导致全表扫,rows_examined会很大)。
注意:rows_examined是执行器 “调用引擎的次数”,不是引擎实际扫描的行数。比如 InnoDB 在查索引时,可能会先扫几行才找到目标,但执行器只调用一次接口,这时rows_examined是 1,引擎实际扫描行数可能更多。
小结:一条 SQL 的 “闯关路线”
再回顾一下select * from T where ID=10的完整流程:
- 连接器:建立连接,验证身份,记录权限;
- 查询缓存:查有没有现成结果,没有就继续;
- 分析器:词法 + 语法分析,确认需求;
- 优化器:选索引、定执行计划;
- 执行器:验权限,调用引擎接口拿数据,返回结果。
这整个流程的核心是 MySQL 的 “分层架构”——Server 层统一处理逻辑,存储引擎层专注数据存储,这种分工让 MySQL 灵活又高效。

1911

被折叠的 条评论
为什么被折叠?



