一条 SQL 的执行流程

一句话总结

SELECT 和 UPDATE 走的路径不同——SELECT 到存储引擎拿数据就返回,UPDATE 要额外写日志(redo/undo/binlog)。但前两步(连接层→Server 层)完全一样。


🌰 整体路线

一条 SQL 从客户端到磁盘,经过四层:


graph TD

    Client["客户端(你的代码/命令行)"] --> L1["① 连接层<br/>认证·连接池·线程处理"]

    L1 --> L2["② Server 层<br/>分析器·优化器·执行器"]

    L2 --> L3["③ 存储引擎层<br/>InnoDB 真正干活的"]

    L3 --> L4["④ 磁盘<br/>数据页·索引页"]


一条 SELECT 语句的一生

 
SELECT * FROM user WHERE id = 1;
 

Step 1:连接器(连接层)

 
客户端发来 SQL → 连接器处理:
 
 
 
1. 检查连接池有没有可用连接
 
2. 验证用户名密码
 
3. 成功后 → 分配一个线程处理这个连接
 

Step 2:查询缓存(MySQL 8.0 已移除)

 
8.0 之前:
 
  └─ 先查缓存有没有这条 SQL 的结果
 
  └─ 有 → 直接返回,后面全跳过
 
  └─ 没有 → 继续往下
 
 
 
8.0 之后:
 
  └─ 没有这一步,直接往下(缓存已被移除)
 

原因:缓存失效太频繁。只要表有更新,整张表的查询缓存全清掉。命中率极低,维护成本高。

Step 3:分析器(Parser)

 
收到 SQL 文本 → 分析器做两件事:
 
 
 
1. 词法分析:拆解关键字
 
   └─ "SELECT" → 查询语句
 
   └─ "user" → 表名
 
   └─ "id" → 列名
 
 
 
2. 语法分析:检查 SQL 有没有语法错误
 
   └─ "SELECT" 后面跟了啥 → 合法?
 
   └─ "WHERE id = 1" → 合法?
 
   └─ 语法不对 → You have an error in your SQL syntax
 

Step 4:预处理器

 
1. 检查表和列是否存在
 
   └─ user 表存在吗?
 
   └─ id 列存在吗?
 
2. 权限检查
 
   └─ 当前用户有 SELECT user 表的权限吗?
 
   └─ 没有 → Access denied
 

Step 5:优化器(Optimizer)

 
这是"决定怎么查"的地方。
 
 
 
SQL:SELECT * FROM user WHERE id = 1;
 
 
 
优化器要考虑:
 
  └─ 走主键索引(直接 B+ 树搜 id=1)
 
  └─ 还是全表扫描(一行行看 id=1 在哪)
 
 
 
明显走主键索引快。
 
但如果 WHERE id > 1 AND id < 1000000 且表很大:
 
  └─ 走索引 vs 全表扫描 → 优化器判断哪个成本低
 
 
 
优化器的输出:一个"执行计划"(access path)
 

优化器可能选错索引。

统计信息不准时,优化器可能选了全表扫描而不是索引。这时可以用 FORCE INDEXUSE INDEX 强制指定。

Step 6:执行器

 
拿着优化器给的执行计划,真正执行:
 
 
 
1. 调用 InnoDB 接口:"取 id=1 的那行数据"
 
2. InnoDB 从 B+ 树找到 id=1 的数据页
 
3. 返回给执行器
 
4. 执行器返回给客户端
 

SELECT 全流程


graph TD

    Client["客户端"] --> Conn["连接器(分配线程,验证身份)"]

    Conn --> Parser["分析器(词法/语法分析)"]

    Parser --> PreProc["预处理器(检查表和列是否存在,权限检查)"]

    PreProc --> Optimizer["优化器(选索引,定执行计划)"]

    Optimizer --> Executor["执行器(调 InnoDB 拿数据)"]

    Executor --> InnoDB["InnoDB(B+ 树搜索 → 返回数据页)"]

    InnoDB --> Result["客户端收到结果"]


一条 UPDATE 语句的一生

 
UPDATE user SET name = 'Alice' WHERE id = 1;
 

前 5 步(连接→分析→预处理→优化→执行)和 SELECT 完全一样。

不同从执行器开始:

Step 6:执行器 → 找到行

 
1. 执行器调 InnoDB:"读取 id=1 的行"
 
2. InnoDB 从 B+ 树找到 id=1 的数据页,加载到 Buffer Pool
 
   (同时对该行加排他锁 X 锁,防止别的事务并发修改这一行)
 
3. 返回给执行器
 

Step 7:写 undo log

 
InnoDB 写 undo log:
 
  └─ "id=1 这一行,name 原来是 'Bob'"
 
  └─ 用于回滚和 MVCC
 

Step 8:在内存里改数据

 
在 Buffer Pool 里把 id=1 的 name 从 'Bob' 改成 'Alice'
 
此时数据页变成"脏页"(内存和磁盘不一致)
 

