news 2026/2/27 6:00:00

MySQL 查询优化器 (Query Optimizer) 详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL 查询优化器 (Query Optimizer) 详解

一、MySQL优化器概述

1.1 什么是查询优化器

查询优化器(Query Optimizer)是MySQL的核心组件,负责将SQL语句转换为最优的执行计划。

工作流程:

SQL语句 → 解析器(Parser) → 优化器(Optimizer) → 执行器(Executor) → 存储引擎

优化器的主要职责:

  • 选择最优的索引
  • 确定表的连接顺序
  • 选择合适的连接算法
  • 优化子查询
  • 简化和重写查询语句

1.2 优化器类型

MySQL主要有两种优化器:

  1. 基于规则的优化器(RBO - Rule-Based Optimizer)

    • 基于预定义的规则进行优化
    • 较为简单,但不够灵活
  2. 基于成本的优化器(CBO - Cost-Based Optimizer)

    • MySQL主要使用这种
    • 通过计算各种执行计划的成本,选择成本最低的
    • 依赖统计信息

二、优化器的工作原理

2.1 成本模型

MySQL优化器通过成本模型评估不同执行计划的代价。

成本计算因素:

总成本=I/O成本+CPU成本-- I/O成本: 从磁盘读取数据的成本-- CPU成本: 处理数据(比较、排序)的成本

成本常量(MySQL 5.7+):

-- 查看成本常量SELECT*FROMmysql.server_cost;SELECT*FROMmysql.engine_cost;-- 主要成本参数:-- disk_temptable_create_cost: 创建临时表成本(默认20.0)-- disk_temptable_row_cost: 临时表行读取成本(默认0.5)-- key_compare_cost: 键比较成本(默认0.05)-- memory_temptable_create_cost: 内存临时表创建成本(默认1.0)-- memory_temptable_row_cost: 内存临时表行成本(默认0.1)-- row_evaluate_cost: 行评估成本(默认0.1)

2.2 统计信息

优化器依赖表和索引的统计信息做决策。

-- 查看表统计信息SHOWTABLESTATUSLIKE'table_name'\G-- 查看索引统计信息SHOWINDEXFROMtable_name;-- 关键统计指标:-- Cardinality: 索引中唯一值的数量(区分度)-- Rows: 表中的行数-- Data_length: 数据文件大小-- Index_length: 索引文件大小-- 更新统计信息ANALYZETABLEtable_name;

统计信息采样:

-- InnoDB统计信息采样设置SHOWVARIABLESLIKE'innodb_stats%';-- innodb_stats_persistent: 持久化统计信息(ON/OFF)-- innodb_stats_auto_recalc: 自动重新计算统计信息-- innodb_stats_sample_pages: 采样页数(默认8)

三、优化器的优化策略

3.1 条件简化和优化

常量传播:

-- 原始SQLSELECT*FROMtWHEREa=5ANDb=a;-- 优化后SELECT*FROMtWHEREa=5ANDb=5;

恒等式消除:

-- 原始SQLSELECT*FROMtWHEREa>3ANDa>5;-- 优化后SELECT*FROMtWHEREa>5;

范围合并:

-- 原始SQLSELECT*FROMtWHERE(a>1ANDa<5)OR(a>3ANDa<7);-- 优化后SELECT*FROMtWHEREa>1ANDa<7;

3.2 索引选择

优化器通过以下步骤选择索引:

1. 找出所有可能的索引:

EXPLAINSELECT*FROMuserWHEREage=25ANDname='张三';-- possible_keys 显示所有可能使用的索引

2. 计算每个索引的成本:

-- 成本计算公式(简化版):成本=(扫描的数据页数 × I/O成本)+(处理的记录数 × CPU成本)

3. 选择成本最低的索引

示例分析:

-- 表结构CREATETABLEuser(idINTPRIMARYKEY,ageINT,nameVARCHAR(50),cityVARCHAR(50),INDEXidx_age(age),INDEXidx_name(name),INDEXidx_age_name(age,name));-- 查询1: 优化器会选择 idx_age_name(覆盖索引)EXPLAINSELECTage,nameFROMuserWHEREage=25;-- 查询2: 如果需要所有字段,可能选择 idx_age(需要回表)EXPLAINSELECT*FROMuserWHEREage=25;-- 使用 optimizer_trace 查看详细过程SEToptimizer_trace='enabled=on';SELECT*FROMuserWHEREage=25;SELECT*FROMinformation_schema.OPTIMIZER_TRACE\GSEToptimizer_trace='enabled=off';

