数据结构 · 核心功能 · 工作流程 · 代价模型 · 优化策略 · 实战调优
MySQL 优化器内部围绕三个核心对象展开:Query_block(查询块)→ JOIN(连接)→ JOIN_TAB(连接表),每层封装不同粒度的优化信息。
// sql/sql_optimizer.h — 核心数据结构 // 查询块:代表一个 SELECT 语句的所有信息 struct Query_block { LEX *lex; // 词法分析上下文指针 TABLE_LIST *leaf_tables; // 叶子表(去掉派生表的真实表) Item *where_cond; // WHERE 条件 Item 树 ORDER *group_list; // GROUP BY 链表 ORDER *order; // ORDER BY 链表 JOIN *join; // 指向关联优化对象 Key_usage *key_usage_array;// 所有可用的索引访问方式 uint select_num; // 查询块序号(1,2,3...) }; // JOIN:整个查询的连接对象,管理所有表的优化决策 struct JOIN { THD *thd; // 线程句柄 JOIN_TAB *join_tab; // 所有表的 JOIN_TAB 数组 uint tables; // 参与 JOIN 的表总数 uint const_tables; // 被判定为常量(最多1行)的表数 double best_read; // 最优计划的估算总代价 ha_rows best_rowcount; // 最优计划估算的总行数 JOIN_TAB *best_ref[]; // 最优连接顺序的表数组 }; // JOIN_TAB:每张参与 JOIN 的表在优化器中的抽象 struct JOIN_TAB { TABLE *table; // 关联的物理表对象 Key_use *keyuse; // 此表可用的所有索引访问方式 ha_rows records; // 估算结果行数(来自统计信息) double read_time; // 此表访问代价估算 enum join_type type; // ALL / ref / range / eq_ref... Key_AND *const_keys; // 可用常量等值连接的索引 Item *condition; // WHERE 过滤条件 enum enum_impossible read_cost; // 是否可直接排除 };
将 WHERE id = 5 这类主键等值条件在优化前就"物化"为常量,节省后续优化时间。如果找不到任何行,直接标记 Impossible 跳过执行。
将 WHERE 条件下推到离数据源最近的地方执行,减少中间结果集行数。例如 JOIN 时把 t2 的条件直接写入 t2 的扫描层,而非等到 JOIN 后再过滤。
MySQL 5.6+ 会将 IN (SELECT...) 转为半连接(Semi Join),避免子查询结果物化导致的性能退化。无法改写的子查询则用 EXISTS 策略执行。
对每张表枚举所有可能的访问方式:全表扫描 / 索引扫描 / 索引范围扫描,计算每种方式的 read_time。通过 records/key_len/condition 估算。
当表数量 ≤ optimizer_search_depth(默认 7)时,使用动态规划穷举所有排列,选最优。当超过时退化为贪心算法,每步选使总代价下降最多的表作为下一张表。
对每张表枚举可能的索引访问方式,计算每种方式的读取代价:
| 访问方式 | 触发条件 | 代价估算公式 | 优先级 |
|---|---|---|---|
const |
主键/唯一索引 等值,返回 ≤1 行 | ≈ 1(一次性读取,无需扫描) | ⭐⭐⭐ 最高 |
eq_ref |
被驱动表,JOIN 时唯一索引等值 | 外层行数 × 1次唯一查找 | ⭐⭐⭐ 高 |
ref |
非唯一索引等值,可返回多行 | 外层行数 × 索引扫描 + 回表 | ⭐⭐ 中高 |
range |
BETWEEN / IN / > < 等范围条件 | 范围行数 × 索引读取 + 回表 | ⭐⭐ 中 |
index |
全索引扫描(不回表) | 索引页数 × IO_cost | ⭐ 低 |
ALL |
无索引可用时 | 表总页数 × IO_cost + 行数 × CPU_cost | ❌ 最高代价 |
JOIN 顺序是影响性能最显著的因素之一。优化器通过穷举/贪心找出最优连接顺序:
优化器决策高度依赖统计信息,主要来源:
-- 查看表统计信息 SELECT table_name, n_rows, clustered_index_size, sum_of_other_index_sizes FROM mysql.innodb_table_stats WHERE database_name = 'your_db' AND table_name = 'orders'; -- 查看索引基数(Cardinality)— 索引选择决策的核心 SHOW INDEX FROM orders; -- 手动重新收集统计信息(统计信息过期时使用) ANALYZE TABLE orders; -- MySQL 8.0 直方图 ANALYZE TABLE orders UPDATE HISTOGRAM ON status, create_time; -- 查看直方图 SELECT * FROM information_schema.COLUMN_STATISTICS;
ANALYZE TABLE 重新收集。
MySQL 代价模型将每次 I/O 读写和每次 CPU 处理量化为数值:
-- 优化器代价常量存储在 MySQL 内部 SELECT * FROM mysql.server_cost; -- 服务器级代价 SELECT * FROM mysql.engine_cost; -- 引擎级代价(InnoDB / MyISAM 可分别配置) -- 代价计算示例 总代价 = IO_cost + CPU_cost = (页读取数 × page_io_cost) + (行处理数 × row_cpu_cost) + (key比较数 × key_cpu_cost) -- 默认代价常量(可调整) page_io_cost = 1.00 -- 读一个数据页的 IO 代价 row_cpu_cost = 0.10 -- 处理一行的 CPU 代价 key_cpu_cost = 0.05 -- 一次 key 比较的 CPU 代价 memory_temp = 1.00 -- 内存临时表页代价 disk_temp = 5.00 -- 磁盘临时表页代价(5倍内存)
server_cost / engine_cost 表中的参数,可以让优化器更倾向索引扫描(提高 row_cpu_cost)或更倾向全表扫描(降低 page_io_cost)。
优化器内置了数十种优化规则,这里展示最核心的 12 种策略及其执行时机:
| Hint | 作用 | 示例 |
|---|---|---|
USE INDEX / FORCE INDEX / IGNORE INDEX |
强制使用 / 忽略指定索引 | SELECT * FROM t USE INDEX(idx_name) WHERE... |
STRAIGHT_JOIN |
按 SQL 书写顺序固定 JOIN 顺序 | SELECT STRAIGHT_JOIN * FROM t1 JOIN t2... |
ORDER BY / GROUP BY |
直接使用索引满足 ORDER BY / GROUP BY,避免排序 | 建复合索引覆盖列顺序 |
SET optimizer_switch |
全局开关各优化策略(ICP/MRR/BKA等) | SET optimizer_switch='mrr=on,index_condition_pushdown=on' |
SET optimizer_switch='batched_key_access=off' |
关闭 BKA(出 bug 时临时关闭) | 关闭后回退到 Nested Loop Join |
SET optimizer_hint |
MySQL 8.0 Hint API(更精细化控制) | SET optimizer_hint='max_seeks_for_key=1000' |
💡 点击开关可交互模拟(实际效果需在 MySQL 中执行 SET 语句)
-- 1. 查看执行计划 EXPLAIN SELECT ...; -- 2. 真实执行分析(MySQL 8.0+)— 会真正执行并返回实际时间 EXPLAIN ANALYZE SELECT ...; -- 3. JSON 格式,代价分解(MySQL 5.7+) EXPLAIN FORMAT=JSON SELECT ...; -- 4. 查看优化器最终选择的 JOIN 顺序 EXPLAIN FORMAT=JSON SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id; -- 5. 强制使用某个索引(测试用) SELECT * FROM orders FORCE INDEX(idx_user_id) WHERE user_id = 123; -- 6. 查看统计信息状态 SHOW TABLE STATUS FROM your_db LIKE 'orders'; SHOW INDEX FROM orders; -- 7. 重新收集统计信息 ANALYZE TABLE orders; -- 8. 查看优化器开关状态 SELECT @@optimizer_switch; -- 9. 查看搜索深度(超过此值使用贪心) SELECT @@optimizer_search_depth; -- 10. 临时修改优化器参数(会话级) SET @@session.optimizer_search_depth = 7; SET @@session.optimizer_switch = 'mrr=off';