Step 9:写 redo log(Prepare 阶段)

 
InnoDB 写 redo log:
 
  └─ "表空间 X 的页号 5 的偏移量 1024 处的 name 改成 'Alice'"
 
  └─ 状态为 Prepare(准备阶段)
 

Step 10:写 binlog

 
MySQL Server 写 binlog:
 
  └─ "UPDATE user SET name = 'Alice' WHERE id = 1"
 
  └─ (ROW 模式下记录行变更)
 

Step 11:写 redo log(Commit 阶段)

 
InnoDB 把 redo log 状态从 Prepare 改成 Commit
 
至此事务才算真正提交成功
 
 
 
如果此时崩了:
 
  重启后检查 redo log:Prepare 状态 + binlog 存在 → 继续提交
 

Step 12:返回成功

 
MySQL 告诉客户端:"更新成功"
 
 
 
此时磁盘上的数据可能还是旧的!
 
Buffer Pool 里的脏页还没刷盘。
 
但没关系——如果崩溃了,redo log 可以恢复。
 

后续:刷脏页

 
空闲时或 Buffer Pool 满了时:
 
  InnoDB 把脏页刷回磁盘
 
  此时磁盘数据才真正变成 'Alice'
 

UPDATE 全流程


graph TD

    Client["客户端"] --> Fe["连接器"]

    Fe --> Parser["分析器"]

    Parser --> PreProc["预处理器"]

    PreProc --> Opt["优化器"]

    Opt --> Exec["执行器"]

    Exec --> Find["找到 id=1 的行"]

    Find --> Undo["写 undo log(旧值)"]

    Undo --> BP["Buffer Pool 里改数据"]

    BP --> RedoP["写 redo log(Prepare)"]

    RedoP --> Binlog["写 binlog"]

    Binlog --> RedoC["写 redo log(Commit)"]

    RedoC --> Succ["返回成功"]

    Succ --> Flush["(后续刷脏页到磁盘)"]


SELECT vs UPDATE 对比

步骤SELECTUPDATE
连接器
分析器
预处理器
优化器
执行器
写 undo log
Buffer Pool 改数据❌(只读)✅(写)
写 redo log
写 binlog
返回结果✅ 数据✅ 成功
刷脏页✅(后台)

一句话讲清

 
延伸提问:"一条 SQL 在 MySQL 里是怎么执行的?"
 
 
 
你:
 
"分两种情况——SELECT 和 UPDATE。
 
 
 
SELECT 相对简单:
 
连接器(身份认证)→ 分析器(语法检查)→
 
预处理器(表和字段存在?权限?)→
 
优化器(选索引,定执行计划)→
 
执行器(调 InnoDB 拿数据)→ 返回。
 
 
 
UPDATE 多了一套日志流程:
 
找到行之后 → Buffer Pool 里改数据 →
 
写 undo log(用于回滚和 MVCC)→
 
写 redo log Prepare → 写 binlog →
 
写 redo log Commit → 返回成功。
 
两阶段提交保证了 redo log 和 binlog 的一致性。"
 

记忆口诀

SELECT 查路:连接→分析→预处理→优化→执行→返回。

UPDATE 多三样:undo 保回滚,redo 保不丢,binlog 保同步。

两阶段提交保一致——Prepare→binlog→Commit,这是 MySQL 不丢数据的底气。

速记卡(面试闪卡)

Q1:一句话讲清「一条 SQL 的执行流程」到底是什么?

A:一条SQL经连接层认证、Server层分析优化执行、存储引擎取数据、落磁盘四层;SELECT到引擎拿数据就返回,UPDATE还要写redo/undo/binlog日志。

Q2:连接层:门卫查证件 —— 怎么理解?

A:连接层(Connection Layer)像公司门卫:检查连接池、验证用户名密码、分配线程处理。所有SQL进来的第一关,负责认证和线程调度,和你具体查什么无关。

Q3:Server层:大脑做计划 —— 怎么理解?

A:Server 层(Server Layer)是 MySQL 大脑:分析器(Parser)做词法语法拆解,优化器(Optimizer)选执行计划(如用哪个索引),执行器(Executor)调用存储引擎接口干活。

Q4:存储引擎层:真正干活的手 —— 怎么理解?

A:存储引擎层(Storage Engine Layer)以 InnoDB 为代表,是真正读写磁盘的”手”。SELECT 到这里拿数据就返回;UPDATE 除读数据外还要写 redo/undo/binlog 保证原子和持久。

Q5:查询缓存:8.0 已退役 —— 怎么理解?

A:查询缓存(Query Cache)在 MySQL 8.0 被移除:它缓存”SQL文本→结果”,但只要表一更新整表缓存全清,命中率极低、维护贵,所以官方直接砍掉,8.0 后这步直接跳过。

Q6:核心速记主线有哪些?

  • SQL 走四层:连接层→Server层→存储引擎→磁盘

  • 连接层认证分配线程,Server层分析优化执行

  • SELECT 到引擎取数即返回,UPDATE 额外写日志

  • 8.0 已移除查询缓存(失效频繁、维护贵)

口诀

A:进门先查身份证件

大脑拆解选计划

引擎动手取数据

改数据要写日志

相关链接