3.3 JOIN优化

连接顺序优化:

-- 三表连接SELECT*FROMt1JOINt2ONt1.id=t2.t1_idJOINt3ONt2.id=t3.t2_idWHEREt1.status=1;-- 优化器会评估6种连接顺序(3! = 6):-- t1 → t2 → t3-- t1 → t3 → t2-- t2 → t1 → t3-- t2 → t3 → t1-- t3 → t1 → t2-- t3 → t2 → t1

连接算法选择:

  1. 嵌套循环连接(Nested-Loop Join)
-- 简单嵌套循环(Simple Nested-Loop)for each rowint1:for each rowint2:ifrowmatchesjoincondition: outputrow-- 时间复杂度: O(n * m)
  1. 索引嵌套循环(Index Nested-Loop Join)
-- 使用索引加速内表查询for each rowint1:useindextofind matchingrowsint2 outputmatchedrows-- 时间复杂度: O(n * log m)
  1. 块嵌套循环(Block Nested-Loop Join)
-- 使用join buffer缓存外表数据-- MySQL 8.0+ 使用Hash Join替代-- 查看join buffer大小SHOWVARIABLESLIKE'join_buffer_size';
  1. Hash Join(MySQL 8.0.18+)
-- 构建哈希表,性能更好-- 适用于等值连接EXPLAINFORMAT=TREESELECT*FROMt1JOINt2ONt1.id=t2.id;-- 可以看到 "Hash Join" 字样

JOIN优化建议:

-- ✅ 小表驱动大表SELECT*FROMsmall_table t1JOINlarge_table t2ONt1.id=t2.small_id;-- ✅ 确保JOIN字段有索引ALTERTABLEt2ADDINDEXidx_small_id(small_id);-- ✅ 使用STRAIGHT_JOIN强制连接顺序(谨慎使用)SELECT*FROMt1 STRAIGHT_JOIN t2ONt1.id=t2.t1_id;

3.4 子查询优化

子查询转换策略:

1. 子查询物化(Subquery Materialization):

-- 原始SQLSELECT*FROMt1WHEREidIN(SELECTt1_idFROMt2WHEREstatus=1);-- 优化过程:-- 1. 先执行子查询,结果存入临时表-- 2. 临时表加索引-- 3. 用临时表进行JOIN-- EXPLAIN 中看到 "MATERIALIZED"

2. 子查询转JOIN:

-- 原始SQL(相关子查询)SELECT*FROMt1WHEREEXISTS(SELECT1FROMt2WHEREt2.t1_id=t1.id);-- 优化后(Semi-Join)SELECTt1.*FROMt1 SEMIJOINt2ONt2.t1_id=t1.id;

3. 子查询展开:

-- 原始SQLSELECT*FROMt1WHERE(SELECTCOUNT(*)FROMt2WHEREt2.t1_id=t1.id)>5;-- 优化后SELECTt1.*FROMt1JOIN(SELECTt1_id,COUNT(*)ascntFROMt2GROUPBYt1_idHAVINGcnt>5)t2ONt1.id=t2.t1_id;

控制子查询优化:

-- 查看子查询优化策略SHOWVARIABLESLIKE'optimizer_switch';-- 关键参数:-- materialization: 子查询物化-- semijoin: 半连接优化-- subquery_materialization_cost_based: 基于成本选择

3.5 ORDER BY 和 GROUP BY 优化

索引排序 vs 文件排序:

-- ✅ 使用索引排序(Index Scan)-- 假设有索引 idx_age(age)EXPLAINSELECT*FROMuserORDERBYage;-- Extra: Using index-- ❌ 使用文件排序(Filesort)EXPLAINSELECT*FROMuserORDERBYname;-- Extra: Using filesort-- Filesort过程:-- 1. 根据WHERE条件读取数据-- 2. 将需要排序的字段放入sort buffer-- 3. 如果数据量大于sort_buffer_size,使用磁盘临时文件-- 4. 进行排序(快速排序)

