第 8 章

优化器内核

MySQL 优化器内核深度解析

MySQL查询优化器是战略决策者。理解其成本模型、算法和启发式方法解释了为什么查询以特定方式执行,以及如何引导它朝向更好的计划。

1. 优化器架构


解析树 (AST)
     ↓ 预处理
     ├─ sql_select.cc:简化表达式
     ├─ 删除常数折叠
     ├─ 展平子查询
     └─ 解析列引用
     ↓ 逻辑转换
     ├─ 谓词下推(将WHERE移入JOIN)
     ├─ IN转EXISTS转换
     ├─ 派生过滤条件
     └─ 删除冗余条件
     ↓ 成本估计
     ├─ 分析表统计
     ├─ 构建直方图(MySQL 8.0+)
     ├─ 枚举可能的访问方法
     ├─ 枚举JOIN顺序
     ├─ 计算每个组合的成本
     └─ 缓存成本以重用
     ↓ 计划选择
     ├─ 选择最低成本的计划
     ├─ 应用优化器提示
     └─ 生成执行计划
     ↓ 执行计划(准备执行)

2. 成本模型


MySQL成本模型(MySQL 5.7+):

磁盘I/O成本:
  io_block_read_cost = 1.0(默认)
  读一个块的成本 = 1.0

内存访问成本:
  memory_block_read_cost = 0.25(默认)
  成本低得多(已在内存中)

行评估成本:
  row_evaluate_cost = 0.1(默认)
  检查一行的成本

键比较成本:
  key_compare_cost = 0.05(默认)
  与索引键比较的成本

示例:SELECT * FROM users WHERE id = 1

计划A:全表扫描
├─ 块数:100
├─ 行数:10,000
├─ 成本 = (100 × 1.0) + (10,000 × 0.1) = 1,100

计划B:索引查询
├─ 索引块:1
├─ 键比较:log(10,000) ≈ 14
├─ 成本 = (1 × 1.0) + (14 × 0.05) = 1.8

优化器选择计划B(成本低)

自定义成本模型:

UPDATE mysql.server_cost
SET cost_value = 0.1
WHERE cost_name = 'io_block_read_cost';

FLUSH OPTIMIZER_COSTS;

3. 表统计和索引选择


HISTOGRAM(MySQL 8.0+):

最准确:列值分布

ANALYZE TABLE users UPDATE HISTOGRAM ON country;

直方图显示:
- 频繁国家:'US'(5%)、'CN'(4%)等
- 单例桶用于常见值
- 范围桶用于不常见值

SELECTIVITY估计:

WHERE country = 'US'
├─ 查询:1,000,000行 × 5% = 50,000行(估计)
├─ 过滤成本:较低(扫描行数较少)
└─ 可能启用不同的查询计划

SARGABLE vs非SARGABLE:

SARGABLE(可使用索引):
WHERE id = 5                      # 简单相等
WHERE id > 10 AND id < 20         # 范围与AND
WHERE country = 'US'              # 索引列相等
WHERE id IN (1, 2, 3)            # 值列表

NOT SARGABLE(全表扫描):
WHERE YEAR(date) = 2024          # 列上的函数
WHERE id + 1 = 100               # 列上的表达式
WHERE LOWER(country) = 'us'      # 列上的函数

4. JOIN顺序优化


问题:使用n个表,有(n-1)!个可能的JOIN顺序

SELECT * FROM users u
JOIN orders o ON u.id = o.user_id
JOIN products p ON o.product_id = p.id;

可能的顺序(3! = 6):
1. users → orders → products
2. users → products → orders
3. orders → users → products
...等等

每个成本不同!

优化器方法:

对于小n:尝试所有(n-1)!个顺序
对于大n:使用启发式(限制搜索深度)

对于5个表:4! = 24个计划(详尽搜索)
对于10个表:9! = 362,880个计划(太多!)

GREEDY启发式:

1. 选择成本最低的第一个表
2. 对于下一个表,选择减少结果集最多的
3. 继续直到所有表添加

5. 子查询优化


子查询转换:

原始:IN子查询
SELECT * FROM orders o
WHERE o.user_id IN (
  SELECT id FROM users WHERE country = 'US'
);

问题:每行执行一次子查询
成本:O(orders_count × subquery_cost)

转换1:转换为JOIN
SELECT DISTINCT o.* FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.country = 'US';

子查询物化(MySQL 5.6+):

策略1:DEPENDENT SUBQUERY
├─ 对每个外部行执行子查询
└─ 成本:高(子查询运行多次)

