编程 MySQL 9.7 重磅:等了六年的 Hypergraph 优化器终于全民可用,复杂 JOIN 提速 4 倍背后发生了什么

2026-07-28 03:15:40 +0800 CST views 4

MySQL 9.7 重磅:等了六年的 Hypergraph 优化器终于全民可用,复杂 JOIN 提速 4 倍背后发生了什么

2026 年 7 月 22 日,Oracle 官方博客宣布:随着 MySQL 9.7 Community Edition 发布,Hypergraph Optimizer(超图优化器)正式对所有人开放

如果你是老 MySQL 用户,看到这条消息应该会心头一震。这个特性最早出现在 2020 年的 MySQL 8.0.22 里——但当时只在 debug 构建中可用,普通用户根本摸不到。六年过去,它终于从实验室走进了社区版的生产环境。

这不是一次普通的版本更新。这是 MySQL 二十多年来对查询优化器核心——JOIN 顺序枚举算法——的第一次彻底重写。官方给出的实测案例里,一条 8 表 JOIN 的报表查询,不改一行 SQL,执行时间从 451ms 降到 122ms,接近 4 倍提速

但官方同时泼了一盆冷水:不要无脑全局开启

这篇文章我会从优化器的第一性原理讲起,拆解传统优化器和超图优化器的本质区别,复现官方的对比实验,最后给出一套生产环境的灰度落地策略。读完你应该能回答三个问题:它为什么快?什么时候会更慢?我的业务该不该开?

一、背景:MySQL 优化器的"历史包袱"

先说一个很多人没意识到的事实:你今天在 MySQL 8.x / 9.x 里用的查询优化器,其核心骨架可以追溯到上世纪末

MySQL 的传统优化器采用的是经典的 System R 风格代价模型 + 左深树(left-deep tree)枚举 + 贪心启发式剪枝。这套架构在 OLTP 场景下表现优秀——毕竟大多数业务查询就是两三张表按主键 JOIN,优化器几乎没有决策难度。

但问题出在复杂查询上。

JOIN 顺序是个组合爆炸问题

假设你要 JOIN N 张表,可能的 JOIN 顺序有多少种?

  • 2 张表:2 种
  • 4 张表:120 种(考虑不同树形)
  • 8 张表:约 1700 万种
  • 10 张表以上:天文数字

这是一个经典的 NP-hard 问题。优化器不可能穷举所有方案——如果花 10 秒找"完美计划",而查询本身只跑 1 秒,那就是本末倒置。

传统 MySQL 优化器的解法是"砍搜索空间":

  1. 只考虑左深树:JOIN 结果永远作为下一次 JOIN 的左输入,不考虑 bushy tree(浓密树)形态
  2. 贪心启发式optimizer_search_depth 控制搜索深度,超过阈值就用贪心算法糊弄过去
  3. 各种硬编码规则:比如小表优先、有索引优先等经验法则

这套方案 90% 的场景够用。但代价是:对于多表复杂 JOIN,很多本质上更优的执行计划,传统优化器压根"看不见"——它们不在被搜索的空间里。

为什么现在必须换?

两个时代背景:

第一,HTAP 需求爆炸。 越来越多团队直接在 MySQL 上跑报表和分析查询(是的,不是每家公司都有数仓)。8 表以上的 JOIN、星型/雪花模式的聚合查询越来越常见,这恰恰是传统优化器的软肋。

第二,竞品压力。 PostgreSQL 的优化器一直支持 bushy tree 和基于动态规划的 JOIN 枚举(表少时用标准 DP,表多时切换到遗传算法 GEQO)。在复杂查询场景,PG "碾压 MySQL 优化器"几乎成了 DBA 圈的共识。MySQL 不追,差距只会越拉越大。

二、核心概念:查询优化器到底在优化什么

在拆超图优化器之前,先把地基打牢。

一条 SQL 进入数据库后的旅程:

SQL 文本
  → 解析器(Parser)      → 语法树 AST
  → 预处理(Resolver)    → 语义检查、名称解析
  → 优化器(Optimizer)   → 执行计划   ← 今天的主角
  → 执行器(Executor)    → 结果集

