第4章:查询优化与执行计划
📖 章节概述
本章将深入讲解 MySQL 查询优化器的工作原理和执行计划分析,这是数据库性能调优的核心技能。通过本章学习,你将掌握:
- 查询优化器的决策过程
- 执行计划的成本模型
- EXPLAIN 的深度分析技巧
- JOIN 优化策略
- 排序和分组优化
- 统计信息与优化器决策
🧩 一、查询优化器是什么?
查询优化器(Query Optimizer)是 MySQL 的"大脑":
它决定 SQL 语句该如何执行、用哪个索引、以何种顺序连接表。
MySQL 的执行分两步:
- SQL Parser(解析器):生成语法树
- Optimizer(优化器):生成执行计划(execution plan)
优化器的目标: 找到"代价(cost)最小"的执行路径 —— 最少的磁盘 I/O、CPU、内存消耗。
🧠 二、优化器的决策过程
假设语句:
SELECT * FROM orders WHERE user_id = 10 AND create_time > '2025-10-01';
优化器会经历以下步骤:
1️⃣ 语法树 → 逻辑计划
分析表、索引、字段、条件,生成一棵逻辑树。
2️⃣ 访问路径分析(Access Path)
评估每个索引的可行性与代价:
- 是否命中索引
- 索引区分度
- 是否能覆盖
- 是否需回表
3️⃣ 连接顺序优化(Join Order)
对于多表 JOIN,优化器会尝试不同连接顺序,选择代价最小的方案(基于统计信息)。
4️⃣ 生成最终物理执行计划
确定使用的索引、连接算法、排序策略等。
⚙️ 三、执行计划的成本模型
优化器依据"成本估算模型"选择方案:
- I/O 成本:读取数据页代价
- CPU 成本:比较、计算代价
- 内存代价:排序、临时表代价
- 网络代价:结果集传输代价
MySQL 会为每个可能的执行路径计算:
total_cost = io_cost + cpu_cost + memory_cost + network_cost
然后选出最小成本的方案。
🔬 四、优化器内部架构与源码分析
MySQL 优化器是一个复杂的决策系统,让我们深入了解其内部机制:
1️⃣ 优化器执行流程
SQL Parser → Query Block → JOIN Tab → make_join_statistics()
↓
best_access_path() → choose_plan() → optimize_join_order()
↓
make_join_plan() → JOIN::exec()
📁 核心源码文件
模块 | 文件路径 | 功能 |
---|---|---|
查询优化 | sql/sql_optimizer.cc | 主要优化逻辑 |
执行器 | sql/sql_executor.cc | 执行计划执行 |
查询解析 | sql/sql_select.cc | SELECT 语句处理 |
成本计算 | sql/opt_costmodel.cc | 成本模型实现 |
索引选择 | sql/opt_range.cc | 范围查询优化 |
子查询优化 | sql/sql_optimizer.cc | 子查询重写 |
统计信息 | sql/table.cc | 表统计信息管理 |
2️⃣ 查询重写优化案例
案例1:子查询重写为JOIN
原始查询:
SELECT * FROM users u
WHERE u.id IN (
SELECT user_id FROM orders
WHERE amount > 1000
);
优化器重写为:
SELECT DISTINCT u.* FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000;
重写原因:
- 子查询需要为每行执行一次
- JOIN 可以更好地利用索引
- 减少临时表的使用
案例2:EXISTS 优化
原始查询:
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id AND o.status = 'paid'
);
优化器可能重写为:
SELECT u.* FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'paid';
案例3:ORDER BY 优化
原始查询:
SELECT * FROM users
WHERE age > 25
ORDER BY name;
优化器考虑:
- 如果
(age, name)
有索引,使用索引排序 - 否则使用 filesort
- 根据数据量选择排序算法
3️⃣ 优化器决策树
查询优化决策树
├── 单表查询
│ ├── 全表扫描 vs 索引扫描
│ ├── 索引选择(成本最低)
│ └── 覆盖索引优化
├── 多表查询
│ ├── JOIN 顺序优化
│ ├── JOIN 算法选择
│ └── 子查询重写
└── 复杂查询
├── 子查询优化
├── 派生表优化
└── 临时表优化
4️⃣ 成本模型详解
// 成本计算核心函数(简化版)
double calculate_scan_cost(TABLE* table, ha_rows rows) {
double io_cost = rows * table->cost_model()->page_read_cost();
double cpu_cost = rows * table->cost_model()->row_evaluate_cost();
return io_cost + cpu_cost;
}
// 源码路径:sql/opt_costmodel.cc
// 索引成本计算
double calculate_index_cost(TABLE* table, uint key_no, ha_rows rows) {
double io_cost = rows * table->cost_model()->key_compare_cost();
double cpu_cost = rows * table->cost_model()->row_evaluate_cost();
return io_cost + cpu_cost;
}
// JOIN 成本计算
double calculate_join_cost(JOIN_TAB* tab1, JOIN_TAB* tab2) {
double cost1 = tab1->read_cost;
double cost2 = tab2->read_cost;
double join_cost = cost1 + cost2 + (cost1 * cost2) / 1000;
return join_cost;
}
5️⃣ 优化器开关与调优
优化器开关:
-- 查看当前优化器开关
SHOW VARIABLES LIKE 'optimizer_switch';
-- 关键开关说明
-- index_merge: 索引合并优化
-- index_condition_pushdown: 索引下推
-- materialization: 物化优化
-- subquery_materialization_cost_based: 基于成本的子查询物化
-- use_index_extensions: 使用索引扩展
-- condition_fanout_filter: 条件扇出过滤
-- derived_merge: 派生表合并
-- use_invisible_indexes: 使用不可见索引
-- skip_scan: 跳跃扫描
-- hash_join: 哈希连接
-- subquery_to_derived: 子查询转派生表
优化器调优案例:
案例1:索引合并优化
-- 原始查询
SELECT * FROM users
WHERE age > 25 AND city = 'Beijing';
-- 如果有 (age) 和 (city) 两个索引
-- 优化器可能使用索引合并
EXPLAIN SELECT * FROM users
WHERE age > 25 AND city = 'Beijing';
-- 可能显示:type=index_merge, Extra=Using intersect(age,city)
案例2:哈希连接优化(MySQL 8.0+)
-- 大表连接查询
SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id;
-- 优化器可能选择哈希连接
EXPLAIN FORMAT=JSON SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id;
-- 查看 execution_plan 中的 join_type: "hash"
案例3:派生表合并
-- 原始查询
SELECT * FROM (
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id
) t
WHERE order_count > 5;
-- 优化器可能合并为
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;
🔍 成本参数配置
-- 查看成本模型参数
SHOW VARIABLES LIKE 'optimizer_cost%';
-- 关键参数说明
-- optimizer_cost_model: 成本模型类型
-- optimizer_switch: 优化策略开关
-- optimizer_trace: 优化器跟踪开关
3️⃣ 索引选择算法
// 索引选择核心逻辑
int best_access_path(TABLE* table,
const key_map& usable_keys,
double* cost) {
double best_cost = DBL_MAX;
int best_key = -1;
for (uint i = 0; i < table->s->keys; i++) {
if (!usable_keys.is_set(i)) continue;
double key_cost = calculate_key_cost(table, i);
if (key_cost < best_cost) {
best_cost = key_cost;
best_key = i;
}
}
*cost = best_cost;
return best_key;
}
// 源码路径:sql/sql_optimizer.cc
4️⃣ 连接顺序优化
MySQL 使用动态规划算法优化多表连接顺序:
// 连接顺序优化(简化版)
void optimize_join_order(JOIN* join) {
// 1. 生成所有可能的连接顺序
for (uint i = 0; i < join->tables; i++) {
for (uint j = 0; j < join->tables; j++) {
if (i == j) continue;
// 2. 计算连接成本
double cost = calculate_join_cost(table[i], table[j]);
// 3. 选择最优顺序
if (cost < best_cost) {
best_cost = cost;
best_order = {i, j};
}
}
}
}
// 源码路径:sql/sql_optimizer.cc
📊 连接顺序验证
-- 查看优化器选择的连接顺序
EXPLAIN SELECT * FROM a, b, c WHERE a.id = b.aid AND b.id = c.bid;
-- 强制指定连接顺序
EXPLAIN SELECT STRAIGHT_JOIN * FROM a, b, c WHERE a.id = b.aid AND b.id = c.bid;
-- 对比两种方式的成本差异
SHOW SESSION STATUS LIKE 'Last_query_cost';
🔍 四、EXPLAIN 深度分析
EXPLAIN 是 MySQL 性能分析的必备工具:
EXPLAIN SELECT * FROM orders WHERE user_id=10 AND create_time > '2025-10-01';
输出字段解析
字段 | 说明 |
---|---|
id | 查询中语句块的执行顺序 |
select_type | SIMPLE / PRIMARY / SUBQUERY 等 |
table | 涉及的表名 |
type | 访问类型(越靠左性能越好) |
possible_keys | 可能使用的索引 |
key | 实际使用的索引 |
key_len | 索引使用字节数 |
rows | 优化器预估扫描的行数 |
filtered | 条件过滤后保留的行比例 |
Extra | 额外信息 |
🧱 五、type 字段详解(性能等级表)
访问类型越靠前性能越好:
type | 说明 | 性能 |
---|---|---|
system | 表只有一行(如 dual) | ✅ 极快 |
const | 主键或唯一索引等值查询 | ✅ 很快 |
eq_ref | 多表 join,使用唯一索引连接 | ✅ |
ref | 普通索引等值查询 | 👍 |
range | 范围扫描(BETWEEN, >, <, LIKE 'T%') | ⚡ |
index | 全索引扫描(无 WHERE) | ⚠️ |
ALL | 全表扫描 | ❌ |
📌 优化目标: 让 type ≥ range,最好是 ref 或 const
🧩 六、Extra 字段常见信息
Extra 内容 | 含义 | 优化建议 |
---|---|---|
Using index | 覆盖索引,未回表 | ✅ 理想情况 |
Using where | 条件过滤 | 正常 |
Using temporary | 使用临时表(多见于 GROUP BY) | ❌ 尝试优化聚合 |
Using filesort | 需要额外排序 | ❌ 尝试使用索引排序 |
Using index condition | 使用索引下推(ICP) | ✅ 表示已优化过滤 |
Using join buffer | JOIN 过程中未命中索引 | ❌ 建立索引或调整 join 顺序 |
⚔️ 七、连接优化策略(JOIN Optimization)
优化器会尝试多种连接算法:
JOIN 算法 | 特征 | 说明 |
---|---|---|
Nested Loop Join (NLJ) | 嵌套循环 | 默认算法,逐行匹配 |
Block Nested Loop Join (BNL) | 块循环 | 使用 join buffer 加速 |
Index Nested Loop Join (INLJ) | 使用索引加速连接 | ✅ 最常见 & 高效 |
Hash Join | MySQL 8.0+ 新增 | 适合大表连接 |
📘 优化建议:
- 确保 ON / WHERE 中的 join 字段上有索引
- 小表驱动大表(减少外层循环)
- 用 STRAIGHT_JOIN 强制 join 顺序(必要时)
📊 八、排序与分组优化
1️⃣ ORDER BY 优化
- 若排序列有索引,且方向一致(ASC/ASC 或 DESC/DESC)→ 可利用索引排序
- 若排序列方向不同或包含非索引列 → 需 filesort
-- 检查是否使用索引排序
EXPLAIN SELECT * FROM user ORDER BY name, age;
-- 若显示 Extra: Using index 说明使用索引排序,无需额外排序
2️⃣ GROUP BY 优化
- 默认使用临时表聚合
- 若索引覆盖聚合字段,可直接利用索引
-- 推荐写法
SELECT name, COUNT(*) FROM user GROUP BY name;
-- 若有索引 (name) 则不需临时表
⚙️ 九、统计信息与优化器决策偏差
优化器依赖统计信息(Statistics)来估算表的行数、索引分布。
ANALYZE TABLE
:更新统计信息SHOW INDEX FROM table
:查看 Cardinality(索引区分度)- 若统计信息不准,优化器可能选错索引
📘 调优建议:
- 定期更新统计信息
- 可使用 USE INDEX 或 FORCE INDEX 临时纠偏
- 避免多索引合并(optimizer_switch)
🛠️ 实操演示
实操1:基础执行计划分析
-- 创建测试表
USE mysql_learning;
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
product_id INT NOT NULL,
amount DECIMAL(10,2),
status ENUM('pending', 'paid', 'shipped', 'delivered') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB;
-- 插入测试数据
INSERT INTO orders (user_id, product_id, amount, status) VALUES
(1, 101, 99.99, 'paid'),
(1, 102, 199.99, 'shipped'),
(2, 101, 299.99, 'pending'),
(2, 103, 399.99, 'delivered'),
(3, 102, 499.99, 'paid');
实操2:不同访问类型的执行计划
-- 1. const 类型(主键查询)
EXPLAIN SELECT * FROM orders WHERE id = 1;
-- 2. ref 类型(普通索引等值查询)
EXPLAIN SELECT * FROM orders WHERE user_id = 1;
-- 3. range 类型(范围查询)
EXPLAIN SELECT * FROM orders WHERE created_at > '2024-01-01';
-- 4. ALL 类型(全表扫描)
EXPLAIN SELECT * FROM orders WHERE amount > 100;
实操3:JOIN 执行计划分析
-- 创建用户表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
email VARCHAR(100)
) ENGINE=InnoDB;
INSERT INTO users (name, email) VALUES
('张三', 'zhangsan@example.com'),
('李四', 'lisi@example.com'),
('王五', 'wangwu@example.com');
-- 分析 JOIN 查询
EXPLAIN SELECT u.name, o.amount, o.status
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'paid';
-- 查看详细的 JOIN 信息
EXPLAIN FORMAT=JSON SELECT u.name, o.amount, o.status
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'paid';
实操4:排序优化分析
-- 创建索引
CREATE INDEX idx_user_created ON orders(user_id, created_at);
-- 测试索引排序
EXPLAIN SELECT * FROM orders WHERE user_id = 1 ORDER BY created_at;
-- 测试非索引排序
EXPLAIN SELECT * FROM orders ORDER BY amount;
-- 测试混合排序
EXPLAIN SELECT * FROM orders WHERE user_id = 1 ORDER BY amount;
实操5:分组优化分析
-- 测试分组查询
EXPLAIN SELECT user_id, COUNT(*), AVG(amount)
FROM orders
GROUP BY user_id;
-- 创建覆盖索引
CREATE INDEX idx_user_amount ON orders(user_id, amount);
-- 再次测试分组查询
EXPLAIN SELECT user_id, COUNT(*), AVG(amount)
FROM orders
GROUP BY user_id;
实操6:统计信息管理
-- 查看表的统计信息
SHOW TABLE STATUS LIKE 'orders';
-- 查看索引统计信息
SHOW INDEX FROM orders;
-- 更新统计信息
ANALYZE TABLE orders;
-- 查看优化器开关
SHOW VARIABLES LIKE 'optimizer_switch';
-- 查看成本模型参数
SHOW VARIABLES LIKE 'optimizer_cost%';
实操7:性能分析工具
-- 开启性能分析
SET profiling = 1;
-- 执行查询
SELECT u.name, o.amount, o.status
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'paid';
-- 查看性能分析结果
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;
-- 关闭性能分析
SET profiling = 0;
实操8:优化器提示使用
-- 使用 USE INDEX 提示
EXPLAIN SELECT * FROM orders USE INDEX (idx_user_id) WHERE user_id = 1;
-- 使用 FORCE INDEX 强制使用索引
EXPLAIN SELECT * FROM orders FORCE INDEX (idx_user_id) WHERE user_id = 1;
-- 使用 IGNORE INDEX 忽略索引
EXPLAIN SELECT * FROM orders IGNORE INDEX (idx_user_id) WHERE user_id = 1;
-- 使用 STRAIGHT_JOIN 强制连接顺序
EXPLAIN SELECT u.name, o.amount
FROM users u
STRAIGHT_JOIN orders o ON u.id = o.user_id;
🎯 实战案例
案例1:复杂查询优化
问题场景: 电商系统需要查询用户最近30天的订单统计,按用户分组,按订单金额排序。
原始查询:
SELECT
u.name,
COUNT(o.id) as order_count,
SUM(o.amount) as total_amount,
AVG(o.amount) as avg_amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.name
ORDER BY total_amount DESC
LIMIT 10;
执行计划分析:
EXPLAIN SELECT
u.name,
COUNT(o.id) as order_count,
SUM(o.amount) as total_amount,
AVG(o.amount) as avg_amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.name
ORDER BY total_amount DESC
LIMIT 10;
优化方案:
-- 1. 创建复合索引
CREATE INDEX idx_user_created_amount ON orders(user_id, created_at, amount);
-- 2. 优化后的查询
EXPLAIN SELECT
u.name,
COUNT(o.id) as order_count,
SUM(o.amount) as total_amount,
AVG(o.amount) as avg_amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.name
ORDER BY total_amount DESC
LIMIT 10;
案例2:慢查询诊断与优化
问题场景: 系统出现慢查询,需要分析并优化。
诊断步骤:
-- 1. 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 2. 执行问题查询
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
AND o.created_at BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY o.created_at DESC;
-- 3. 分析执行计划
EXPLAIN SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
AND o.created_at BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY o.created_at DESC;
-- 4. 查看慢查询日志
SHOW VARIABLES LIKE 'slow_query_log_file';
优化方案:
-- 创建覆盖索引
CREATE INDEX idx_status_created_user ON orders(status, created_at, user_id);
-- 验证优化效果
EXPLAIN SELECT o.id, o.user_id, o.amount, o.status, o.created_at, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
AND o.created_at BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY o.created_at DESC;
📝 练习题目
基础题
- 什么是查询优化器?它的主要作用是什么?
- EXPLAIN 中的 type 字段有哪些值?性能如何排序?
- 什么是覆盖索引?如何通过 EXPLAIN 识别?
进阶题
- 如何分析一个复杂的 JOIN 查询的执行计划?
- 统计信息不准确会导致什么问题?如何解决?
- 如何优化 ORDER BY 和 GROUP BY 查询?
实战题
- 分析一个慢查询,找出性能瓶颈并提出优化方案
- 设计一个多表关联查询的索引策略
- 如何监控和调优查询优化器的行为?
📚 扩展阅读
下一章预告: 第5章:存储与锁机制 - 深入理解 InnoDB 的存储结构和并发控制机制。