mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
676 字
2 分钟
MySQL 查询执行流程:从 SQL 到结果集
2023-04-27

前言#

当你执行一条 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_priv
FROM user WHERE User = 'root';
-- 数据库权限表
SELECT Host, Db, User, Select_priv, Insert_priv
FROM db;
-- 表权限表
SELECT Host, Db, User, Table_name, Table_priv
FROM 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 = 1
Token 流:
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 逻辑优化示例#

条件简化

-- 原始 SQL
SELECT * FROM users WHERE 1 = 1 AND id > 0;
-- 优化后
SELECT * FROM users;
-- 原始 SQL
SELECT * FROM users WHERE id = 1 AND id = 2;
-- 优化后(条件冲突,返回空结果)
Impossible WHERE

子查询优化

-- 原始子查询
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 1);
-- 优化为 JOIN
SELECT orders.* FROM orders
JOIN users ON orders.user_id = users.id
WHERE 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 o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id
WHERE 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=JSON
SELECT * 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_log
ORDER BY start_time DESC
LIMIT 10;

8.3 索引优化建议#

-- 查看索引使用情况
SELECT
TABLE_NAME,
INDEX_NAME,
CARDINALITY,
SEQ_IN_INDEX
FROM information_schema.STATISTICS
WHERE 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

关键要点#

  1. 连接器:管理连接、验证权限
  2. 分析器:词法分析、语法分析、语义检查
  3. 优化器:逻辑优化、物理优化、生成执行计划
  4. 执行器:调用存储引擎、执行查询
  5. 存储引擎:Buffer Pool、日志、数据文件

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

MySQL 查询执行流程:从 SQL 到结果集
https://blog.souloss.com/posts/principles/mysql-query-execution-process/
作者
Souloss
发布于
2023-04-27
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时