openGauss OpFusion 失效分析
发表于 2026/04/22
0
为什么这条 SQL 没有走 Bypass?从源码看 openGauss OpFusion 的准入条件、失效原因与调优入口
在 openGauss 的性能排查里,OpFusion 是一个很典型、也很容易被误解的模块。官方文档把它归到SQL by pass,这个说法没有问题,但真正到了线上定位,大家最常见的问题通常不是“Bypass 是什么”,而是另外一句更直接的话:
同样是单表 SQL,为什么有的 EXPLAIN 前面已经显示 [Bypass],有的却只能看到[No Bypass]?
只靠产品说明,很难把这个问题回答透。文档会告诉你,OpFusion 面向高频、简单、稳定的 OLTP 语句,目的是缩短执行路径,降低通用执行框架带来的额外成本。问题在于,这个定义只适合做特性介绍,不足以支持排障。真正开始定位时,核心问题从来都不是“它是不是一个优化特性”,而是“内核为什么认为这条 SQL 不满足快路径条件”。
这篇文章只做一件事:沿着当前openGauss-server 仓库里的实现,拆开 OpFusion 的判断逻辑。重点不是再讲一遍SQL by pass 的概念,而是把几个更实用的问题说清楚:
• simple query 是在什么位置开始尝试进入 OpFusion
• 内核支持哪些FusionType
• 一条 SQL 看起来已经很简单,为什么仍然会回退到通用执行器
• [No Bypass] reason 这句提示是怎样从源码里生成出来的
• 真正做定位时,应该按什么顺序缩小范围
先交代两个边界。
第一,本文以当前仓库实现为准,主线聚焦simple query,也就是exec_simple_query() 之后的判断流程。prepared statement / PBE 也会和 OpFusion 发生关系,但那是一条单独的链路,本文只在需要时点到为止。
第二,文中的源码索引统一使用“文件路径 + 函数名 / 参数名”的方式,不写具体行号。原因很简单,行号会漂移,函数入口和文件路径更适合写成可复核的长期引用。
先把诊断开关打开,再谈优化
排查 OpFusion 的第一步,不是猜,也不是立刻改 SQL,而是先把诊断信息放出来。
openGauss 在这个点上其实做得很务实。它没有把“不命中 bypass”当成一个沉默的回退路径,而是保留了一套专门的失败原因枚举,并允许通过日志把这些原因打印出来。很多时候大家觉得OpFusion 很“玄”,并不是因为逻辑本身真的不可追,而是因为默认看不到足够的信息。
常见的会话级设置如下:
set enable_opfusion = on;
set opfusion_debug_mode = 'log';
set log_min_messages = debug4;
set logging_module = 'on(OPFUSION)';
这几个参数里,最关键的是下面三个。
• enable_opfusion 是总开关,定义在 src/common/backend/utils/misc/guc/guc_sql.cpp。它控制简单增删改查是否允许尝试快路径。
• opfusion_debug_mode 同样定义在 guc_sql.cpp。它负责决定是否把“不命中原因”转换成可读日志。
• logging_module='on(OPFUSION)' 决定 OpFusion 模块的调试日志能不能真正落出来。只打开opfusion_debug_mode 而不打开模块日志,很多信息仍然看不到。
还有一个很容易被忽略的参数:enable_partition_opfusion。这个参数也在guc_sql.cpp 中定义,它不是 bypass 的总开关,而是分区表场景的额外门槛。换句话说,分区表不是在打开enable_opfusion 之后就自动进入候选集,它需要单独通过一次分区相关的准入判断。
如果只记一条经验,我建议记下面这句:
在判断一条 SQL 为什么没走 OpFusion 之前,先把 opfusion_debug_mode 和 OPFUSION 模块日志打开。
否则你看到的只有结果,没有原因。
官方材料可参考:
• openGauss 文档:SQL by Pass
<https://docs.opengauss.org/zh/docs/latest/docs/AboutopenGauss/SQL-by-pass.html>
• openGauss 文档:记录日志的内容中的 opfusion_debug_mode
<https://docs.opengauss.org/zh/docs/5.0.0/docs/DatabaseReference/%E8%AE%B0%E5%BD%95%E6%97%A5%E5%BF%97%E7%9A%84%E5%86%85%E5%AE%B9.html>
• openGauss 文档:其他优化器选项中的 enable_opfusion / enable_partition_opfusion
<https://docs.opengauss.org/zh/docs/2.1.0/docs/Developerguide/%E5%85%B6%E4%BB%96%E4%BC%98%E5%8C%96%E5%99%A8%E9%80%89%E9%A1%B9.html>
`OpFusion` 不只是“少走几步”,它本质上是一组专用执行器
很多文章在介绍 bypass 时,喜欢用一句比较抽象的话:绕过传统执行器,走更短的路径。这个方向没有错,但如果只停留在这里,读者很容易把OpFusion 理解成几个简单的捷径分支,甚至理解成“只不过少调了几个函数”。
从当前实现看,这个理解明显不够准确。
在 src/include/opfusion/opfusion.h 里,OpFusion 把状态拆成了 m_global 和m_local 两部分。前者保存可复用的执行骨架,比如PlannedStmt、目标关系、参数位置信息、元组描述符、函数指针等;后者保存每次执行时真正会变化的现场,比如参数值、接收器、结果槽、snapshot、扫描对象、portal 状态等。
这个拆分很说明问题。它反映的并不是“在通用执行器前面加一层短路判断”,而是“把一类足够稳定的计划模板单独提炼出来,为它准备专门的执行模型”。
也正因为如此,src/gausskernel/runtime/opfusion/ 目录下不是只有一个入口文件,而是有一组对应不同模板的具体实现:
• opfusion_select.cpp
• opfusion_insert.cpp
• opfusion_update.cpp
• opfusion_delete.cpp
• opfusion_indexscan.cpp
• opfusion_indexonlyscan.cpp
• opfusion_sort.cpp
• opfusion_agg.cpp
这些文件不是形式上的拆分,它们对应的就是不同FusionType 的专用执行逻辑。
例如在 src/gausskernel/runtime/opfusion/opfusion_select.cpp 中,SelectFusion 会在初始化阶段抽取LIMIT/OFFSET,再根据计划节点选择具体的ScanFusion 实现。运行时,它直接从扫描对象里取 TupleSlot,然后把结果交给 receiver。这个过程和通用执行器里那种完整初始化 PlanState 树、再递归推进各层节点的方式并不是一回事。
再看 src/gausskernel/runtime/opfusion/opfusion_insert.cpp 里的 InsertFusion。它会提前整理目标表的 tuple descriptor、参数位置、常量位置,以及 targetlist 中函数表达式的位置。到了执行阶段,它更像是在一个已经排好版的模板里填值,而不是现场再解释一轮通用 targetlist。
所以如果要给 OpFusion 下一个更贴切的定义,我更愿意这样表述:
OpFusion 不是一句“绕过执行器”的口号,而是一组面向稳定 SQL 模板的专用执行器。
它的前提不是“SQL 看起来简单”,而是“SQL 对应的计划形态足够稳定,稳定到值得内核单独维护一条快路径”。
把这个前提想清楚之后,再去看它为什么拒绝某些 SQL,很多现象就不再奇怪了。
simple query 是怎样走到 `OpFusion` 门口的
官方介绍经常会说,SQL by pass 会在 parse 阶段判断语句是否属于简单模式。这个说法用于概念介绍没有问题,但如果沿着当前 simple query 代码往下追,你会发现真正决定 fusion 类型的,并不是原始 SQL 字符串,而是后面生成出来的计划树。
simple query 的主入口在src/gausskernel/process/tcop/postgres.cpp 的 exec_simple_query()。
它的主流程仍然遵循后端经典路径:
pg_parse_query
-> pg_analyze_and_rewrite
-> pg_plan_queries
-> 尝试 OpFusion
-> 如果不满足条件,再回到 Portal / Executor 主路径
这里最重要的不是记流程图,而是看清楚 bypass 判断发生在什么时机。
在 exec_simple_query() 中,后端会先得到 parsetree_list,再完成 analyze / rewrite,最终拿到 plantree_list。只有到了这一步,它才会检查runOpfusionCheck 是否成立,然后进一步调用:
OpFusion::getFusionType()
-> OpFusion::FusionFactory()
-> OpFusion::process(FUSION_EXECUTE, ...)
这个顺序对应一个很关键的事实:
从当前 simple query 源码实现看,OpFusion 的核心判断依据不是 SQL 文本,而是 PlannedStmt。
它真正识别的是计划模板,而不只是语法外观。
这也是为什么很多 SQL 明明字面上已经很短、很单纯,最后还是进不了 bypass。问题往往不在“写得长不长”,而在“生成出来的计划是不是还属于OpFusion 能接受的那几种模板”。
顺带补一句,prepared statement / PBE 场景也会碰到 OpFusion。OpFusion::IsSqlBypass() 定义在 src/include/opfusion/opfusion.h 中,说明 cached plan 路径也可以判断是否属于 bypass。不过那一条线涉及 plan cache、portal 复用和参数绑定,本文不把主线拉过去。
能不能走 bypass,核心不是布尔值,而是 `FusionType`
如果只把 OpFusion 理解成“命中”或者“没命中”两个结果,排查时很容易陷入模糊判断。因为在源码里真正起决定作用的,不是一个布尔值,而是FusionType。
FusionType 定义在src/include/opfusion/opfusion_util.h。单看这个枚举就能知道,OpFusion 不是一条快路径,而是一组快路径。
最常见的一组,是单表 OLTP 语句的基础模板:
• SELECT_FUSION
• SELECT_FOR_UPDATE_FUSION
• INSERT_FUSION
• UPDATE_FUSION
• DELETE_FUSION
再往下,是几类更窄的变体:
• INSERT_SUB_FUSION
• DELETE_SUB_FUSION
除此之外,还有一组更像扩展能力或者试验能力的类型:
• AGG_INDEX_FUSION
• SORT_INDEX_FUSION
• SELECT_FOR_ANN_FUSION
• MOT_JIT_SELECT_FUSION
• MOT_JIT_MODIFY_FUSION
这里最容易出现误判的一点是:看到枚举里类型不少,就下意识觉得系统默认能覆盖的范围也同样宽。实际上并不是这样。
例如 AGG_INDEX_FUSION 和 SORT_INDEX_FUSION 虽然已经出现在代码里,但在src/gausskernel/runtime/opfusion/opfusion_util.cpp 的 getSelectFusionType() 中,这两条路径依赖enable_beta_opfusion。而enable_beta_opfusion 本身又只在单机环境下定义。也就是说,代码里“有这个分支”和线上“默认会命中这个分支”是两件不同的事。
所以理解 FusionType 时,最好拆成两个问题来看:
1. 代码里是否已经存在对应的专用实现?
2. 当前环境、当前节点形态和当前 GUC 设置,是否真的允许走到这个实现?
这两个问题不分开,定位结论就很容易过度乐观。
失效原因不要硬背枚举,要按定位顺序分层理解
OpFusion 真正有调试价值的地方,不在于命中时那句[Bypass],而在于失败时已经把拒绝原因编码成了一组NOBYPASS_*。
这些枚举及其对应的文案主要集中在src/gausskernel/runtime/opfusion/opfusion_util.cpp。如果一个个从头背到尾,信息会很散。更好的做法,是按排查顺序把它们分成几层。
第一层:计划形态本身不够简单
最上层的判断入口是 src/gausskernel/runtime/opfusion/opfusion.cpp 中的 OpFusion::getFusionType()。
在进入更细的语句分类之前,它会先过滤掉几类明显不适合专用执行器的情况,例如:
• 语句列表不止一条
• 当前节点不是PlannedStmt
• 实际上是 utility 语句
• 计划中带有subplans 或initPlan
• 命中了版本表扫描
这类场景的共同点,是还没到讨论扫描方式的阶段,就已经不满足“稳定模板”这个大前提。
从工程实现上看,这个选择是合理的。OpFusion 的收益来自模板稳定。如果计划树已经包含子计划、初始化计划、多语句混杂或者特殊系统路径,那么为了继续保留快路径,就必须额外堆一批边角逻辑。openGauss 在这里采取的策略很清楚:模板一旦开始失稳,就回退到通用执行器。
对 DBA 或内核开发者来说,这一层最重要的不是记住某个具体枚举,而是建立判断顺序:
先看它是不是简单计划,再看它是不是简单扫描。
如果第一层都过不去,后面的索引、参数、表达式就已经不是重点。
第二层:扫描路径不符合专用执行器的预设
这是最常见的一层,也是最容易在直觉上产生误判的一层。
很多人会自然地把“单表查询”理解成“应该很适合 bypass”。但从getSelectFusionType()、getUpdateFusionType() 和 getDeleteFusionType() 的实现看,单表只是起点,不是结论。真正决定能不能继续往下走的,是扫描路径本身。
在当前实现里,普通 SELECT 的标准模板仍然是IndexScan 或 IndexOnlyScan。如果计划没有落在这两类节点上,或者索引类型、排序方式、条件形态不满足限制,函数会很快返回类似下面这些原因:
• NOBYPASS_NO_INDEXSCAN
• NOBYPASS_ONLY_SUPPORT_BTREE_INDEX
• NOBYPASS_INDEXSCAN_WITH_ORDERBY
• NOBYPASS_INDEXONLYSCAN_WITH_ORDERBY
• NOBYPASS_INDEXSCAN_WITH_QUAL
• NOBYPASS_INDEXSCAN_CONDITION_INVALID
这些限制背后反映的,其实是OpFusion 的成本边界。它不是在所有计划节点上统一做一轮“减肥”,而是在少数已经被充分约束的扫描模板上写了专用执行逻辑。既然如此,它当然更偏好那些可以被完全掌控的数据访问路径,比如条件明确、投影明确、输出形式也明确的索引扫描。
这一点在官方参数说明里也能看到高层表述:简单查询优化主要支持indexscan 和 indexonlyscan,而且过滤条件需要落在索引上。只不过文档给的是结论,源码呈现的是更细的边界。
第三层:SQL 看起来简单,但表达式已经超出模板
如果前两层都过了,接下来要看的就是表达式、参数和 targetlist。
这是 bypass 失效里特别容易被低估的一层。很多 SQL 从文本上看并不复杂,但对 OpFusion 来说,关键问题不是“这段 SQL 好不好读”,而是“这些表达式能不能被提前拆成稳定模板”。
在 opfusion_util.cpp 中,这类限制写得很直接:
• LIMIT 不是常量,会返回NOBYPASS_LIMIT_NOT_CONST
• LIMIT / OFFSET 为负值,会返回对应的负值原因
• 参数类型不满足,会返回NOBYPASS_PARAM_TYPE_INVALID
• targetlist 涉及系统列、非表列或不支持的表达式形态,会落到 NOBYPASS_TARGET_WITH_SYS_COL、NOBYPASS_TARGET_WITH_NO_TABLE_COL、NOBYPASS_EXP_NOT_SUPPORT
如果只看表层规则,这些条件看起来像是“限制很多”。但结合SelectFusion 和 InsertFusion 的执行方式去看,就会发现它们其实很自然。
InsertFusion 会在初始化阶段把参数、常量和简单函数节点提前拆开;SelectFusion 也不是临时构造一套完整的通用表达式环境,而是先绑定好对应的 ScanFusion,再按快路径把TupleSlot 推给 receiver。
既然它的设计目标就是“先把模板展开,再快速执行”,那么任何让模板失去稳定性的东西,最后都会变成拒绝条件。也正因为如此,很多 SQL 没命中的根因,并不在索引,而在表达式形态。
第四层:真正挡路的,往往不是 SQL,而是 relation 属性
到了这一层,定位视角就应该从 SQL 文本切换到表元数据。
在 getSelectFusionType()、getInsertFusionType()、getUpdateFusionType() 和 getDeleteFusionType() 中,除了扫描和表达式检查,后面还会继续判断 relation 本身是否适合快路径。常见限制包括:
• 有触发器、规则、returning、子表等复杂能力
• 列存、TsStore、某些 Ustore 场景
• 正在重分布,或者关系类型本身不支持
• 分区表但没有打开enable_partition_opfusion
• 分区键表达式、分区数、分区类型或者 GPC 共享计划限制不满足
这类限制看起来零碎,但在生产环境里非常常见。因为很多 SQL 从文本层面已经“瘦身到头”了,再怎么改写也不一定能命中 bypass。真正把它挡在门外的,可能是目标表已经挂了触发器,已经变成分区表,或者本身就是不适合OpFusion 处理的 relation。
从调试思路上说,这一层常常意味着一个明确转折:
这时不该继续折腾 SQL 写法,而该回头审视表设计、分区策略和存储形态。
因为快路径拒绝的原因已经不在语句本身,而在 relation 的元数据。
一个特别容易混淆的点:单机 `DELETE` 的 `SeqScan` 特例
如果只看普通 SELECT 的逻辑,很容易形成一个印象:OpFusion 的核心就是索引扫描快路径。这个判断大体没错,但并不完整。
在 getDeleteFusionType() 中,DELETE 还有一个值得单独拿出来讲的分支,也就是单机场景下的SeqScan 特例。
这条逻辑依赖 enable_iud_fusion,该参数定义在src/common/backend/utils/misc/guc.cpp。只有当这个开关打开时,单机DELETE 才有机会继续判断SeqScan 是否可以走专用路径;如果不开,这类计划会直接被视为不满足 bypass 条件。
换句话说,单机 DELETE 的SeqScan 快路径不是普通 bypass 判断顺带给出的结果,而是一条额外放开的窄门。
这件事真正重要的,不是记住参数名,而是建立正确的心智模型:
• 常规SELECT / UPDATE / DELETE 的主流判断,仍然主要围绕索引路径展开
• 但 openGauss 在少数场景下,确实会为单机 DML 再增加一条专门的优化分支
• 因此当你看到Seq Scan 下的[Bypass] Delete 时,不应该拿它去反推普通点查的规则
很多人在阅读 OpFusion 时最容易犯的错误,就是把一个特例路径理解成整个框架的普遍规律。
命中之后到底省掉了什么
只讲“为什么失败”还不够。要真正理解OpFusion 为什么值得存在,还得顺着命中路径再往里看一步:它到底帮系统省掉了什么。
从 OpFusion::fusionExecute()、executeInit() 和 executeEnd() 的实现看,这条快路径至少在三个层面上比通用执行器更薄。
第一,它不需要为每次执行都构造一棵完整的通用执行状态树。
通用执行器的优势是适配面广。无论是什么计划节点,只要满足统一接口,就可以接入PlanState 树,按递归方式执行。代价也同样明确:哪怕是最简单的 SQL,也要先走一套完整的通用框架。OpFusion 的取舍刚好相反,它接受支持面更窄,换来执行路径更短。
第二,它把一部分解释性工作前移到了初始化阶段。
以 InsertFusion 为例,哪些列来自参数,哪些列来自常量,哪些列需要简单函数求值,它在初始化阶段就会整理好。真正执行时,更多是在按既定模板填值,而不是现场重新解释 targetlist。
第三,它缩短了资源和状态的生命周期。
executeInit() 会完成权限检查、只读事务校验和 snapshot 注册;executeEnd() 负责回收 snapshot、重置临时内存、清理 hash 中的 fusion 对象,并更新统计和审计信息。整个过程并没有省掉数据库必须完成的动作,但它围绕的是专用执行对象,而不是一整棵通用节点树。
所以如果要把收益说得更直白一点,可以概括成一句话:
OpFusion 省掉的不是数据库必须做的工作,而是那些为了支持“通用性”才不得不长期背着的框架性成本。
这也是为什么它特别适合高频、短小、模板稳定的 OLTP 语句。对于这类语句,真正昂贵的部分未必总是 IO,本身的执行框架成本也可能占到不小比重。
`[No Bypass] reason` 不是点缀,而是定位入口
只要做过几次真实排查,很快就会发现,OpFusion 最有价值的输出并不是[Bypass] 这三个字,而是[No Bypass] reason 后面的那句原因说明。
这句提示不是临时拼接出来的字符串,它背后有一套明确的映射关系:
1. getBypassReason() 负责把 FusionType 或 NOBYPASS_* 枚举翻译成可读文本。
2. BypassUnsupportedReason() 负责在 opfusion_debug_mode == BYPASS_LOG 时把这段文本输出出来。
这个设计至少有两个明显好处。
第一个好处是,它把失败原因从“隐式逻辑”变成了“显式协议”。
也就是说,不命中 bypass 并不是后端默默回退,而是会明确告诉你,它到底拒绝了哪一类条件。
第二个好处是,它把源码阅读门槛显著降下来了。
你不需要先把 opfusion_util.cpp 从头读到尾,才能猜出该看哪个分支。更有效的方式是先把 reason 打出来,再顺着这句提示回到对应的判断函数。
举个最典型的例子。如果日志里出现的是:
Bypass not executed because query used limit grammar with a non-constant value
这时就没必要再去怀疑索引、分区形态或者触发器了。定位范围已经收窄到LIMIT 的形态,下一步直接回头检查getSelectFusionType() 对 limitCount 和 limitOffset 的判断即可。
这比“凭经验猜大概是哪条规则卡住了”要高效得多。
三个值得直接看的回归样例
为了避免文章变成纯概念分析,下面直接看三个当前仓库里已经存在的回归样例。它们不是临时拼出来的演示 SQL,而是 openGauss 自己用来验证 bypass 行为的用例。
例子一:标准 `INSERT` 的命中场景
在 src/test/regress/expected/bypass_simplequery_support.out 中,可以搜索这条 SQL:
explain insert into test_bypass_sq1 values (0,0,'test_insert');
对应输出是:
[Bypass]
Insert on test_bypass_sq1
-> Result
这个例子看起来非常朴素,但恰恰因此最有代表性:单表、单VALUES、没有returning、targetlist 形态也足够规整。对 OpFusion 来说,这正是最标准的一类命中模板。
它说明了一件很重要的事:OpFusion 最擅长的,不是去加速那些已经很复杂的 SQL,而是处理业务里大量重复出现、结构高度稳定的短语句。
例子二:为什么 `LIMIT -1` 一定失败
仍然在 bypass_simplequery_support.out 中,可以搜索:
limit -1
你会看到类似下面的输出:
[No Bypass]reason: Bypass not executed because query used limit count grammar with const less than zero.
这个例子特别适合说明一个事实:bypass 并不只是看索引。
这条 SQL 的表结构、单表形态和访问背景本身都不算复杂,真正触发回退的,是 LIMIT 已经超出了模板允许的范围。对于已经把索引设计得还不错的业务来说,这类例子很有价值,因为它提醒你继续排查时不要只盯着访问路径,也要看表达式和分页写法有没有把 SQL 从模板里踢出去。
例子三:单机 `DELETE` 的 `Seq Scan` 旁路
第三个例子建议看 src/test/regress/expected/single_node_delete_optimize.out,搜索:
DELETE FROM table_noindex WHERE id_idx BETWEEN 0 and 8000
你会看到一个非常容易让人误解的结果:
[Bypass]
Delete on delete_optimize.table_noindex
-> Seq Scan on delete_optimize.table_noindex
这条输出的价值在于,它直接打破了一个过度简化的认知:OpFusion 并不等于“只处理索引点查”。
更准确的说法应该是:
• 主流路径的判断,确实围绕IndexScan / IndexOnlyScan
• 但单机场景下,enable_iud_fusion 又为某些 DELETE 语句保留了 SeqScan 特例
如果不知道这条旁路,很多人第一次看到这个样例时都会怀疑是不是文档和实现不一致。其实不是。问题不在文档,而在于你不能用普通点查的判断模型去解释所有 bypass 现象。
真正有用的调优顺序,是先缩小原因,再决定要不要改 SQL
如果目标是让一条 SQL 更接近 bypass,我不建议一开始就改写语句或者盲目加 Hint。更有效的做法,是先按下面这个顺序缩小原因。
第一步,先判断是不是“模板问题”。
也就是先看计划是否已经脱离 simple plan,是否带子计划、多语句,或者复杂 ModifyTable 结构。如果这一层过不去,后面就没必要继续纠结索引和表达式。
第二步,再判断是不是“访问路径问题”。
看最终计划是否真的落在IndexScan / IndexOnlyScan,以及索引条件、排序方式是否足够规整。很多 SQL 最终没命中 bypass,并不是表达式太复杂,而是执行计划已经自然落到了别的扫描节点。
第三步,再判断是不是“表达式问题”。
重点检查 LIMIT、参数类型、targetlist、简单函数以及系统列。很多 SQL 读起来很短,但对快路径来说,模板已经不再稳定。
第四步,最后才是“表属性问题”。
包括分区表、存储格式、触发器、规则、子表、重分布状态等。这些因素一旦介入,往往意味着你再怎么修 SQL 也不一定有效,因为真正挡路的是 relation 自身的属性。
如果把这四步排查顺序建立起来,OpFusion 的定位就会从“经验判断”变成一套比较机械、但很可靠的诊断过程。
而机械并不是坏事,它意味着你可以少拍脑袋,多看证据。
写在最后
OpFusion 最值得重视的地方,不是它让某几条 SQL “看起来更快”,而是它把一类非常典型的 OLTP 语句从通用执行器框架里剥离出来,单独做成了专用执行器。
这背后的工程逻辑其实很清楚:内核承认通用性是有成本的,也承认有些语句的结构稳定到值得单独优化。
所以更成熟的调优思路,不应该是“开了开关却没命中,那一定是数据库不够聪明”。更现实的判断应该是:
• 这条 SQL 有没有落进快路径愿意处理的模板
• 如果没有,缺的是哪一层条件
• 这个条件应该通过改 SQL 解决,还是通过改索引、改表设计,或者干脆接受它回到通用执行器
这也是我认为 OpFusion 最有意思的地方。它不是一个停留在宣传层面的性能术语,而是一套已经把“命中”和“拒绝”都编码得相当明确的内核机制。只要愿意沿着FusionType、NOBYPASS_* 和 [No Bypass] reason 往下走,很多原本模糊的性能问题,其实都可以被拆成一组很具体的实现约束。
而这种可拆解性,本身就比一句笼统的“性能提升了多少”更有价值。
参考源码路径
• src/gausskernel/process/tcop/postgres.cpp
• src/gausskernel/runtime/opfusion/opfusion.cpp
• src/gausskernel/runtime/opfusion/opfusion_select.cpp
• src/gausskernel/runtime/opfusion/opfusion_insert.cpp
• src/gausskernel/runtime/opfusion/opfusion_util.cpp
• src/include/opfusion/opfusion.h
• src/include/opfusion/opfusion_util.h
• src/common/backend/utils/misc/guc/guc_sql.cpp
• src/common/backend/utils/misc/guc.cpp
• src/test/regress/expected/bypass_simplequery_support.out
• src/test/regress/expected/single_node_delete_optimize.out
参考链接
• openGauss 文档:SQL by Pass
<https://docs.opengauss.org/zh/docs/latest/docs/AboutopenGauss/SQL-by-pass.html>
• openGauss 文档:记录日志的内容中的 opfusion_debug_mode
<https://docs.opengauss.org/zh/docs/5.0.0/docs/DatabaseReference/%E8%AE%B0%E5%BD%95%E6%97%A5%E5%BF%97%E7%9A%84%E5%86%85%E5%AE%B9.html>
• openGauss 文档:其他优化器选项中的 enable_opfusion / enable_partition_opfusion
<https://docs.opengauss.org/zh/docs/2.1.0/docs/Developerguide/%E5%85%B6%E4%BB%96%E4%BC%98%E5%8C%96%E5%99%A8%E9%80%89%E9%A1%B9.html>


