HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • 学习 MySQL

    • MySQL 深入学习手册
    • 第1章:MySQL 架构与存储引擎
    • 第2章:事务与隔离级别原理
    • 第3章:索引底层实现
    • 第4章:查询优化与执行计划
    • 第5章:存储引擎深度解析
    • 第6章:高可用与复制原理
    • 第7章:性能与存储调优
    • 第8章:MySQL 面试题大全

第4章:查询优化与执行计划

📖 章节概述

本章将深入讲解 MySQL 查询优化器的工作原理和执行计划分析,这是数据库性能调优的核心技能。通过本章学习,你将掌握:

  • 查询优化器的决策过程
  • 执行计划的成本模型
  • EXPLAIN 的深度分析技巧
  • JOIN 优化策略
  • 排序和分组优化
  • 统计信息与优化器决策

🧩 一、查询优化器是什么?

查询优化器(Query Optimizer)是 MySQL 的"大脑":

它决定 SQL 语句该如何执行、用哪个索引、以何种顺序连接表。

MySQL 的执行分两步:

  1. SQL Parser(解析器):生成语法树
  2. 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.ccSELECT 语句处理
成本计算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_typeSIMPLE / 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 bufferJOIN 过程中未命中索引❌ 建立索引或调整 join 顺序

⚔️ 七、连接优化策略(JOIN Optimization)

优化器会尝试多种连接算法:

JOIN 算法特征说明
Nested Loop Join (NLJ)嵌套循环默认算法,逐行匹配
Block Nested Loop Join (BNL)块循环使用 join buffer 加速
Index Nested Loop Join (INLJ)使用索引加速连接✅ 最常见 & 高效
Hash JoinMySQL 8.0+ 新增适合大表连接

📘 优化建议:

  1. 确保 ON / WHERE 中的 join 字段上有索引
  2. 小表驱动大表(减少外层循环)
  3. 用 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(索引区分度)
  • 若统计信息不准,优化器可能选错索引

📘 调优建议:

  1. 定期更新统计信息
  2. 可使用 USE INDEX 或 FORCE INDEX 临时纠偏
  3. 避免多索引合并(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;

📝 练习题目

基础题

  1. 什么是查询优化器?它的主要作用是什么?
  2. EXPLAIN 中的 type 字段有哪些值?性能如何排序?
  3. 什么是覆盖索引?如何通过 EXPLAIN 识别?

进阶题

  1. 如何分析一个复杂的 JOIN 查询的执行计划?
  2. 统计信息不准确会导致什么问题?如何解决?
  3. 如何优化 ORDER BY 和 GROUP BY 查询?

实战题

  1. 分析一个慢查询,找出性能瓶颈并提出优化方案
  2. 设计一个多表关联查询的索引策略
  3. 如何监控和调优查询优化器的行为?

📚 扩展阅读

  • MySQL 官方文档 - 查询优化
  • MySQL 官方文档 - EXPLAIN 输出格式
  • MySQL 成本模型详解

下一章预告: 第5章:存储与锁机制 - 深入理解 InnoDB 的存储结构和并发控制机制。

Prev
第3章:索引底层实现
Next
第5章:存储引擎深度解析