GROUP BY优化:

-- ✅ 松散索引扫描(Loose Index Scan)-- 假设索引 idx_age_city(age, city)EXPLAINSELECTage,COUNT(*)FROMuserGROUPBYage;-- Extra: Using index for group-by-- ✅ 紧凑索引扫描(Tight Index Scan)EXPLAINSELECTage,city,COUNT(*)FROMuserWHEREage>20GROUPBYage,city;-- Extra: Using where; Using index-- ❌ 临时表分组EXPLAINSELECTcity,COUNT(*)FROMuserGROUPBYcity;-- Extra: Using temporary; Using filesort

优化配置:

-- 查看排序缓冲区大小SHOWVARIABLESLIKE'sort_buffer_size';-- 默认256KBSHOWVARIABLESLIKE'max_length_for_sort_data';-- 默认1024-- 临时表相关SHOWVARIABLESLIKE'tmp_table_size';-- 内存临时表大小SHOWVARIABLESLIKE'max_heap_table_size';-- 堆表最大值

四、优化器提示(Hint)

4.1 索引提示

-- 强制使用某个索引SELECT*FROMuserFORCEINDEX(idx_age)WHEREage=25;-- 建议使用某个索引(优化器可能忽略)SELECT*FROMuserUSEINDEX(idx_age)WHEREage=25;-- 忽略某个索引SELECT*FROMuserIGNOREINDEX(idx_age)WHEREage=25;-- MySQL 8.0+ 新语法SELECT/*+ INDEX(user idx_age) */*FROMuserWHEREage=25;

4.2 JOIN提示

-- 强制连接顺序SELECT*FROMt1 STRAIGHT_JOIN t2ONt1.id=t2.t1_id;-- MySQL 8.0+ JOIN提示SELECT/*+ JOIN_ORDER(t1, t2, t3) */*FROMt1,t2,t3WHEREt1.id=t2.t1_idANDt2.id=t3.t2_id;-- 指定JOIN算法SELECT/*+ BNL(t1, t2) */*-- Block Nested-LoopFROMt1JOINt2ONt1.id=t2.id;SELECT/*+ HASH_JOIN(t1, t2) */*-- Hash Join(8.0.18+)FROMt1JOINt2ONt1.id=t2.id;

4.3 其他提示

-- 子查询物化SELECT/*+ SUBQUERY(MATERIALIZATION) */*FROMt1WHEREidIN(SELECTt1_idFROMt2);-- 指定临时表使用内存SELECT/*+ SET_VAR(internal_tmp_mem_storage_engine=TempTable) */age,COUNT(*)FROMuserGROUPBYage;-- 限制执行时间(8.0+)SELECT/*+ MAX_EXECUTION_TIME(1000) */*FROMuser;-- 1秒超时-- 查看所有可用提示SELECT/*+ QB_NAME(qb1) */*FROMt1;

五、优化器跟踪

5.1 使用optimizer_trace

-- 开启优化器跟踪SEToptimizer_trace='enabled=on';-- 执行查询SELECT*FROMuserWHEREage=25ANDname='张三';-- 查看优化过程SELECT*FROMinformation_schema.OPTIMIZER_TRACE\G-- 关闭跟踪SEToptimizer_trace='enabled=off';

trace信息解读:

{"steps":[{"join_preparation":{"select_id":1,"steps":[{"expanded_query":"/* 展开后的查询 */"}]}},{"join_optimization":{"select_id":1,"steps":[{"condition_processing":{/* 条件优化过程 */}},{"table_dependencies":[/* 表依赖关系 */]},{"rows_estimation":[/* 行数估算 */{"table":"user","range_analysis":{"potential_range_indexes":[/* 可能使用的索引 */],"analyzing_range_alternatives":{/* 分析每个索引的成本 */"range_scan_alternatives":[{"index":"idx_age","ranges":["25 <= age <= 25"],"rows":100,"cost":121}]},"chosen_range_access_summary":{/* 选择的索引 */"range_access_plan":{"type":"range_scan","index":"idx_age","rows":100,"cost":121}}}}]},{"considered_execution_plans":[/* 考虑的执行计划 */{"plan_prefix":[],"table":"user","best_access_path":{/* 最佳访问路径 */}}]},{"attaching_conditions_to_tables":{/* 附加条件到表 */}}]}},{"join_execution":{/* 执行阶段 */}}]}