优化器的工作可以分为两大块:

逻辑优化(Rewrite):不改变语义的等价变换。比如外连接消除、子查询上拉、条件下推、分区裁剪。

物理优化(Plan Selection):这是重头戏,要决定三件事:

  1. 访问路径:每张表走全表扫描、索引扫描还是索引范围扫描?
  2. JOIN 顺序:先 JOIN 谁后 JOIN 谁?
  3. JOIN 算法:嵌套循环(Nested Loop)、哈希连接(Hash Join)还是归并连接?

决策依据是代价模型:基于表统计信息(行数、索引基数、直方图)估算每个候选计划的 I/O 和 CPU 开销,选代价最低的那个。

关键洞察:同样的结果集,不同执行计划的耗时可以差几个数量级。一个把千万行表放在嵌套循环内层的计划,和一个先用高选择性条件把数据过滤到几百行再 JOIN 的计划,可能是分钟级和毫秒级的差距。

三、架构分析:超图优化器凭什么更强

3.1 理论根基:DPhyp 算法

MySQL 的 Hypergraph Optimizer 不是闭门造车,它的理论基础是数据库学术圈的经典论文——Moerkotte 和 Neumann 2008 年发表的 《Dynamic Programming Strikes Back》,即 DPhyp(Dynamic Programming over Hypergraphs)算法。这也是 HyPer、Umbra 等新一代数据库引擎采用的方案。

核心思想分三步:

第一步:把查询建模为超图(Hypergraph)。

普通图的边连接两个顶点。超图的边(hyperedge)可以连接两个顶点集合

为什么需要超图?看这个查询:

SELECT ...
FROM t1
JOIN t2 ON t1.a = t2.a
JOIN t3 ON t1.b + t2.b = t3.b;  -- 这个条件同时涉及 t1、t2、t3

第二个 JOIN 条件 t1.b + t2.b = t3.b 无法用"两表之间的边"表达——它天然是一条 {t1,t2} → {t3} 的超边。传统优化器面对这类条件往往只能退化处理,而超图模型可以精确刻画它。

第二步:只枚举"连通子图对"(csg-cmp-pairs)。

DPhyp 的精髓在于:不暴力枚举所有 JOIN 顺序,而是沿着超图的连通性做动态规划——只枚举**连通子图(connected subgraph)与其连通补图(connected complement)**构成的合法配对。每一个 csg-cmp-pair 对应一次有意义的 JOIN。

这带来的收益是数量级的:不连通的表组合(笛卡尔积)天然被排除,重复子问题被 DP 缓存复用。搜索空间从 O(n!) 级别压缩到与图结构相关的多项式~指数之间(链式 JOIN 图是多项式级,团状图才会退化)。

第三步:自底向上构建最优计划,天然支持 bushy tree。

这是与传统 MySQL 优化器最实质的区别。DP 过程中,左右子计划都可以是中间 JOIN 结果——也就是说,超图优化器可以生成这样的计划:

          ⋈ (最终 JOIN)
        /    \
      ⋈       ⋈        ← 两个分支各自先 JOIN
     / \     / \
   独立分支  独立分支

两个分支各自过滤、各自收敛,最后合并。对于"当前数据 JOIN 历史归档数据"这类查询(后面实战会看到),bushy 计划经常是碾压性的优势——而左深树优化器在结构上就不可能生成这种计划

3.2 工程层面的三个变化

除了算法内核,超图优化器还带来了工程层面的改进:

1. 统一的代价框架下更激进地使用 Hash Join。 MySQL 8.0.18 引入了 Hash Join,但传统优化器对它的运用比较保守。超图优化器把 Hash Join 作为一等公民纳入枚举,构建侧/探测侧的选择也纳入代价评估。

2. 更干净的计划表示。 超图优化器输出的是一棵显式的操作符树(AccessPath 树),EXPLAIN FORMAT=TREEEXPLAIN ANALYZE 的输出更接近 Volcano 模型的语义,可读性显著好于传统的表访问顺序列表。

3. 抛弃了大量历史启发式补丁。 传统优化器里积累了二十年的特判逻辑(很多是为了修某个年代某个 bug),超图优化器选择"重写而非修补",代价估算路径更一致。

