MySQL 查询优化器如何选择执行计划与索引

MySQL 查询优化器是数据库性能的核心组件之一,它负责将 SQL 语句转换为高效的执行计划,并决定使用哪些索引。理解其工作原理,不仅能帮助你在面试中脱颖而出,更能指导日常的 SQL 优化实践。

一、查询优化器的作用与架构

当一条 SQL 到达 MySQL Server 层时,会经历解析器(Parser)→ 预处理器(Preprocessor)→ 查询优化器(Optimizer)→ 执行器(Executor)几个阶段。优化器的核心任务是在语义等价的前提下,寻找代价最小的执行路径。

优化器主要完成两类工作:

  1. 逻辑优化:如子查询展开、外连接转内连接、常量折叠、谓词下推、等价类推导等。
  2. 物理优化:选择访问路径(全表扫描 vs 索引扫描)、选择连接算法(Nested Loop Join、Hash Join、BNL)、决定连接顺序。

二、基于代价的优化模型(CBO)

MySQL 采用基于代价的优化器(Cost-Based Optimizer)。它并不保证找到理论最优解,而是在有限时间内找到一个“足够好”的计划。代价模型主要估算两类开销:

  • I/O 代价:读取数据页的成本。默认 innodb_page_size = 16KB,优化器通过统计信息估算需要读取多少页。
  • CPU 代价:比较、排序、聚合等操作的成本。

总代价公式可简化为:

Total Cost = IO_cost + CPU_cost

优化器会枚举多个候选执行计划,分别计算代价,最终选择代价最低者。

三、统计信息:优化器的“眼睛”

没有准确的统计信息,CBO 就是“盲人摸象”。MySQL 的统计信息主要来自:

  • 表统计SHOW TABLE STATUS 中的 Rows(表行数估算)、Data_length 等。
  • 索引统计SHOW INDEX FROM table 中的 Cardinality(索引列唯一值数量),它决定了索引的选择性。
  • 直方图(MySQL 8.0+):information_schema.COLUMN_STATISTICS,用于描述列的数据分布,对非均匀分布列尤为重要。

统计信息由后台线程ANALYZE TABLE 更新。innodb_stats_auto_recalc 控制是否自动更新,innodb_stats_persistent 决定是否持久化存储。

面试常问:为什么索引明明存在,优化器却不用?
常见原因:Cardinality 过低(如性别列)、统计信息过期、查询返回行数占比过高(如超过 20%~30%)、使用了函数或隐式类型转换导致索引失效。

四、访问路径的选择

对于单表查询,优化器主要在以下访问方法中选择:

  1. 全表扫描(ALL):当需要读取大部分数据行时,顺序 I/O 比随机 I/O 更划算。
  2. 索引扫描(index):遍历整棵二级索引树,通常比全表扫描快,因为索引更小。
  3. 范围扫描(range):用于 >、<、BETWEEN、IN 等范围条件。
  4. ref / eq_ref:等值匹配,eq_ref 用于主键或唯一索引连接。
  5. const:通过主键或唯一索引与常量比较,最多返回一行。
  6. index merge:对多个索引分别扫描后合并(交集、并集),实际使用较少,因为效率常不如复合索引。

优化器如何决策?核心是选择性(Selectivity):

选择性 = 不重复值数量 / 总行数

选择性越接近 1,索引过滤效果越好。例如 user_id 选择性高,适合建索引;status 只有几个值,选择性低,全表扫描可能更优。

五、连接顺序与连接算法

多表连接时,优化器需要决定:

  • 驱动表(outer table)与被驱动表(inner table)的顺序。
  • 使用哪种连接算法。

MySQL 常用算法:

  • Nested Loop Join(NLJ):驱动表每行去被驱动表匹配,适合被驱动表连接列有索引。
  • Block Nested Loop(BNL):无索引时使用 join buffer 批量匹配,MySQL 8.0.18 前常用。
  • Hash Join:MySQL 8.0.18+ 引入,适合等值连接且无可用索引的场景。

优化器倾向于选择结果集小的表作为驱动表,以减少外层循环次数。STRAIGHT_JOIN 可强制指定连接顺序,但应谨慎使用。

六、如何查看与分析执行计划

使用 EXPLAINEXPLAIN FORMAT=JSON 查看执行计划。关键字段:

字段 含义
type 访问类型,性能:system > const > eq_ref > ref > range > index > ALL
key 实际使用的索引
rows 预估扫描行数
filtered 过滤后剩余行数百分比
Extra 额外信息,如 Using index、Using filesort、Using temporary

EXPLAIN ANALYZE(8.0.18+)可输出实际执行的耗时与行数,对比预估可发现统计信息偏差。

七、优化器提示(Optimizer Hints)

当优化器选错计划时,可用 Hint 干预:

SELECT /*+ INDEX(t idx_name) */ * FROM t WHERE ...;
SELECT /*+ NO_INDEX(t idx_name) */ * FROM t WHERE ...;
SELECT /*+ JOIN_ORDER(t1, t2) */ ...;

但 Hint 是“最后手段”,优先应通过更新统计信息、调整索引或重写 SQL 解决。

八、面试高频问题总结

  1. 优化器为什么有时不走索引?
    统计信息不准、选择性低、回表代价高、隐式转换、函数操作、OR 条件等。

  2. Cardinality 是什么?如何更新?
    索引列唯一值估算,通过 ANALYZE TABLE 或自动更新。

  3. EXPLAIN 中 type 为 ALL 一定差吗?
    不一定。若表很小或需返回大部分数据,全表扫描反而更优。

  4. 如何强制使用某个索引?
    FORCE INDEXUSE INDEX,但需评估副作用。

  5. MySQL 8.0 优化器有哪些改进?
    直方图、Hash Join、降序索引、EXPLAIN ANALYZE、优化器提示增强等。

九、实践建议

  • 定期 ANALYZE TABLE,尤其在大批量数据变更后。
  • 为高选择性列建立索引,优先考虑复合索引的最左前缀。
  • 避免在索引列上使用函数、类型转换、前导通配符 LIKE '%x'
  • EXPLAIN 验证每个慢查询的执行计划。
  • 理解业务数据分布,必要时用直方图辅助优化器。

掌握查询优化器的工作原理,是从“会写 SQL”到“写好 SQL”的关键一步。在面试中,能结合统计信息、代价模型和执行计划综合分析,往往能体现出扎实的数据库功底。

未经允许不得转载:任鹏个人博客 » MySQL 查询优化器如何选择执行计划与索引

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