---
title: openGauss OpFusion 失效分析-官方技术文章-鲲鹏社区
description: 文章解析了openGauss OpFusion模块的实现机制，揭示SQL未走Bypass的核心原因。通过分析FusionType准入条件、失效原因枚举及调试日志，指出OpFusion本质是针对稳定SQL模板的专用执行器，而非简单绕过通用执行器。文章提供分层排查思路，帮助定位SQL因计划复杂度、扫描路径、表达式形态或表属性等原因未能命中快路径的问题。
keywords: OpFusion,失效分析,官方技术文章,鲲鹏社区,No Bypass,SQL,Pass,AboutopenGauss
url: https://www.hikunpeng.com/developer/techArticles/20260422-2?envFlag=1
section: (其他)
---

# openGauss OpFusion 失效分析-官方技术文章-鲲鹏社区

URL: https://www.hikunpeng.com/developer/techArticles/20260422-2?envFlag=1
描述: 文章解析了openGauss OpFusion模块的实现机制，揭示SQL未走Bypass的核心原因。通过分析FusionType准入条件、失效原因枚举及调试日志，指出OpFusion本质是针对稳定SQL模板的专用执行器，而非简单绕过通用执行器。文章提供分层排查思路，帮助定位SQL因计划复杂度、扫描路径、表达式形态或表属性等原因未能命中快路径的问题。
关键词: OpFusion,失效分析,官方技术文章,鲲鹏社区,No Bypass,SQL,Pass,AboutopenGauss

官方技术文章 [了解详情](https://www.hikunpeng.com/zh/developer/techArticles)

openGauss OpFusion 失效分析

openGauss OpFusion 失效分析

openGauss

发表于 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>