策略2:MATERIALIZED
├─ 执行子查询一次
├─ 结果存入临时表
└─ 成本:低(子查询只运行一次)

控制子查询策略:

-- 强制物化
SELECT /*+ SUBQUERY(MATERIALIZE) */ * FROM orders o
WHERE o.user_id IN (SELECT id FROM users WHERE country = 'US');

6. 直方图分布分析


HISTOGRAMS (MySQL 8.0+):

创建直方图(适合数据倾斜的列):
ANALYZE TABLE orders UPDATE HISTOGRAM ON status USING 100 BUCKETS;

查看直方图:
SELECT *
FROM INFORMATION_SCHEMA.COLUMN_STATISTICS
WHERE TABLE_NAME = 'orders' AND COLUMN_NAME = 'status';

示例直方图数据:
status        | 频率
pending       | 45%(热值)
processing    | 30%
completed     | 20%
cancelled     | 5%

当优化器看到 WHERE status = 'pending':
├─ 直方图显示45%的行
├─ 估计:1,000,000 × 45% = 450,000行
├─ 计算更好的JOIN顺序
└─ 可能选择不同的索引策略

删除直方图:
ANALYZE TABLE orders DROP HISTOGRAM ON status;

7. EXPLAIN和优化器输出


基本EXPLAIN:

mysql> EXPLAIN SELECT * FROM users WHERE country = 'US'\G

rows:50000(过滤前的估计行数)
filtered:45(通过WHERE的百分比)

Extra含义:
├─ Using index:从索引提供查询(覆盖索引)
├─ Using filesort:ORDER BY需要分离排序
├─ Using temporary:GROUP BY需要临时表
└─ Using join buffer:JOIN行缓冲以更好的I/O

JSON格式(MySQL 5.7+):

EXPLAIN FORMAT=JSON SELECT ...\G
{
  "query_block": {
    "cost_info": { "query_cost": "500.50" },
    "table": {
      "table_name": "users",
      "access_type": "range",
      "key": "idx_country",
      "rows_examined_per_scan": 50000
    }
  }
}

分析EXPLAIN:

1. 检查"type"列
   ├─ ALL:全表扫描(差!)
   ├─ range:索引范围扫描(好)
   ├─ ref:索引查询(很好)
   └─ const:单行(最好)

2. 检查"key"列
   ├─ NULL:未使用索引(通常错误)
   ├─ idx_something:使用该索引

3. 检查"rows"列
   ├─ 如果太高
   ├─ 统计信息过时:需要ANALYZE TABLE

4. 检查"Extra"列
   ├─ 无"Using filesort"=好
   ├─ "Using index"=优秀

8. 优化器提示


使用提示覆盖优化器:

INDEX提示:
SELECT * FROM users USE INDEX (idx_country)
WHERE country = 'US';

优化器提示(MySQL 5.7.7+):
SELECT /*+ INDEX(users idx_country) */ * FROM users
WHERE country = 'US';

JOIN_ORDER提示:
SELECT /*+ JOIN_ORDER(o, u, p) */ * FROM orders o
JOIN users u ...;

MAX_EXECUTION_TIME:
SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM ...;
-- 如果超过1000ms就杀死查询

何时使用提示:
✓ 统计信息非常陈旧
✓ 优化器一贯错误
✓ 特定查询需要特殊处理

✗ 常规优化(使用ANALYZE TABLE代替)
✗ 隐藏根本问题(暂时使用提示)

9. 关键优化要点


1. 定期ANALYZE TABLE
   └─ 确保统计信息最新

2. 使用直方图(MySQL 8.0+)
   └─ 改进倾斜列的估计

3. 编写可索引查询
   └─ 避免列上的函数

4. 监控EXPLAIN输出
   └─ 检查意外的全表扫描

5. 战略性地创建索引
   └─ 高选择性列优先

6. 保持统计信息新鲜
   └─ 定期安排ANALYZE TABLE

7. 理解你的数据
   └─ 知道列分布
   └─ 预期数据倾斜

8. 测试后再投入生产
   └─ 运行EXPLAIN
   └─ 基准测试不同计划

9. 谨慎使用提示
   └─ 记录为什么需要提示

10. 关注优化器进化
    └─ MySQL 8.0+有超图优化器
    └─ 升级后测试

总结

MySQL优化器基于统计、成本模型和启发式做出决策。理解这些内部机制让你编写优化器理解的查询、诊断为什么计划是次优的,以及通过索引、统计和提示引导优化器做出更好的决策。

本章评分
4.8  / 5  (53 评分)

💬 留言讨论