首页 开发教程 字节真实面经:以Mysql为例,讲一下一条SQL的执行过程和原理!

字节真实面经:以Mysql为例,讲一下一条SQL的执行过程和原理!

开发教程 2025年12月4日
996 浏览

一条 SQL 的奇幻漂流:从敲下回车到结果返回,MySQL 到底经历了什么?

这恐怕是很多后端同学第一次被“SQL 执行流程”拷问灵魂的瞬间 。
明明平时写 SQL 写得飞起,什么 SELECT * FROM user WHERE id = 1; 手到擒来,
可真让你说清楚它从 「回车键」「结果返回」 的全过程,
十有八九都是一脸懵圈。

今天我们就来当一回数据库侦探 ‍,
跟着这条 SQL 一起,深入 MySQL 的地底世界。
顺便也看看它的老对手 PostgreSQL 是怎么干这事的。


一条 SQL 的旅程,从客户端到结果集

假设我们执行这样一句 SQL:

SELECT name, age FROM users WHERE city = \'Shanghai\' ORDER BY age DESC LIMIT 10;

这句话从敲下回车的那一刻起,其实会经历 「七个阶段」

别急,我们一个一个扒开看。


1. 连接管理器(Connection Manager)

MySQL 是个典型的 「客户端/服务器架构」。 当你通过客户端(比如 Navicat、Go 的 database/sql)发起连接时, MySQL 会先进入 「连接管理阶段」

「MySQL」 使用线程池模型,一个连接对应一个线程。 「PostgreSQL」 传统上是一个连接一个进程(也因此多连接下开销更大)。

连接建立好后,MySQL 会先验证账号密码、权限等。 连接成功后,你就能愉快地发送 SQL 了。

小知识点: 「MySQL 8.0 引入了连接池 plugin,可以减少线程开销。」 而 PostgreSQL 则常用第三方连接池(如 pgBouncer)来减压。


2. 查询缓存(Query Cache)——曾经的“短命天才”

MySQL 曾经有一个 「Query Cache」, 如果你发的 SQL 在缓存里有一模一样的结果,它直接返回 。

问题是,只要表中有一行数据被更新, 相关缓存就全部失效 。 高并发场景下,这反而拖慢了性能。

于是从 MySQL 8.0 开始,「Query Cache 被彻底移除」。 而 PostgreSQL 则一直没有内建 Query Cache, 因为它更倾向于用 「共享缓冲区 + 执行计划缓存」 来提速。


3. 解析器(Parser)——把 SQL 拆成语法树

解析器的工作就像语文老师批作文。 它要判断这句话语法对不对, 然后把 SQL 拆成一个内部可理解的结构:「抽象语法树(AST)」

举个例子 我们的 SQL:

SELECT name, age FROM users WHERE city = \'Shanghai\' ORDER BY age DESC LIMIT 10;

会被解析成一棵树结构(简化版):

Select
 ├── Columns: [name, age]
 ├── From: users
 ├── Where: city = \'Shanghai\'
 ├── OrderBy: age DESC
 └── Limit: 10

PostgreSQL 同理,也会生成 AST,但它在此阶段就做了更多语义检查(比如字段是否存在)。


4. 优化器(Optimizer)——SQL 的灵魂工程师

到这里是重头戏 。 优化器的任务是:

比如上面的查询,优化器要考虑:

  • 要不要用索引?哪个索引?
  • 是先过滤 city 还是先排序?
  • 是否可以走覆盖索引?
  • 是否能用回表、临时表、文件排序?

MySQL 使用 「基于代价的优化器(Cost-Based Optimizer)」。 它会估算每种执行计划的代价(I/O、CPU、行数等),然后选最优。

而 PostgreSQL 则以优化器强悍著称: 它支持更多的 join 策略(如 Hash Join、Merge Join) 还能利用统计信息动态决定 plan。

小技巧: 执行前可以通过 EXPLAIN 看执行计划。 比如:

EXPLAIN SELECT name, age FROM users WHERE city = \'Shanghai\' ORDER BY age DESC LIMIT 10;

这会告诉你是否使用了索引、扫描方式(全表/索引)、排序代价等。

「那怎么判断是否使用了索引?」

执行结果一般会有一列叫 type 和一列叫 key