3.3 天下没有免费的午餐

搜索更大的空间 = 花更多的优化时间。这是铁律。

  • 对一条跑 800ms 的 8 表分析查询,多花 5ms 优化换来 4 倍执行提速,血赚
  • 对一条跑 0.5ms 的主键点查,多花哪怕 1ms 优化,都是纯亏

这就是为什么 Oracle 官方在发布博客里明确说:"Should you just turn it on and forget about it? The short answer is no."(能不能开了就不管?答案是不能。)

四、代码实战:复现官方 4 倍提速案例

4.1 三种开启方式

MySQL 9.7 提供了三个粒度的开关,这个设计对灰度非常友好:

-- 方式一:单条语句级(inline hint,推荐用于评估)
SELECT /*+ SET_VAR(optimizer_switch='hypergraph_optimizer=on') */
       COUNT(*)
FROM ...;

-- 方式二:会话级(测试整批工作负载)
SET SESSION optimizer_switch = 'hypergraph_optimizer=on';

-- 方式三:全局级(评估完成后全量开启)
SET GLOBAL optimizer_switch = 'hypergraph_optimizer=on';

单语句 hint 是对比测试的最佳姿势:SQL、schema、数据完全一致,唯一变量就是优化器本身。

4.2 实验场景

官方博客的实验用了一个典型的电商报表 schema:当前销售表 sales_order / sales_line,维表 customer / product / seller,外加 2025 年的归档表 orders_2025 / order_items_2025 / 归档商品表。

查询需求:统计 2026 年黑五期间(11-20 至 11-30)的已支付订单收入,且只看在 2025 年也买过同品类商品的老客户

SELECT COUNT(*) AS matched_lines,
       ROUND(SUM(sl.net_amount), 2) AS current_revenue
FROM sales_order so
JOIN sales_line  sl  ON sl.order_id    = so.order_id
JOIN product     p   ON p.product_id   = sl.product_id
JOIN customer    c   ON c.customer_id  = so.customer_id
JOIN seller      s   ON s.seller_id    = so.seller_id
JOIN orders_2025 o25 ON o25.customer_id = c.customer_id
JOIN order_items_2025 i25 ON i25.order_id = o25.order_id
JOIN product     p25 ON p25.product_id = i25.product_id
                    AND p25.category_id = p.category_id
WHERE so.ordered_at BETWEEN '2026-11-20' AND '2026-11-30'
  AND so.status = 'PAID'
  AND o25.order_date BETWEEN '2025-01-01' AND '2025-12-31'
  AND o25.status = 'PAID'
  AND c.region IN ('US','DE','NO')
  AND s.active = TRUE
  AND p.category_id IN (7,12,19,34);

8 张表、多条选择性谓词、两个独立的数据分支(当前 vs 归档)——这正是 JOIN 顺序空间巨大、传统优化器容易"看不到"更优解的典型场景。

4.3 传统优化器的计划

EXPLAIN ANALYZE FORMAT=JSON 拿真实执行统计(注意是 ANALYZE,看实际执行数据而不是估算):

EXPLAIN ANALYZE FORMAT=JSON
SELECT COUNT(*) AS matched_lines, ...

传统优化器给出的是一条教科书式的线性嵌套循环链

全表扫描 o25(过滤 PAID + 2025 日期范围)
  → 主键点查 customer
    → ix_customer 索引查 sales_order
      → 主键点查 seller
        → ix_order 索引查 sales_line
          → 主键点查 product
            → ix_order_id 索引查 order_items_2025
              → 主键点查 p25
                → 最终过滤 p25.category_id = p.category_id

问题一眼可见:驱动表选了归档大表 o25 全表扫描,然后整条链一层层嵌套下去,每一层的行数都受上一层驱动。品类匹配条件 p25.category_id = p.category_id 被拖到最后一步才过滤——前面所有 JOIN 白做的行,到这里才被扔掉。

实测耗时:约 451ms

4.4 超图优化器的计划

同一条 SQL,加上 hint 再跑:

