‍ 一条 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有道 , 获取更多资料和真实面经

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]