5.2 使用EXPLAIN详细分析

-- 传统EXPLAINEXPLAINSELECT*FROMuserWHEREage=25;-- 格式化输出(MySQL 8.0+)EXPLAINFORMAT=TREESELECT*FROMuserWHEREage=25;EXPLAINFORMAT=JSONSELECT*FROMuserWHEREage=25;-- 查看实际执行统计(MySQL 8.0.18+)EXPLAINANALYZESELECT*FROMuserWHEREage=25;

EXPLAIN关键字段详解:

字段说明重要值
id查询序列号数字越大越先执行
select_type查询类型SIMPLE, PRIMARY, SUBQUERY, DERIVED
table表名实际表名或别名
partitions分区匹配的分区
type访问类型system > const > eq_ref > ref > range > index > ALL
possible_keys可能的索引候选索引列表
key实际索引实际使用的索引
key_len索引长度使用的索引字节数
ref引用与索引比较的列
rows扫描行数预估扫描的行数
filtered过滤百分比满足条件的行百分比
Extra额外信息Using index, Using where, Using filesort等

type类型详解:

-- system: 表只有一行(系统表)-- const: 通过主键或唯一索引查询,最多返回一行EXPLAINSELECT*FROMuserWHEREid=1;-- eq_ref: 唯一索引扫描,用于JOINEXPLAINSELECT*FROMt1JOINt2ONt1.id=t2.id;-- ref: 非唯一索引扫描EXPLAINSELECT*FROMuserWHEREage=25;-- range: 范围扫描EXPLAINSELECT*FROMuserWHEREageBETWEEN20AND30;-- index: 全索引扫描EXPLAINSELECTidFROMuser;-- ALL: 全表扫描(最差)EXPLAINSELECT*FROMuserWHEREname='张三';-- name无索引

六、优化器常见问题

6.1 优化器选错索引

原因:

  • 统计信息不准确
  • 成本估算偏差
  • 数据分布不均匀

解决方案:

-- 1. 更新统计信息ANALYZETABLEuser;-- 2. 使用索引提示SELECT*FROMuserFORCEINDEX(idx_age)WHEREage=25;-- 3. 调整优化器参数SEToptimizer_search_depth=5;-- 控制JOIN搜索深度SEToptimizer_prune_level=1;-- 启用优化器剪枝-- 4. 修改索引或查询-- 例如:添加更合适的组合索引

6.2 JOIN顺序不优

-- 查看JOIN顺序EXPLAINFORMAT=TREESELECT*FROMlarge_table t1JOINsmall_table t2ONt1.id=t2.large_id;-- 如果顺序不对,使用STRAIGHT_JOINSELECT*FROMsmall_table t2 STRAIGHT_JOIN large_table t1ONt1.id=t2.large_id;

6.3 子查询性能差

-- ❌ 相关子查询(每行都执行一次)SELECT*FROMt1WHERE(SELECTCOUNT(*)FROMt2WHEREt2.t1_id=t1.id)>5;-- ✅ 改写为JOINSELECTt1.*FROMt1JOIN(SELECTt1_idFROMt2GROUPBYt1_idHAVINGCOUNT(*)>5)t2ONt1.id=t2.t1_id;-- ✅ 或使用EXISTSSELECT*FROMt1WHEREEXISTS(SELECT1FROMt2WHEREt2.t1_id=t1.idGROUPBYt1_idHAVINGCOUNT(*)>5);

七、优化器最佳实践

7.1 定期维护

-- 1. 定期更新统计信息ANALYZETABLEuser;-- 2. 优化表(重建索引,回收空间)OPTIMIZETABLEuser;-- 3. 检查表CHECKTABLEuser;-- 4. 修复表REPAIRTABLEuser;

7.2 监控慢查询