字段 含义
type 表示访问类型(性能从好到坏:system > const > eq_ref > ref > range > index > ALL)
key 表示使用的索引名,如果是 NULL 则说明没用索引

例子一:

type: range, key: idx_city

说明走了索引扫描(利用了 city 索引)

例子二:

type: ALL, key: NULL

说明是全表扫描(MySQL 从头到尾扫一遍表的数据页)

一般来说,全表扫描 (ALL) 性能最差,尤其是大表上。 而索引扫描(range / ref)表示查询条件命中索引,效率更高。 可以用 EXPLAIN ANALYZE(MySQL 8.0.18+)进一步看到真实执行时间。


️ 5. 执行器(Executor)——执行计划落地的地方

优化器决定了“怎么干”,执行器就负责“真干”。 它会按照优化器的 plan,调用底层的存储引擎 API。

比如说它要从表里取数据,就会调用:

engine.read(\"users\", condition)

(当然底层不是真的 Go 哈,这只是方便理解 )

每读一行,就判断 city == \'Shanghai\', 符合条件的就放进结果集,最后排序、截取前 10 条。


️ 6. 存储引擎层(Storage Engine Layer)

这是 MySQL 最有“灵魂分裂感”的部分 。 因为不同表,可以有不同的存储引擎!

最常见的当然是 「InnoDB」

InnoDB 负责的数据层逻辑包括:

  • 数据页缓存(Buffer Pool)
  • 索引(B+ Tree)
  • 事务(Redo/Undo 日志)
  • 行锁机制

而 PostgreSQL 没有“存储引擎”这个概念, 它的存储层和执行层是紧耦合的。 但它在 「MVCC(多版本并发控制)」 上比 MySQL 更纯粹:每次更新会产生新版本,而不是回滚段。


7. 返回结果(Result Set)

当执行器拿到结果集后, 会通过网络协议一批一批地返回给客户端。

你以为这一步就结束了? 其实不是

客户端驱动(比如 Go 的 sql.Rows) 还会逐行读取结果、反序列化成结构体。 这就是为什么一次性查询 10 万条时,客户端内存会爆炸的原因 。


总结:MySQL 执行 SQL 的完整图示

下面这张图就是整条 SQL 的生命周期:

[客户端]
   │
   ▼
[连接器][查询缓存(已废弃)][解析器][优化器][执行器][存储引擎]
   │                                                    │
   └────────────────────────────── 结果集返回 ───────────┘

延伸思考

  1. PostgreSQL 的优化器比 MySQL 强在哪里? 更智能的执行计划评估 + 更丰富的 join 策略。
  2. 为什么深分页 (LIMIT 1000000, 10) 慢? 因为执行器仍要扫描前一百万行,只是丢掉不用的部分。 (这也是上一篇我们讲过的优化点 )
  3. 为什么有时候 EXPLAIN 看似走了索引却仍然慢? 因为数据分布、索引选择、排序代价都可能抵消索引优势。

小结一下(怕你看累了 )

阶段 关键职责 对应 PostgreSQL 对比
连接器 线程管理、权限校验 进程模型,性能更稳
查询缓存 8.0 已废弃 无,靠 plan cache
解析器 生成语法树 同理,但语义更严
优化器 选最优执行计划 更智能的代价模型
执行器 调用引擎执行 更统一的执行框架
存储引擎 InnoDB 事务、索引、锁 内置 MVCC,无引擎层
返回结果 网络传输 同理

最后的思考题

记住,懂执行顺序不仅是为了背面试题, 更是为了当系统慢时,能知道该“查哪一层”。

比如:

  • 慢在优化器:可能统计信息不准;
  • 慢在执行器:可能索引没生效;
  • 慢在存储层:可能 I/O 或锁冲突;
  • 慢在客户端:可能网络或反序列化太重。

只有理解了整个流程,才能真正玩转数据库。 别忘了,「SQL 不只是数据语言,它是系统协作的艺术。」



「技术有道|用故事讲技术,让底层原理不再枯燥」 VX搜索Tech有道 , 获取更多资料和真实面经

发表评论
暂无评论

还没有评论呢,快来抢沙发~

客服

点击联系客服 点击联系客服

在线时间:09:00-18:00

关注微信公众号

关注微信公众号
客服电话

400-888-8888

客服邮箱 122325244@qq.com

手机

扫描二维码

手机访问本站

扫描二维码
搜索