EXPLAIN ANALYZE FORMAT=JSON
SELECT /*+ SET_VAR(optimizer_switch='hypergraph_optimizer=on') */
       COUNT(*) AS matched_lines, ...

计划形态发生了质变:

①  o25 主键索引扫描(2025 归档分支)
②  so 走 ix_date 索引范围扫描(黑五日期分支)
③  Hash Join:so.customer_id = o25.customer_id   ← 关键!
④  → 点查 seller、customer
⑤  → ix_order 索引查 sales_line → 点查 product
⑥  → ix_order_id 查 order_items_2025
⑦  → p25 走 ix_category 覆盖索引点查

三个决定性差异:

1. 早期 Hash Join 合并两个分支。 超图优化器识别出"当前订单"和"归档订单"是两个可以独立收敛的数据分支:so 先用日期索引范围扫描把黑五订单捞出来构建哈希表,o25 扫描后直接探测。两个分支在数据量都还很小的时候就完成了合并,后续所有 JOIN 的输入行数被大幅削减。

2. 驱动侧选择完全不同。 传统计划被迫从 o25 全表扫描开始单线推进;超图计划则让两个分支各走各的最优访问路径(一个主键索引扫描,一个日期范围索引)。

3. p25 用上了覆盖索引。 最后一步品类匹配走 ix_category 覆盖索引(covering lookup),不回表。

实测耗时:约 122ms,提速接近 4 倍。SQL 一个字符都没改。

4.5 EXPLAIN 输出怎么读

几个实用技巧:

-- 树形输出,肉眼最友好
EXPLAIN FORMAT=TREE SELECT ...;

-- 带真实执行统计(actual time / rows)
EXPLAIN ANALYZE SELECT ...;

EXPLAIN ANALYZE 重点盯三个数:

  • actual time=X..Y:首行耗时..全部行耗时,定位时间黑洞
  • rows=N(actual)vs 估算 rows:两者差距大 = 统计信息失真,优化器在"盲开车"
  • loops=N:嵌套循环内层执行次数,rows × loops 才是真实工作量

五、性能优化:生产落地的完整策略

5.1 什么查询会受益,什么会受伤

根据官方说明和算法特性,可以画出一张预期收益表:

大概率受益:

  • 5 表以上的多路 JOIN,尤其带多个选择性谓词
  • 星型/雪花 schema 的报表聚合
  • "当前数据 × 历史归档"的双分支对比查询(bushy 计划的主场)
  • 带复杂 JOIN 条件(跨多表表达式)的查询

基本无感:

  • 2~3 表的简单 JOIN——搜索空间本来就小,两个优化器殊途同归
  • 主键/唯一键点查

可能劣化:

  • 超高频短查询(微秒~毫秒级 OLTP):优化耗时占比被放大
  • 统计信息严重失真的表:更大的搜索空间 + 错误的代价输入 = 更自信地选错
  • 依赖了传统优化器某些特定行为的老查询(比如恰好被某个启发式救了的 SQL)

5.2 四步灰度法

千万不要在生产直接 SET GLOBAL 一把梭。推荐路径:

第一步:离线摸底。 从慢查询日志里捞出 TOP 50 慢 SQL,在预发环境用 inline hint 逐条对比:

-- 传统 vs 超图,各跑 5 遍取中位数(排除 buffer pool 冷热干扰)
EXPLAIN ANALYZE SELECT ... ;
EXPLAIN ANALYZE SELECT /*+ SET_VAR(optimizer_switch='hypergraph_optimizer=on') */ ... ;

第二步:先喂饱统计信息。 超图优化器比传统优化器更依赖准确的代价输入,上线前先做功课:

ANALYZE TABLE sales_order, sales_line, orders_2025;

-- 对倾斜列建直方图,这对选择性估算至关重要
ANALYZE TABLE customer UPDATE HISTOGRAM ON region WITH 32 BUCKETS;
ANALYZE TABLE product  UPDATE HISTOGRAM ON category_id WITH 64 BUCKETS;

数据倾斜的列(状态字段、地区、品类)没有直方图,任何优化器都会估错行数。

第三步:按业务分层开启。 报表库/只读从库先行——这里复杂查询密度最高、收益最大、风险最可控:

