676 字
2 分钟
MySQL 查询执行流程:从 SQL 到结果集
前言
当你执行一条 SELECT * FROM users WHERE id = 1 时,MySQL 内部发生了什么?本文深入剖析 MySQL 查询执行的完整流程,帮助你理解数据库的工作原理。
MySQL 架构概览
flowchart TB
subgraph 客户端
A[MySQL Client]
end
subgraph MySQL Server
B[连接器] --> C[查询缓存]
C --> D[分析器]
D --> E[优化器]
E --> F[执行器]
end
subgraph 存储引擎
G[InnoDB]
H[MyISAM]
I[Memory]
end
A --> B
F --> G
F --> H
F --> I
J[数据文件] --> G
K[日志文件] --> G
一、连接器
1.1 连接管理
sequenceDiagram
participant C as Client
participant S as MySQL Server
participant T as Thread Cache
C->>S: TCP 连接请求
S->>S: 检查最大连接数
S->>T: 获取或创建线程
T->>S: 返回线程
S->>C: 连接成功
S->>S: 认证用户权限
S-->>C: 认证结果
连接参数:
-- 查看最大连接数SHOW VARIABLES LIKE 'max_connections';
-- 查看当前连接数SHOW STATUS LIKE 'Threads_connected';
-- 查看连接详情SHOW PROCESSLIST;+----+------+-----------+------+---------+------+----------+------------------+| Id | User | Host | db | Command | Time | State | Info |+----+------+-----------+------+---------+------+----------+------------------+| 5 | root | localhost | test | Query | 0 | starting | SHOW PROCESSLIST || 6 | app | 10.0.0.1 | prod | Sleep | 120 | | NULL |+----+------+-----------+------+---------+------+----------+------------------+1.2 连接池
// 应用层连接池配置(以 Node.js 为例)const pool = mysql.createPool({ host: "localhost", user: "root", password: "password", database: "test", connectionLimit: 10, // 最大连接数 waitForConnections: true, // 等待可用连接 queueLimit: 0, // 等待队列无限制});1.3 权限验证
flowchart TB
A[用户连接] --> B{验证身份}
B -->|失败| C[拒绝连接]
B -->|成功| D[加载权限]
D --> E[权限缓存]
E --> F{执行 SQL}
F --> G[检查权限]
G -->|有权限| H[执行]
G -->|无权限| I[拒绝执行]
权限表:
-- MySQL 权限存储在 mysql 系统库USE mysql;SHOW TABLES LIKE '%priv%';
-- 用户权限表SELECT Host, User, Select_priv, Insert_priv, Update_privFROM user WHERE User = 'root';
-- 数据库权限表SELECT Host, Db, User, Select_priv, Insert_privFROM db;
-- 表权限表SELECT Host, Db, User, Table_name, Table_privFROM tables_priv;二、查询缓存
2.1 缓存机制
注意:MySQL 8.0 已移除查询缓存,因为缓存命中率低,且维护开销大。
flowchart TB
A[接收查询] --> B{查询缓存?}
B -->|命中| C[直接返回结果]
B -->|未命中| D[继续执行]
D --> E[执行完成]
E --> F[结果写入缓存]
G[表数据变更] --> H[清空相关缓存]
缓存条件:
-- 查看缓存配置SHOW VARIABLES LIKE 'query_cache%';
-- 缓存条件-- 1. 查询必须是确定的(不含 NOW()、RAND() 等)-- 2. 不使用用户变量-- 3. 不使用临时表-- 4. 表无写操作2.2 为什么移除
| 问题 | 说明 |
|---|---|
| 命中率低 | 参数化查询、动态数据导致缓存无效 |
| 维护开销 | 每次写操作都要检查并清理缓存 |
| 并发瓶颈 | 缓存锁影响并发性能 |
| 替代方案 | 应用层缓存(Redis)更有效 |
三、分析器
3.1 词法分析
将 SQL 字符串分解为 Token:
SELECT name FROM users WHERE id = 1Token 流:SELECT (关键字)name (标识符)FROM (关键字)users (标识符)WHERE (关键字)id (标识符)= (操作符)1 (数值字面量)3.2 语法分析
将 Token 构建为语法树:
flowchart TB
A[SELECT 语句] --> B[SELECT 列]
A --> C[FROM 子句]
A --> D[WHERE 子句]
B --> B1[name]
C --> C1[表: users]
D --> D1[条件]
D1 --> D2[id = 1]
D2 --> D3[id]
D2 --> D4[=]
D2 --> D5[1]
3.3 预处理
flowchart TB
A[语法树] --> B{检查表是否存在}
B -->|否| C[Unknown table 错误]
B -->|是| D{检查列是否存在}
D -->|否| E[Unknown column 错误]
D -->|是| F{检查权限}
F -->|否| G[Access denied 错误]
F -->|是| H[继续优化]
四、优化器
4.1 优化流程
flowchart TB
A[语法树] --> B[逻辑优化]
B --> C[物理优化]
C --> D[执行计划]
subgraph 逻辑优化
B1[条件简化]
B2[常量折叠]
B3[子查询优化]
B4[外连接消除]
end
subgraph 物理优化
C1[访问路径选择]
C2[连接顺序选择]
C3[索引选择]
end
4.2 逻辑优化示例
条件简化:
-- 原始 SQLSELECT * FROM users WHERE 1 = 1 AND id > 0;
-- 优化后SELECT * FROM users;
-- 原始 SQLSELECT * FROM users WHERE id = 1 AND id = 2;
-- 优化后(条件冲突,返回空结果)Impossible WHERE子查询优化:
-- 原始子查询SELECT * FROM ordersWHERE user_id IN (SELECT id FROM users WHERE status = 1);
-- 优化为 JOINSELECT orders.* FROM ordersJOIN users ON orders.user_id = users.idWHERE users.status = 1;4.3 访问路径选择
| 访问方式 | 说明 | 适用场景 |
|---|---|---|
| 全表扫描 | 扫描所有数据行 | 无索引或数据量小 |
| 索引扫描 | 使用索引查找 | 有合适索引 |
| 索引覆盖 | 只读索引不回表 | 索引包含所有字段 |
| Range 扫描 | 范围扫描 | 范围查询 |
-- 查看执行计划EXPLAIN SELECT * FROM users WHERE id = 1;+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+| 1 | SIMPLE | users | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+4.4 连接优化
连接顺序选择:
-- 多表连接SELECT * FROM orders oJOIN users u ON o.user_id = u.idJOIN products p ON o.product_id = p.idWHERE u.status = 1;
-- 优化器决定连接顺序-- 通常从小表开始驱动连接算法:
| 算法 | 说明 | 适用场景 |
|---|---|---|
| Nested Loop | 嵌套循环 | 小表驱动大表 |
| Block Nested Loop | 块嵌套循环 | 无索引连接 |
| Hash Join | 哈希连接 | MySQL 8.0+,等值连接 |
| Sort Merge | 排序合并 | 已排序数据 |
flowchart TB
subgraph Nested Loop
A1[外层表每行] --> A2[扫描内层表]
A2 --> A3{匹配条件}
A3 -->|匹配| A4[输出结果]
end
subgraph Block Nested Loop
B1[读取外层表块到 Join Buffer] --> B2[扫描内层表]
B2 --> B3[批量匹配]
end
4.5 索引选择
索引统计信息:
-- 查看索引统计SHOW INDEX FROM users;
-- 分析表更新统计信息ANALYZE TABLE users;选择因素:
-- 索引选择示例-- 假设有索引: idx_name(name), idx_age(age), idx_name_age(name, age)
-- 查询 1:使用 idx_name_age(最左前缀)SELECT * FROM users WHERE name = 'Alice' AND age = 25;
-- 查询 2:使用 idx_age(name 不在条件中)SELECT * FROM users WHERE age = 25;
-- 查询 3:使用 idx_name_age 覆盖索引SELECT name, age FROM users WHERE name = 'Alice';
-- 查询 4:可能全表扫描(选择度低)SELECT * FROM users WHERE gender = 'M'; -- 假设 50% 数据匹配选择度计算:
选择度 = 不重复值数量 / 总行数
高选择度(接近 1):适合建索引低选择度(接近 0):不适合建索引
示例:- id(主键):选择度 = 1,适合索引- gender(性别):选择度 ≈ 0.5,不适合索引- email(邮箱):选择度 ≈ 1,适合索引五、执行器
5.1 执行流程
flowchart TB
A[执行计划] --> B[调用存储引擎接口]
B --> C[获取下一行]
C --> D{是否满足条件}
D -->|是| E[加入结果集]
D -->|否| F[跳过]
E --> G{还有更多行}
F --> G
G -->|是| C
G -->|否| H[返回结果集]
5.2 执行器与存储引擎交互
sequenceDiagram
participant E as 执行器
participant H as Handler 接口
participant S as 存储引擎
participant B as Buffer Pool
E->>H: ha_rnd_init()
H->>S: 初始化扫描
E->>H: ha_rnd_next()
H->>S: 获取下一行
S->>B: 查找数据页
B-->>S: 返回数据
S-->>H: 返回行
H-->>E: 返回行数据
Note over E: 检查 WHERE 条件
E->>H: ha_rnd_next()
Note over E,H: 循环直到结束
E->>H: ha_rnd_end()
H->>S: 结束扫描
5.3 Handler API
// 存储引擎 Handler 接口(简化)class handler {public: // 打开/关闭表 virtual int open(const char *name, int mode, uint test_if_locked) = 0; virtual int close(void) = 0;
// 扫描操作 virtual int rnd_init(bool scan) = 0; virtual int rnd_next(uchar *buf) = 0; virtual int rnd_end() = 0;
// 索引操作 virtual int index_init(uint idx, bool sorted) = 0; virtual int index_read(uchar *buf, const uchar *key, uint key_len, enum ha_rkey_function find_flag) = 0; virtual int index_next(uchar *buf) = 0;
// 写入操作 virtual int write_row(uchar *buf) = 0; virtual int update_row(const uchar *old_data, uchar *new_data) = 0; virtual int delete_row(const uchar *buf) = 0;};六、存储引擎层
6.1 InnoDB 架构
flowchart TB
subgraph InnoDB 内存结构
A[Buffer Pool] --> A1[数据页缓存]
A --> A2[索引页缓存]
A --> A3[自适应哈希索引]
B[Log Buffer] --> B1[Redo Log 缓冲]
C[内存结构]
end
subgraph InnoDB 磁盘结构
D[系统表空间] --> D1[数据字典]
D --> D2[Doublewrite Buffer]
E[独立表空间] --> E1[.ibd 文件]
F[Redo Log] --> F1[ib_logfile0/1]
G[Undo Log] --> G1[回滚段]
end
C <--> D
C <--> E
C <--> F
6.2 Buffer Pool
-- 查看 Buffer Pool 配置SHOW VARIABLES LIKE 'innodb_buffer_pool%';
-- 查看 Buffer Pool 状态SHOW STATUS LIKE 'Innodb_buffer_pool%';Buffer Pool 结构:
Buffer Pool (默认 128MB)├── Instance 0│ ├── Chunk 0│ │ ├── Page 0 (16KB)│ │ ├── Page 1│ │ └── ...│ └── Chunk 1└── Instance 1 └── ...页面管理:
flowchart LR
subgraph LRU 列表
A[Young 区域<br/>热点数据] --> B[Old 区域<br/>冷数据]
end
C[新读取的页] --> D{是否热点?}
D -->|是| E[加入 Young 头部]
D -->|否| F[加入 Old 头部]
G[访问 Old 页] --> H[移动到 Young 头部]
6.3 数据页结构
InnoDB 数据页结构(16KB):┌────────────────────────────────────┐│ File Header (38 bytes) │ 页面通用信息├────────────────────────────────────┤│ Page Header (56 bytes) │ 页面状态信息├────────────────────────────────────┤│ Infimum + Supremum (26 bytes) │ 最小/最大记录├────────────────────────────────────┤│ User Records │ 用户数据│ (行格式: Compact/Dynamic/...) │├────────────────────────────────────┤│ Free Space │ 空闲空间├────────────────────────────────────┤│ Page Directory │ 页目录(槽位)├────────────────────────────────────┤│ File Trailer (8 bytes) │ 校验和└────────────────────────────────────┘6.4 B+ 树索引
flowchart TB
subgraph B+ 树结构
A[根节点<br/>页号 100] --> B[中间节点<br/>页号 101]
A --> C[中间节点<br/>页号 102]
B --> D[叶子节点<br/>页号 201]
B --> E[叶子节点<br/>页号 202]
C --> F[叶子节点<br/>页号 203]
C --> G[叶子节点<br/>页号 204]
D -->|双向链表| E
E -->|双向链表| F
F -->|双向链表| G
end
索引查找过程:
sequenceDiagram
participant C as Client
participant E as 执行器
participant B as Buffer Pool
participant D as 磁盘
C->>E: SELECT * WHERE id = 5
E->>B: 读取根页 100
Note over B: 命中缓存
E->>B: 读取中间页 101
Note over B: 命中缓存
E->>B: 读取叶子页 201
alt 缓存命中
B-->>E: 返回数据页
else 缓存未命中
B->>D: 读取磁盘页
D-->>B: 返回页数据
B-->>E: 返回数据页
end
E->>E: 在页内二分查找
E-->>C: 返回记录
七、更新操作流程
7.1 UPDATE 执行流程
sequenceDiagram
participant C as Client
participant S as Server
participant B as Buffer Pool
participant U as Undo Log
participant R as Redo Log
participant D as 磁盘
C->>S: UPDATE users SET name='Bob' WHERE id=1
Note over S: 1. 查找记录
S->>B: 读取数据页
Note over S: 2. 记录 Undo Log
S->>U: 写入旧值到 Undo Log
Note over S: 3. 更新内存
S->>B: 修改 Buffer Pool 中的页
Note over S: 4. 记录 Redo Log
S->>R: 写入 Redo Log (WAL)
Note over S: 5. 返回成功
S-->>C: Query OK
Note over S: 后台异步刷盘
B->>D: 刷脏页到磁盘
7.2 WAL 机制
Write-Ahead Logging:
flowchart LR
A[修改数据] --> B[写入 Redo Log Buffer]
B --> C[刷入 Redo Log 文件]
C --> D[提交成功]
E[后台线程] --> F[刷脏页到数据文件]
G[崩溃恢复] --> H[重放 Redo Log]
刷盘策略:
-- Redo Log 刷盘策略SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
-- 0: 每秒刷盘(可能丢失 1 秒数据)-- 1: 每次提交刷盘(最安全,默认)-- 2: 每次提交写入 OS 缓存,每秒刷盘7.3 两阶段提交
保证 Redo Log 和 Binlog 一致性:
sequenceDiagram
participant T as 事务
participant R as Redo Log
participant B as Binlog
Note over T: 阶段 1: Prepare
T->>R: 写入 Redo Log (prepare 状态)
Note over T: 阶段 2: 写 Binlog
T->>B: 写入 Binlog
Note over T: 阶段 3: Commit
T->>R: 写入 Redo Log (commit 状态)
Note over T: 事务完成
八、性能优化
8.1 执行计划分析
-- 查看完整执行计划EXPLAIN FORMAT=JSONSELECT * FROM users WHERE name = 'Alice';
-- 使用优化器跟踪SET optimizer_trace='enabled=on';SELECT * FROM users WHERE name = 'Alice';SELECT * FROM information_schema.OPTIMIZER_TRACE;SET optimizer_trace='enabled=off';8.2 慢查询分析
-- 开启慢查询日志SET GLOBAL slow_query_log = 'ON';SET GLOBAL long_query_time = 1; -- 超过 1 秒记录
-- 查看慢查询SELECT * FROM mysql.slow_logORDER BY start_time DESCLIMIT 10;8.3 索引优化建议
-- 查看索引使用情况SELECT TABLE_NAME, INDEX_NAME, CARDINALITY, SEQ_IN_INDEXFROM information_schema.STATISTICSWHERE TABLE_SCHEMA = 'your_db'ORDER BY TABLE_NAME, INDEX_NAME;
-- 分析索引建议-- 使用 pt-index-usage 工具总结
MySQL 查询完整流程
flowchart TB
A[SQL 语句] --> B[连接器]
B --> C[查询缓存]
C --> D[分析器]
D --> E[优化器]
E --> F[执行器]
F --> G[存储引擎]
subgraph 分析阶段
D1[词法分析] --> D2[语法分析] --> D3[语义分析]
end
subgraph 优化阶段
E1[逻辑优化] --> E2[物理优化] --> E3[生成执行计划]
end
subgraph 执行阶段
F1[调用 Handler API] --> F2[访问 Buffer Pool] --> F3[访问磁盘]
end
关键要点
- 连接器:管理连接、验证权限
- 分析器:词法分析、语法分析、语义检查
- 优化器:逻辑优化、物理优化、生成执行计划
- 执行器:调用存储引擎、执行查询
- 存储引擎:Buffer Pool、日志、数据文件
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
MySQL 查询执行流程:从 SQL 到结果集
https://blog.souloss.com/posts/principles/mysql-query-execution-process/ 部分信息可能已经过时
相关文章 智能推荐
1
PostgreSQL 查询优化器:如何选择最优执行计划
原理 深入剖析 PostgreSQL 查询优化器的完整工作流程,从语法解析到计划生成,涵盖查询重写、代价估计、连接算法、并行查询等核心机制。
2
Redis 请求处理流程:从命令到响应
原理 深入剖析 Redis 处理客户端请求的完整流程,从网络接收到命令执行,从事件循环到响应发送,揭示 Redis 高性能的秘密。
3
Kafka 可靠性与 exactly-once
消息队列与事件流 深入 Kafka 可靠性——ACK 策略、min.insync.replicas、幂等生产者、事务 API,理解 Kafka 如何保证消息不丢不重。
4
一致性级别:从强一致到最终一致
分布式系统深入 系统梳理分布式一致性级别——从线性一致性到最终一致性的光谱,以及不同级别的适用场景
5
查询处理与优化:从 SQL 到执行计划
数据库 深入解析数据库查询处理的全流程——SQL 解析→重写→优化→执行,代价模型与启发式优化,统计信息的重要性,以及跨数据库 EXPLAIN 解读。






