螃蟹大乱斗无限金币版
92.15MB · 2025-10-26
这恐怕是很多后端同学第一次被“SQL 执行流程”拷问灵魂的瞬间 。
明明平时写 SQL 写得飞起,什么 SELECT * FROM user WHERE id = 1; 手到擒来,
可真让你说清楚它从 「回车键」 到 「结果返回」 的全过程,
十有八九都是一脸懵圈。
今天我们就来当一回数据库侦探 ,
跟着这条 SQL 一起,深入 MySQL 的地底世界。
顺便也看看它的老对手 PostgreSQL 是怎么干这事的。
假设我们执行这样一句 SQL:
SELECT name, age FROM users WHERE city = 'Shanghai' ORDER BY age DESC LIMIT 10;
这句话从敲下回车的那一刻起,其实会经历 「七个阶段」
别急,我们一个一个扒开看。
MySQL 是个典型的 「客户端/服务器架构」。 当你通过客户端(比如 Navicat、Go 的 database/sql)发起连接时, MySQL 会先进入 「连接管理阶段」。
「MySQL」 使用线程池模型,一个连接对应一个线程。 「PostgreSQL」 传统上是一个连接一个进程(也因此多连接下开销更大)。
连接建立好后,MySQL 会先验证账号密码、权限等。 连接成功后,你就能愉快地发送 SQL 了。
小知识点: 「MySQL 8.0 引入了连接池 plugin,可以减少线程开销。」 而 PostgreSQL 则常用第三方连接池(如 pgBouncer)来减压。
MySQL 曾经有一个 「Query Cache」, 如果你发的 SQL 在缓存里有一模一样的结果,它直接返回 。
问题是,只要表中有一行数据被更新, 相关缓存就全部失效 。 高并发场景下,这反而拖慢了性能。
于是从 MySQL 8.0 开始,「Query Cache 被彻底移除」。 而 PostgreSQL 则一直没有内建 Query Cache, 因为它更倾向于用 「共享缓冲区 + 执行计划缓存」 来提速。
解析器的工作就像语文老师批作文。 它要判断这句话语法对不对, 然后把 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,但它在此阶段就做了更多语义检查(比如字段是否存在)。
到这里是重头戏 。 优化器的任务是:
比如上面的查询,优化器要考虑:
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+)进一步看到真实执行时间。
优化器决定了“怎么干”,执行器就负责“真干”。 它会按照优化器的 plan,调用底层的存储引擎 API。
比如说它要从表里取数据,就会调用:
engine.read("users", condition)
(当然底层不是真的 Go 哈,这只是方便理解 )
每读一行,就判断 city == 'Shanghai', 符合条件的就放进结果集,最后排序、截取前 10 条。
这是 MySQL 最有“灵魂分裂感”的部分 。 因为不同表,可以有不同的存储引擎!
最常见的当然是 「InnoDB」。
InnoDB 负责的数据层逻辑包括:
而 PostgreSQL 没有“存储引擎”这个概念, 它的存储层和执行层是紧耦合的。 但它在 「MVCC(多版本并发控制)」 上比 MySQL 更纯粹:每次更新会产生新版本,而不是回滚段。
当执行器拿到结果集后, 会通过网络协议一批一批地返回给客户端。
你以为这一步就结束了? 其实不是
客户端驱动(比如 Go 的 sql.Rows) 还会逐行读取结果、反序列化成结构体。 这就是为什么一次性查询 10 万条时,客户端内存会爆炸的原因 。
下面这张图就是整条 SQL 的生命周期:
[客户端]
│
▼
[连接器] → [查询缓存(已废弃)] → [解析器] → [优化器] → [执行器] → [存储引擎]
│ │
└────────────────────────────── 结果集返回 ───────────┘
LIMIT 1000000, 10) 慢? 因为执行器仍要扫描前一百万行,只是丢掉不用的部分。 (这也是上一篇我们讲过的优化点 )EXPLAIN 看似走了索引却仍然慢? 因为数据分布、索引选择、排序代价都可能抵消索引优势。| 阶段 | 关键职责 | 对应 PostgreSQL 对比 |
|---|---|---|
| 连接器 | 线程管理、权限校验 | 进程模型,性能更稳 |
| 查询缓存 | 8.0 已废弃 | 无,靠 plan cache |
| 解析器 | 生成语法树 | 同理,但语义更严 |
| 优化器 | 选最优执行计划 | 更智能的代价模型 |
| 执行器 | 调用引擎执行 | 更统一的执行框架 |
| 存储引擎 | InnoDB 事务、索引、锁 | 内置 MVCC,无引擎层 |
| 返回结果 | 网络传输 | 同理 |
记住,懂执行顺序不仅是为了背面试题, 更是为了当系统慢时,能知道该“查哪一层”。
比如:
只有理解了整个流程,才能真正玩转数据库。 别忘了,「SQL 不只是数据语言,它是系统协作的艺术。」
「技术有道|用故事讲技术,让底层原理不再枯燥」 VX搜索Tech有道 , 获取更多资料和真实面经