-- 只在报表服务的连接池初始化 SQL 里加
SET SESSION optimizer_switch = 'hypergraph_optimizer=on';

OLTP 主链路保持传统优化器,观察一到两个迭代周期。

第四步:全局开启 + 逃生通道。 确认收益后全局开启,同时给个别劣化查询留 hint 回退:

SET GLOBAL optimizer_switch = 'hypergraph_optimizer=on';

-- 个别劣化的查询单独回退
SELECT /*+ SET_VAR(optimizer_switch='hypergraph_optimizer=off') */ ...;

5.3 监控什么

上线后重点看三个信号:

  1. P99 优化耗时performance_schema 里语句阶段统计,关注 optimizing 阶段占比是否异常上升
  2. 计划抖动:同一 SQL 指纹的执行计划是否频繁变化(统计信息波动会被更大的搜索空间放大)
  3. 临时表与内存:Hash Join 用得更多意味着 join_buffer 和内存水位模式会变化,注意 Created_tmp_disk_tables 的趋势

六、横向对比:MySQL 这一步棋在行业里什么位置

把视野拉宽一点:

  • PostgreSQL:小于 geqo_threshold(默认 12 表)用标准 DP 枚举(支持 bushy),超过切遗传算法。MySQL 9.7 之后,两者在"复杂 JOIN 计划质量"上的代差显著缩小
  • HyPer / Umbra(学术前沿):DPhyp 正是这一系的看家算法,MySQL 相当于把学术界验证了十几年的成果工程化落地
  • TiDB / OceanBase 等分布式数据库:早已采用 Cascades/Volcano 风格的现代优化器框架,MySQL 补上这一课后,单机场景的计划质量重新有了竞争力

值得玩味的是发布策略:这次 Hypergraph Optimizer 直接进了 Community Edition,而不是像向量支持、JavaScript 存储过程那样锁在 HeatWave 付费版里。结合 2026 年以来 Oracle 在 MySQL 上的一系列动作(公开路线图、worklog 透明化、Contributor Summit),能感觉到社区版的战略权重在回升——毕竟 PostgreSQL 的增长压力是实打实的。

七、总结与展望

核心要点回顾:

  1. MySQL 9.7 Community Edition 让 Hypergraph Optimizer 全民可用,这是 MySQL 优化器二十多年来最大的一次架构升级
  2. 理论根基是 DPhyp 算法:查询建模为超图 + 连通子图动态规划 + 支持 bushy tree,能发现传统左深树优化器结构上不可能生成的执行计划
  3. 官方实测 8 表 JOIN 报表查询 451ms → 122ms,接近 4 倍,SQL 零改动
  4. 它不是免费加速开关:复杂分析查询大概率受益,高频短查询可能因优化开销劣化
  5. 落地姿势:inline hint 逐条评估 → 补齐统计信息和直方图 → 报表/从库先行 → 全局开启留逃生通道
  6. 判断标准永远是 EXPLAIN ANALYZE 的真实执行数据,不是估算,更不是信仰

展望: 优化器重写是"地基工程",真正的红利在后面——更大的计划搜索空间为并行执行、自适应重优化(执行中发现估算偏差自动换计划)铺平了道路。传统优化器的历史包袱曾让这些特性寸步难行,现在地基换新了。对于还在 8.0 LTS 上观望的团队,如果你的痛点恰好是复杂报表查询,MySQL 9.7 值得认真排期评估一次。

数据库的世界里没有银弹,但这一次,MySQL 用户手里确实多了一把好牌。用不用、怎么用,让 EXPLAIN ANALYZE 告诉你答案。

推荐文章

16.6k+ 开源精准 IP 地址库
2024-11-17 23:14:40 +0800 CST
服务器购买推荐
2024-11-18 23:48:02 +0800 CST
JavaScript 实现访问本地文件夹
2024-11-18 23:12:47 +0800 CST
go错误处理
2024-11-18 18:17:38 +0800 CST
在 Rust 中使用 OpenCV 进行绘图
2024-11-19 06:58:07 +0800 CST
程序员茄子在线接单