-- 开启慢查询日志SETGLOBALslow_query_log=ON;SETGLOBALlong_query_time=1;-- 1秒SETGLOBALlog_queries_not_using_indexes=ON;-- 查看慢查询日志位置SHOWVARIABLESLIKE'slow_query_log_file';-- 分析慢查询日志(使用mysqldumpslow工具)-- mysqldumpslow -s t -t 10 /path/to/slow.log

7.3 使用性能监控

-- Performance SchemaSELECT*FROMperformance_schema.events_statements_summary_by_digestORDERBYSUM_TIMER_WAITDESCLIMIT10;-- 查看索引使用情况SELECT*FROMsys.schema_unused_indexes;SELECT*FROMsys.schema_redundant_indexes;

7.4 版本升级建议

  • MySQL 5.7: 引入成本模型,优化器改进
  • MySQL 8.0: Hash Join, CTE, Window Function, Invisible Index
  • MySQL 8.0.18+: EXPLAIN ANALYZE(实际执行统计)
  • MySQL 8.0.20+: Hash Join默认启用
-- 查看MySQL版本SELECTVERSION();-- 查看优化器特性SHOWVARIABLESLIKE'optimizer_switch';

八、总结

MySQL优化器是一个复杂的系统,理解其工作原理有助于:

  1. 编写更高效的SQL
  2. 设计合理的索引
  3. 排查性能问题
  4. 合理使用优化器提示

核心要点:

  • 优化器基于成本模型选择执行计划
  • 依赖准确的统计信息
  • 需要定期维护(ANALYZE TABLE)
  • 使用EXPLAIN分析执行计划
  • 谨慎使用优化器提示
  • 关注MySQL版本新特性
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/25 5:04:53

FCKEditor支持Word图片上传转存保留矢量格式属性

企业级文档导入功能集成方案 1. 需求分析与技术选型 1.1 核心需求 Word粘贴导入功能&#xff1a;支持从Word、Excel、PPT、PDF导入&#xff0c;保留样式&#xff08;表格、公式、字体等&#xff09;。微信公众号内容解析&#xff1a;自动下载图片并上传至服务器&#xff08;…

作者头像 李华
网站建设 2026/2/21 22:59:18

FCKEditor实现Word图片转存服务器路径映射配置

&#x1f468;&#x1f4bb; Word图片一键转存功能开发日记 &#x1f50d; 寻找解决方案的漫漫长路 作为一个即将毕业的吉林软件工程专业大三学生&#xff0c;我最近在给我的CMS新闻管理系统添加一个超实用的功能 - Word文档一键粘贴并自动上传图片&#xff01;这简直是内容编…

作者头像 李华
网站建设 2026/2/22 0:29:35

前端如何配合.NET Core实现大文件上传的拖拽上传与进度显示?

大三学长毕业设计救星&#xff1a;原生JS大文件传输系统&#xff08;附完整代码&#xff09; 兄弟&#xff0c;作为刚摸爬滚打完毕设的通讯专业学长&#xff0c;太懂你现在的处境了——找工作要作品&#xff0c;大文件上传需求卡壳&#xff0c;网上开源代码全是“断头路”&…

作者头像 李华
网站建设 2026/2/18 1:56:21

C#如何利用组件实现.NET Core大文件上传的批量处理功能?

大文件传输解决方案设计书 项目背景与需求分析 作为浙江某软件公司项目负责人&#xff0c;我们目前面临一个重大技术挑战&#xff1a;需要在现有产品体系中集成一个高可靠性的大文件传输系统。经过详细需求分析&#xff0c;主要技术指标如下&#xff1a; 大文件支持&#xf…

作者头像 李华
网站建设 2026/2/23 12:01:24

光伏系统最大功率点跟踪(MPPT)是个挺有意思的话题。今天咱们聊聊怎么用模糊控制搞这个事。先别急着翻教科书,直接上仿真代码更带劲

光伏MPPT仿真-模糊控制先整点基础设定&#xff1a;光伏板的数学模型。用Python写的话大概长这样&#xff1a; def pv_curve(V, T25, G1000):Isc 3.45 * (G/1000) Voc 21.7 * (1 - 0.0028*(T-25))Imp 3.15 * (G/1000)Vmp 17.5 * (1 - 0.0028*(T-25))return Imp - (Imp/(Vmp*…

作者头像 李华