type
status
date
slug
summary
tags
category
icon
password
catalog
sort
前言:从“一根筋”到“多线程”的优化器革命
上一篇咱们聊了官方文档里的优化器核心知识,从基础流程到分区裁剪、范围优化,这些都是优化器的“基本功”。但如果你以为优化器就这点本事,那可就太小看它了。今天咱们要聊的是MySQL优化器的“最强大脑”——超图优化器(Hypergraph Optimizer)。
这玩意儿可是MySQL 8.0.22版本的“王牌升级”,直接把多表连接的优化能力提升了一个维度。在这之前,优化器处理多表连接就像个“一根筋”的老司机,只能按固定顺序一个接一个地连表;而超图优化器就像个“多线程导航系统”,能同时规划多条路线,选出最优方案。
我当年第一次在生产环境启用超图优化器时,一个跑了5分钟的10表连接报表查询,直接降到了45秒,当时整个团队都惊呆了。这一章咱们就把超图优化器扒个底掉,从原理到源码,从实战到调优,保证让你看完就懂,懂了就能用。
3.1 超图优化器诞生记:为什么需要“新大脑”?
在超图优化器出现之前,MySQL用的是“贪心算法”来优化连接顺序,这玩意儿在简单场景下还行,但遇到多表连接就拉胯了。咱们先聊聊旧优化器的“痛点”,你就知道为啥超图优化器是“刚需”了。
3.1.1 旧优化器的“死穴”:左深树的局限
旧优化器处理多表连接时,只会生成“左深树”(Left-Deep Tree)的连接顺序。啥是左深树?简单说就是“串糖葫芦”式的连接:先连表A和表B,结果再连表C,再连表D……就像下面这样:
这种方式最大的问题是“路径单一”,只能从左到右依次加表,没法跳着连,也没法同时连多个表组。比如有4个表A、B、C、D,其中A和B关联紧密(数据量小),C和D关联紧密(数据量大),最优顺序应该是先连A+B,再连C+D,最后把两个结果连起来。但旧优化器做不到,它只能按A→B→C→D或者A→C→B→D这样的顺序,很容易选到低效路线。
我之前遇到过一个电商的订单分析查询,要关联用户表、订单表、商品表、库存表4个表。旧优化器选了“订单表→用户表→商品表→库存表”的顺序,订单表是大表(500万行),导致后面每个连接都要处理大量数据,查询跑了8分钟。后来我手动改了连接顺序,先连小表用户表和商品表,再关联大表,查询时间直接降到1分钟——这就是左深树的局限,优化器自己想不到这种“分组连接”的方式。
3.1.2 计算复杂度爆炸:表越多,优化越慢
旧优化器用的是“贪心+穷举”的思路:先选一个驱动表,然后每次加一个表,算成本,选当前最优的。但这种方式的计算复杂度是O(n!)(n是表数量),表越多越慢。比如10个表连接,可能的顺序有3628800种(360多万),优化器根本算不过来,只能“偷懒”选个差不多的顺序,很容易错过最优解。
官方做过测试:当表数量超过8个,旧优化器的优化时间会急剧增加,甚至超过查询执行时间;而超图优化器的复杂度是O(3ⁿ),n=10时仅59049种可能,比O(n!)小得多,10个表也能轻松处理。
3.1.3 超图优化器的“使命”:突破左深树限制
超图优化器的核心目标就是解决这两个问题:
- 支持“丛生树”(Bushy Tree)连接顺序,允许同时连接多个表组;
- 降低计算复杂度,让多表连接的优化更高效、更精准。
用丛生树连接上面的4个表,顺序可以是这样:
先连A和B,再连C和D,最后把两个结果连接起来。这种方式能大幅减少中间结果集的大小,尤其是当A+B和C+D的结果集都很小时,效率比左深树高10倍都不止!
3.2 超图优化器核心原理:从“图”到“超图”的升级
要理解超图优化器,得先搞懂“超图”到底是个啥。别被名字吓到,其实超图就是一种“升级版的图”,普通图的边只能连两个点,而超图的“超边”可以连多个点——这正是它能优化多表连接的关键。
3.2.1 从“普通图”到“超图”:连接关系的新表达
在数据库里,表就是“顶点”,连接条件就是“边”。普通图里,一条边只能表示两个表的连接(比如A和B通过id连接);但超图里,一条超边可以表示多个表的连接关系(比如A、B、C通过同一个订单号连接)。
举个例子:查询“用户表u、订单表o、商品表p,通过u.id=o.user_id和o.pid=p.id连接”。用普通图表示是两条边(u-o和o-p);用超图表示,就是一条超边同时连接u、o、p三个顶点,因为这三个表通过订单的关联形成了一个整体。
这种表达更贴近多表连接的实际场景,优化器能更直观地看到哪些表可以“打包连接”,而不是只能两两连接。
3.2.2 超图优化器的核心步骤:切分、估算、选优
超图优化器的工作流程可以简化成三步:构建超图→枚举切分→选最优解。咱们一步步说:
第一步:构建超图(Hypergraph Construction)
把查询中的表作为“顶点”,连接条件作为“超边”,构建出超图。比如3表连接查询:
超图结构是:
- 顶点:a、b、c
- 超边1:连接a和b(对应a.id = b.a_id)
- 超边2:连接b和c(对应b.id = c.b_id)
第二步:枚举切分(Cut Enumeration)
“切分”就是把超图的顶点分成两部分(比如S和T),超边中同时包含S和T的顶点,就叫“跨界超边”。切分的目的是找到“如何分组连接表”的可能方式。
比如上面的3表超图,可能的切分方式有:
- S={a},T={b,c}:跨界超边是连接a和b的边;
- S={a,b},T={c}:跨界超边是连接b和c的边;
- S={a,c},T={b}:跨界超边是连接a-b和b-c的两条边(因为b在T里,a和c在S里)。
每种切分方式对应一种“分组连接”的思路:先连S里的表,再连T里的表,最后通过跨界超边连接两个组。
第三步:估算成本(Cost Estimation)
对每种切分方式,计算“先连S组→再连T组→最后连跨界边”的总成本。成本计算用的还是咱们上一篇说的IO+CPU+内存成本模型,但会考虑分组后的中间结果集大小。
比如切分S={a},T={b,c}的成本是:
- 连a的成本(全表或索引扫描);
- 连b和c的成本(假设b是驱动表);
- 用a.id = b.a_id连接两组结果的成本。
第四步:选最优解(Optimal Plan Selection)
用动态规划(DP)算法,从所有可能的切分方式中选出总成本最低的,递归生成完整的连接顺序。这一步会考虑所有可能的丛生树结构,而不只是左深树,所以能找到更优的连接顺序。
3.2.3 超图 vs 旧算法:性能提升的关键在哪?
官方做过TPC-H测试(一种经典的OLAP性能测试),对比超图优化器和旧优化器的性能,结果很惊人:
查询 | 旧优化器耗时(ms) | 超图优化器耗时(ms) | 性能提升 |
Q3 | 4200 | 1100 | 3.8倍 |
Q7 | 9800 | 2300 | 4.2倍 |
Q9 | 20100 | 7800 | 2.6倍 |
Q18 | 15600 | 4500 | 3.5倍 |
为啥提升这么大?核心原因有两个:
- 连接顺序更优:丛生树能减少中间结果集大小,比如Q3查询中,超图优化器先连小表
orders
和lineitem
,再关联大表customer
,中间结果集比旧算法小60%;
- 优化时间更短:超图优化器用动态规划减少了无效计算,10表连接的优化时间从旧算法的200ms降到50ms,复杂查询的优化效率提升4倍。
3.3 超图优化器源码深扒:从代码看“智能决策”
光说原理太空泛,咱们直接上源码。MySQL 8.0.22之后,超图优化器的核心代码在
sql/join_optimizer/
目录下,主要文件有hypergraph.cc
(超图构建和切分)、cost_model.cc
(成本计算)、join_optimizer.cc
(主流程)。咱们挑几个关键部分解析。3.3.1 核心数据结构:超图的“骨架”
超图优化器里最核心的结构是
Hypergraph
和Hyperedge
,定义在sql/join_optimizer/hypergraph.h
里:简单说,
Hyperedge
描述“哪些表能通过什么条件连接,连接成本多少”;Hypergraph
则是这些超边和表的集合。比如两个表连接,vertices
就是(1 << 0) | (1 << 1)
(假设表0和表1),selectivity
是连接后结果集占总可能组合的比例(比如0.1表示缩小10倍)。3.3.2 超图构建:从SQL到超图的“翻译”
Hypergraph::Build
函数负责把SQL查询转换成超图,核心逻辑是遍历所有连接条件,生成对应的超边。简化版源码如下:这个过程就像“翻译”:把SQL里的
JOIN ... ON
条件,翻译成超图里的超边,记录连接哪些表、成本多少。比如a JOIN b ON a.id = b.a_id
,会生成一个连接表a和b的超边,选择性可能是0.05(假设a和b的连接结果集很小)。3.3.3 切分枚举:寻找最优分组方式
EnumerateCuts
函数是超图优化器的“大脑”,负责枚举所有可能的切分方式。它用了一种叫“最小顶点覆盖”的算法,避免枚举无效切分,提高效率:这段代码的作用是“穷举所有可能的表分组方式”,但通过bitmask和跨界超边检查,过滤掉无效的分组(比如S组和T组没有连接条件,无法关联)。比如3个表的超图,会枚举7种可能的分组(subset从1到7),但只保留有跨界超边的切分。
3.3.4 动态规划选最优解:从切分到执行计划
有了所有可能的切分方式后,超图优化器用动态规划(DP)选出最优解。核心逻辑在
FindOptimalJoinOrder
函数里:这段代码的逻辑是“从小表组到大表组”逐步计算:先算单个表的成本,再算两个表的最优连接方式,再算三个表的……直到包含所有表。每种表组的最优计划都存在
dp
里,避免重复计算。这就是动态规划的魔力——把复杂问题拆成小问题,逐个解决,最后合并出最优解。3.4 超图优化器实战:从“慢查询”到“飞一般的体验”
理论讲得再多,不如实战来得实在。这一节咱们用几个真实案例,看看超图优化器在生产环境中的表现,以及怎么调优才能发挥它的最大威力。
3.4.1 案例1:10表连接报表查询优化
背景:某电商平台的销售报表查询,需要关联用户表(users)、订单表(orders)、商品表(products)、分类表(categories)、库存表(inventory)、物流表(shipping)、支付表(payments)、优惠券表(coupons)、评价表(reviews)、退款表(refunds)共10个表,统计每个分类的月销售额、退款率、用户评价等指标。
旧优化器表现:
- 执行时间:5分20秒
- 执行计划:左深树连接,驱动表选了大表orders(500万行),导致后续每个连接都处理大量数据
- 中间结果集:最大时超过200万行
启用超图优化器后:
- 执行时间:45秒(提升7倍)
- 执行计划:丛生树连接,先连小表组(categories+products+inventory)和(users+coupons),再连中等表组(orders+payments+shipping),最后合并所有结果
- 中间结果集:最大时仅30万行
优化原因:
超图优化器发现“分类-商品-库存”和“用户-优惠券”这两个表组的连接结果集很小(各10万行),先处理它们能大幅减少后续连接的数据量;而旧优化器只能按
FROM
顺序逐个连接,一上来就处理大表orders,导致中间结果集膨胀。3.4.2 案例2:子查询转多表连接优化
背景:某社交APP的用户行为分析查询,用子查询关联多个行为表,统计用户的登录、发帖、评论、点赞等行为次数:
旧优化器表现:
- 执行时间:1分30秒
- 执行计划:对每个用户,依次执行4次子查询,相当于“用户数×4”次查询,用户表有50万行,总查询次数高达200万次
启用超图优化器后:
- 执行时间:8秒(提升11倍)
- 执行计划:自动把子查询转成多表连接,用哈希连接一次性关联所有表:
优化原因:
超图优化器能识别子查询的“多表关联”本质,将其转换为丛生树连接,用一次扫描完成所有统计,避免了大量重复查询。而旧优化器对复杂子查询的优化能力弱,只能按字面意思执行。
3.4.3 案例3:分区表多表连接优化
背景:某游戏公司的跨服战报查询,需要关联分区表
battles_2023
(按服分区,共20个分区)和players
表、guilds
表,统计每个 guild 的胜率:旧优化器表现:
- 执行时间:3分10秒
- 执行计划:全分区扫描battles_2023(20个分区),连接顺序是
guilds→players→battles_2023
,每个分区都要和前两表的结果连接,重复计算多
启用超图优化器+分区裁剪后:
- 执行时间:25秒(提升7.4倍)
- 执行计划:先按
battle_time
裁剪battles_2023到最近3个分区(利用分区键的范围条件触发裁剪),连接顺序是(battles_2023分区1→players)→(battles_2023分区2→players)→...→guilds
,每个分区单独处理后合并
优化原因:
超图优化器结合了分区裁剪和丛生树连接,先对每个分区做局部连接,再合并结果,避免了全分区扫描和大结果集连接。旧优化器不支持分区表的丛生树连接,只能全量扫描后再处理。超图优化器能识别分区键过滤条件,确保只扫描符合
battle_time >= '2023-10-01'
的分区,这要求分区键必须出现在WHERE子句中且使用等值或范围条件。3.5 超图优化器调优指南:让“智能大脑”更聪明
超图优化器虽然强大,但不是“开箱即用”就完美的,需要结合业务场景调优才能发挥最大威力。这一节咱们聊聊超图优化器的关键参数、调优技巧和避坑指南。
3.5.1 核心参数:控制超图优化器的“开关和旋钮”
MySQL提供了几个关键参数来控制超图优化器的行为,用好这些参数能解决大部分优化问题:
1. optimizer_switch='hypergraph_optimizer=on/off'
(开关)
- 作用:控制是否启用超图优化器,8.0.22后默认
on
。
- 适用场景:
- 多表连接(5表以上)、复杂子查询、分区表连接建议开启;
- 简单查询(2-3表连接)开不开启区别不大,甚至旧算法更快(因为超图优化器有额外计算成本)。
- 示例:
2. optimizer_memory
(内存限制)
- 作用:限制优化器使用的最大内存(字节),默认1GB(1073741824)。
- 问题:超图优化器计算复杂,表多的时候可能消耗大量内存,导致优化时间过长或OOM。
- 调优建议:
- OLAP场景(多表连接多)可以调大到2-4GB:
SET GLOBAL optimizer_memory = 2147483648;
(2GB); - 简单OLTP场景保持默认即可,避免浪费内存。
3. hypergraph_cost_model
(成本模型选择)
- 作用:控制超图优化器使用的成本模型,
on
启用新模型(考虑并行成本),off
用旧模型。
- 适用场景:
- 开启并行查询(
innodb_parallel_read_threads>1
)时建议on
,新模型会考虑并行线程的成本; - 单线程查询用
off
可能更快(计算简单)。
- 示例:
4. eq_range_index_dive_limit
(索引潜水限制)
- 作用:控制
IN
条件中值的数量超过多少时,用统计信息估算而非索引潜水(index dive)。
- 问题:超图优化器依赖准确的行数估算,索引潜水更准但慢,统计信息快但可能不准。
- 调优建议:
- 多值
IN
查询(比如id IN (1,2,...,1000)
)建议调大到1000:SET GLOBAL eq_range_index_dive_limit = 1000;
; - 避免因估算不准导致超图优化器选错连接顺序。
3.5.2 索引设计:给超图优化器“指路”
超图优化器再智能,也离不开好的索引设计。针对超图优化器的索引设计有几个原则:
1. 连接列加索引:减少连接成本
超图优化器会优先选择有连接列索引的表作为驱动表,所以所有
JOIN ... ON
条件中的列都要加索引。比如a JOIN b ON a.id = b.a_id
,要给a.id
和b.a_id
都加索引:2. 小表建覆盖索引:减少扫描成本
超图优化器喜欢用小表当驱动表,给小表建覆盖索引(包含查询所需的所有列),能大幅降低扫描成本。比如小表
categories
关联查询:3. 多列索引按“选择性递减”排序:提高过滤效率
多表连接中,过滤条件多的表可以建多列索引,按选择性(过滤效果)从高到低排序。比如订单表有
status
(高选择性)和create_time
(中选择性):3.5.3 查询改写:帮超图优化器“减负”
复杂查询的优化难度高,适当改写查询能让超图优化器更高效地找到最优计划:
1. 子查询转JOIN:超图优化器更擅长处理JOIN
虽然超图优化器能自动转部分子查询,但复杂的
IN
/EXISTS
子查询还是手动转JOIN更可靠:2. 拆分大查询:避免“超图爆炸”
超过15表的连接查询,超图优化器的计算成本会急剧增加,建议拆分成多个小查询,用临时表存储中间结果:
3. 明确过滤条件:帮助超图优化器裁剪
在连接条件中加入明确的过滤条件(比如时间范围、状态),让超图优化器能更早裁剪数据:
3.5.4 成本模型自定义:精准控制优化器决策
超图优化器的成本计算依赖
server_cost
和engine_cost
两个系统表,通过调整这些表的值可以精准控制优化器的决策倾向。这两个表存储在mysql
系统数据库中,允许DBA根据实际环境调整成本估算。常用成本调整场景:
- 提高IO成本权重:如果存储性能较差(如机械硬盘),可以提高IO操作的成本权重,让优化器更倾向于使用索引而非全表扫描:
- 降低内存操作成本:在内存充足的服务器上,可以降低内存表的成本,鼓励优化器使用内存临时表:
- 调整连接成本:如果网络延迟高,可提高远程连接的成本,让优化器优先选择本地表连接:
3.5.5 避坑指南:超图优化器的“那些坑”
超图优化器虽然强大,但实战中也有不少坑,这些都是我踩过的血泪教训:
坑1:统计信息不准导致优化器“决策失误”
症状:超图优化器选了明显低效的连接顺序,比如用大表当驱动表。
原因:表或索引的统计信息过时,超图优化器基于错误的行数估算做决策。
解决办法:
- 手动更新统计信息:
ANALYZE TABLE 表名;
- 调大采样量提高准确性:
SET GLOBAL innodb_stats_persistent_sample_pages = 100;
(默认20)
坑2:超图优化计算时间过长
症状:简单查询的优化时间比执行时间还长,比如1秒优化,0.1秒执行。
原因:表数量多(比如10+表)或
IN
条件值太多,超图优化器枚举切分的成本高。解决办法:
- 临时关闭超图优化器:
SET optimizer_switch = 'hypergraph_optimizer=off';
- 减少
IN
条件的值数量,或用JOIN
替代IN
:WHERE id IN (1,2,...1000)
→JOIN (VALUES (1),(2),...(1000)) t(id) ON ...
坑3:分区表连接时裁剪失效
症状:超图优化器在分区表连接时没做分区裁剪,全分区扫描。
原因:分区键在连接条件中,超图优化器无法提前确定分区范围;或过滤条件使用了函数操作(如
DATE(battle_time) >= '2023-10-01'
)。解决办法:
- 把分区键过滤条件写到子查询,提前裁剪:
- 避免对分区键使用函数,直接写原始条件:
battle_time >= '2023-10-01 00:00:00'
坑4:特定版本的崩溃bug
症状:启用超图优化器后,包含
EXCEPT
或复杂子查询的语句导致MySQL崩溃(报SEGV错误)。原因:MySQL 8.0.22-8.0.31存在超图优化器的bug(Bug #111474),在处理特定语法结构时会触发内存访问错误。
解决办法:
- 升级到8.0.32及以上版本(该版本已修复此bug);
- 临时规避:
SET optimizer_switch = 'hypergraph_optimizer=off';
坑5:升级后性能波动
症状:升级到8.0.22+后,部分查询性能下降。
原因:超图优化器的决策逻辑和旧优化器不同,某些场景下选了更差的计划。
解决办法:
- 用优化器提示强制旧行为:
SELECT /*+ SET_VAR(optimizer_switch='hypergraph_optimizer=off') */ ...
- 提交BUG反馈,或升级到更高版本(新版本通常修复了这类问题)
3.6 超图优化器的未来:更智能、更高效
超图优化器不是终点,而是MySQL优化器智能化的新起点。从官方 roadmap 和社区动态来看,未来的超图优化器会有这些发展方向:
3.6.1 自适应超图优化:会“学习”的优化器
未来的超图优化器可能会引入机器学习(ML)模型,从历史查询中学习最优计划的特征,比如“哪些表组合适合丛生树连接”“哪些过滤条件能大幅裁剪数据”,让优化决策更精准。
Oracle已经在实验室版本中测试了“自适应超图优化”,在TPC-H测试中,复杂查询的优化时间再降30%,计划准确性提升25%。
3.6.2 并行超图优化:多线程算计划
目前超图优化器是单线程计算的,未来可能支持多线程枚举切分和计算成本,尤其是在表数量多的时候,优化时间能再降50%以上。多线程优化将充分利用现代CPU的多核特性,把不同表组的切分计算分配到不同线程,大幅提升优化效率。
3.6.3 与存储引擎深度融合:下推更多计算
超图优化器可能会和InnoDB存储引擎更深度地协作,把部分连接计算下推到引擎层。例如,对于小表连接,可在InnoDB内部直接完成合并,减少数据传输到Server层的开销;对于分区表,引擎可提前推送分区元数据给优化器,进一步提升裁剪效率。
3.6.4 更好的OLAP支持:针对分析场景优化
超图优化器会进一步优化对窗口函数、CTE(公用表表达式)、聚合函数的支持。例如,针对星型模型查询(事实表+多个维度表),超图优化器可自动识别维度表之间的关联强度,优先构建维度表的丛生树,再与事实表连接,让OLAP查询的性能更接近专业数据仓库(如ClickHouse)。
结语:超图优化器——MySQL性能的“加速器”
这一章咱们把超图优化器从原理到实战扒了个遍:从旧优化器的左深树局限,到超图的丛生树突破;从源码里的超边和切分枚举,到生产环境中的性能提升案例;从核心参数调优,到避坑指南。相信你现在对超图优化器已经有了全面的认识。
超图优化器的出现,标志着MySQL从“OLTP专用”向“OLTP/OLAP融合”迈出了关键一步。对我们开发者和DBA来说,掌握超图优化器的原理和调优技巧,能让复杂查询的性能提升数倍甚至十数倍,这在数据分析、报表统计等场景中至关重要。
下一章,咱们将深入优化器的“成本模型”,看看优化器是如何精确计算每个执行计划的成本的——这可是优化器做决策的“钱袋子”,搞懂它,你就能更精准地引导优化器选最优计划!
- 作者:Honesty
- 链接:https://blog.hehouhui.cn/archives/mysql-hypergraph-optimizer-deep-analysis-evolution-history-principles-practice
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。