Contents
CTE 支持设计:适配 PG18 原生 CteScan
背景
ORCA 为 CTE 生成的 DXL 物理计划结构(Sequence + CTEProducer + CTEConsumer)无法直接映射到
PG18 的执行模型。当前翻译层遇到这三类节点时直接抛出异常,导致所有含 CTE 的查询回退到
standard_planner。
本文档描述将 ORCA DXL CTE 节点翻译为 PG18 原生 CteScan 的完整设计方案。放弃
CustomScan 路线,改为让翻译层将 ORCA 的计划结构"折叠"为 PG18 期望的形式:CTE 子计划进入
PlannedStmt.subplans,主树中以 CteScan 节点引用它。
PG18 CTE 预处理与 ORCA 的关系
PG18 在 subquery_planner() 入口处调用 SS_process_ctes()(subselect.c:880),对每个
CTE 做内联或物化的二选一决策。ORCA 通过 planner_hook 在此之前接管规划,因此两套
逻辑是平行独立的,但有以下关键影响:
ORCA 接管时 CTE 尚未被 PG 处理
SS_process_ctes() 从未执行,query->cteList 保留全部原始 CTE,所有 FROM 子句中的引用
仍为 RTE_CTE。ORCA 看到的是完整未展开的 WITH 子句。
ORCA 自行决定内联还是物化
ORCA 内部有 CXformInlineCTEConsumer(及 CXformInlineCTEConsumerUnderSelect)变换,
会对满足条件的 CTE 做内联优化。若 ORCA 选择内联,DXL 计划中不出现
CTEProducer/Consumer,翻译层无需处理;若 ORCA 选择物化,才会生成
Sequence + CTEProducer + CTEConsumer 结构,翻译层需将其折叠为 PG18 的 CteScan。
结论:翻译层只需处理 ORCA 选择物化的情况。
ctematerialized 字段应被 ORCA 尊重
CommonTableExpr.ctematerialized 在 parse 阶段由语法设置:
| 值 | 含义 |
|---|---|
CTEMaterializeDefault |
用户未指定,由优化器自行决定 |
CTEMaterializeAlways |
用户显式写了 MATERIALIZED |
CTEMaterializeNever |
用户显式写了 NOT MATERIALIZED |
ORCA 的 CXformInlineCTEConsumer 应检查此字段:CTEMaterializeAlways 时禁止内联,
CTEMaterializeNever 时强制内联。若当前 ORCA 忽略此字段,则用户的显式 hint 会被忽略,
属于已知缺陷。
cterefcount 在规划时已是最终值
cterefcount 在 parse analysis 阶段(parse_relation.c:2359)按 RTE_CTE 引用次数累加,
ORCA 翻译 query->cteList 时可直接使用此值辅助决策(例如:引用次数为 1 且无 volatile
函数时,倾向内联)。PG18 原生的内联条件是 CTEMaterializeDefault && cterefcount == 1,
ORCA 的 xform 启发式规则应与之对齐。
PG18 原生 CTE 执行模型
计划树结构
PlannedStmt {
planTree: CteScan { ctePlanId=1, cteParam=0, scanrelid=N }
rtable: [ ..., RTE_CTE { ctename="cte", ctelevelsup=0 }, ... ]
subplans: [ <CTE 子计划 Plan*> ] ← subplans[ctePlanId-1]
paramExecTypes: [ InvalidOid ] ← param 0 的类型占位
}
关键字段
| 字段 | 含义 |
|---|---|
CteScan.ctePlanId |
1-based,指向 PlannedStmt.subplans[ctePlanId-1] |
CteScan.cteParam |
PARAM_EXEC 槽编号,executor 用来共享 CteScanState* 指针(leader-follower 机制) |
CteScan.scan.scanrelid |
rtable 中对应 RTE_CTE 的 1-based 索引 |
Executor 执行流程
ExecInitCteScan:- 通过
ctePlanId-1在es_subplanstates中找到 CTE 子计划的PlanState - 检查
es_param_exec_vals[cteParam]:若为 NULL,当前节点为 leader,创建 tuplestore, 将自身指针写入 param 槽;否则为 follower,从 leader 的cte_table分配独立读指针
- 通过
CteScanNext:按需从 CTE 子计划拉取行并追加到 tuplestore;多个 CteScan 共享同一 tuplestore,各持独立读指针
ORCA DXL 计划结构
ORCA 为 WITH cte AS (...) SELECT ... FROM cte 生成:
CDXLPhysicalSequence
├── [0] projlist
├── [1] CDXLPhysicalCTEProducer (cte_id=0)
│ ├── [0] projlist
│ └── [1] <CTE 子计划,如 SeqScan/Agg 等>
└── [2] <主计划,内含 CDXLPhysicalCTEConsumer (cte_id=0)>
多个 CTE 时,Sequence 有多个 CTEProducer 子节点,最后一个子节点是主计划。
翻译策略
核心思路
将 ORCA 的"Sequence 驱动 Producer"结构拆解为 PG18 的"initplan + CteScan"结构:
- CDXLPhysicalCTEProducer → 子计划加入
PlannedStmt.subplans,分配 PARAM_EXEC 槽 - CDXLPhysicalSequence → 直接返回最后一个子节点(主计划)的翻译结果,Sequence 节点消失
- CDXLPhysicalCTEConsumer → 生成
CteScan节点 +RTE_CTE条目
翻译顺序保证
TranslateDXLSequence 按 ul = 1 .. arity-1 顺序翻译子节点:
- ul=1:CTEProducer → 调用 TranslateDXLCTEProducerToPlan,此时分配 param_id,
加入 m_subplan_entries_list,记录 cte_id → (plan_id, param_id) 映射
- ul=arity-1:主计划 → 翻译时遇到 CTEConsumer,查映射取 param_id 和 plan_id,
生成 CteScan
Producer 必然先于 Consumer 翻译,映射在 Consumer 翻译时已存在。
数据结构变更
CContextDXLToPlStmt
新增 CTE 映射,替换旧的 SCTEConsumerInfo:
// cte_id → CTEPlanInfo,在 Producer 翻译时写入,Consumer 翻译时读取
struct SCTEPlanInfo {
int plan_id; // 1-based,subplans 中的位置
int param_id; // PARAM_EXEC 槽编号
SCTEPlanInfo(int pid, int prmid) : plan_id(pid), param_id(prmid) {}
};
using HMUlCTEPlanInfo =
CHashMap<ULONG, SCTEPlanInfo, gpos::HashValue<ULONG>, gpos::Equals<ULONG>,
CleanupDelete<ULONG>, CleanupDelete<SCTEPlanInfo>>;
HMUlCTEPlanInfo *m_cte_plan_info; // 替换 m_cte_consumer_info
新增两个方法(替换 AddCTEConsumerInfo / GetCTEConsumerList):
// Producer 翻译时调用:加入 subplans,分配 PARAM_EXEC 槽,记录映射
// 返回分配的 plan_id (1-based)
int RegisterCTEPlan(ULONG cte_id, Plan *cte_subplan);
// Consumer 翻译时调用:取回 plan_id 和 param_id
// 若未找到则 GPOS_ASSERT 失败(说明翻译顺序被破坏)
SCTEPlanInfo GetCTEPlanInfo(ULONG cte_id) const;
RegisterCTEPlan 实现逻辑:
int CContextDXLToPlStmt::RegisterCTEPlan(ULONG cte_id, Plan *cte_subplan)
{
// 加入 subplans 列表(1-based plan_id = 当前长度 + 1)
AddSubplan(cte_subplan);
int plan_id = list_length(m_subplan_entries_list); // 已追加后的长度即 plan_id
// 分配 PARAM_EXEC 槽(InvalidOid,与 PG 原生行为一致)
int param_id = (int) GetNextParamId(InvalidOid);
// 记录映射
ULONG *key = GPOS_NEW(m_mp) ULONG(cte_id);
SCTEPlanInfo *info = GPOS_NEW(m_mp) SCTEPlanInfo(plan_id, param_id);
m_cte_plan_info->Insert(key, info);
return plan_id;
}
CContextDXLToPlStmt.h
- 删除
SCTEConsumerInfo、HMUlCTEConsumerInfo、m_cte_consumer_info - 删除
AddCTEConsumerInfo、GetCTEConsumerList - 新增
SCTEPlanInfo、HMUlCTEPlanInfo、m_cte_plan_info - 新增
RegisterCTEPlan、GetCTEPlanInfo
翻译函数实现
TranslateDXLSequence — 拆解 Sequence
旧行为:生成 Sequence { subplans = [child1, child2, ...] }
新行为:
- 遍历 child[1 .. arity-2](CTEProducer 们),各自翻译为子计划加入 subplans
- 翻译 child[arity-1](主计划)
- 直接返回主计划的 Plan*,Sequence 节点不创建
伪代码:
Plan *
CTranslatorDXLToPlStmt::TranslateDXLSequence(
const CDXLNode *sequence_dxlnode, CDXLTranslateContext *output_context,
CDXLTranslationContextArray *ctxt_translation_prev_siblings)
{
ULONG arity = sequence_dxlnode->Arity();
// child[0] = projlist, child[1..arity-2] = CTEProducers, child[arity-1] = main plan
CDXLTranslateContext child_context(m_mp, false,
output_context->GetColIdToParamIdMap());
// 翻译所有 CTEProducer 子节点(加入 subplans,不进主树)
for (ULONG ul = 1; ul < arity - 1; ul++)
{
CDXLNode *child_dxlnode = (*sequence_dxlnode)[ul];
// 必须是 CTEProducer,否则 GPOS_ASSERT
GPOS_ASSERT(EdxlopPhysicalCTEProducer ==
child_dxlnode->GetOperator()->GetDXLOperator());
TranslateDXLOperatorToPlan(child_dxlnode, &child_context,
ctxt_translation_prev_siblings);
// 注意:CTEProducer 翻译函数负责调用 RegisterCTEPlan,返回值在此丢弃
}
// 翻译最后一个子节点(主计划)并直接返回
CDXLNode *main_dxlnode = (*sequence_dxlnode)[arity - 1];
Plan *main_plan = TranslateDXLOperatorToPlan(main_dxlnode, &child_context,
ctxt_translation_prev_siblings);
// 将主计划的输出列映射传播到 output_context
// (原 Sequence 的 projlist 由主计划的 targetlist 覆盖)
CDXLNode *proj_list = (*sequence_dxlnode)[0];
CDXLTranslationContextArray *child_contexts =
GPOS_NEW(m_mp) CDXLTranslationContextArray(m_mp);
child_contexts->Append(&child_context);
main_plan->targetlist = TranslateDXLProjList(proj_list, nullptr,
child_contexts, output_context);
SetParamIds(main_plan);
child_contexts->Release();
return main_plan;
}
注意:若 Sequence 只有一个子节点(无 CTEProducer,只有主计划),或者 arity == 2,
则 ul=1..arity-2 循环体不执行,直接翻译 child[1] 作为主计划返回。
Sequence 中只有 CTEProducer 被特殊处理;其他类型的非末尾子节点(如分区场景的 DynamicSeqScan
初始化)若将来出现,需扩展此函数。
TranslateDXLCTEProducerToPlan — 生成子计划
Plan *
CTranslatorDXLToPlStmt::TranslateDXLCTEProducerToSharedScan(
const CDXLNode *cte_producer_dxlnode, CDXLTranslateContext *output_context,
CDXLTranslationContextArray *ctxt_translation_prev_siblings)
{
CDXLPhysicalCTEProducer *cte_prod_dxlop =
CDXLPhysicalCTEProducer::Cast(cte_producer_dxlnode->GetOperator());
ULONG cte_id = cte_prod_dxlop->Id();
// 翻译 CTE 子计划(child[1])
CDXLNode *proj_list_dxlnode = (*cte_producer_dxlnode)[0];
CDXLNode *child_dxlnode = (*cte_producer_dxlnode)[1];
CDXLTranslateContext child_context(m_mp, false,
output_context->GetColIdToParamIdMap());
Plan *child_plan = TranslateDXLOperatorToPlan(child_dxlnode, &child_context,
ctxt_translation_prev_siblings);
CDXLTranslationContextArray *child_contexts =
GPOS_NEW(m_mp) CDXLTranslationContextArray(m_mp);
child_contexts->Append(&child_context);
child_plan->targetlist = TranslateDXLProjList(proj_list_dxlnode, nullptr,
child_contexts, output_context);
TranslatePlanCosts(cte_producer_dxlnode, child_plan);
SetParamIds(child_plan);
child_contexts->Release();
// 注册到 subplans,记录 cte_id → (plan_id, param_id)
m_dxl_to_plstmt_context->RegisterCTEPlan(cte_id, child_plan);
// 翻译函数约定返回 Plan*,但 Sequence 翻译器不会使用此返回值
// 返回 child_plan 仅为满足接口签名
return child_plan;
}
TranslateDXLCTEConsumerToSharedScan — 生成 CteScan
Plan *
CTranslatorDXLToPlStmt::TranslateDXLCTEConsumerToSharedScan(
const CDXLNode *cte_consumer_dxlnode, CDXLTranslateContext *output_context,
CDXLTranslationContextArray * /*ctxt_translation_prev_siblings*/)
{
CDXLPhysicalCTEConsumer *cte_consumer_dxlop =
CDXLPhysicalCTEConsumer::Cast(cte_consumer_dxlnode->GetOperator());
ULONG cte_id = cte_consumer_dxlop->Id();
// 查找 Producer 已注册的信息
CContextDXLToPlStmt::SCTEPlanInfo cte_info =
m_dxl_to_plstmt_context->GetCTEPlanInfo(cte_id);
// 在 rtable 中添加 RTE_CTE 条目
RangeTblEntry *rte = makeNode(RangeTblEntry);
rte->rtekind = RTE_CTE;
rte->ctename = pstrdup("<orca_cte>"); // 仅用于 EXPLAIN,可填 CTE 名称
rte->ctelevelsup = 0;
rte->self_reference = false;
rte->eref = makeAlias("<orca_cte>", NIL);
rte->lateral = false;
rte->inh = false;
rte->inFromCl = true;
m_dxl_to_plstmt_context->AddRTE(rte);
Index scan_relid = list_length(m_dxl_to_plstmt_context->GetRTableEntriesList());
// 构建 CteScan 节点
CteScan *cte_scan = makeNode(CteScan);
Plan *plan = &cte_scan->scan.plan;
plan->plan_node_id = m_dxl_to_plstmt_context->GetNextPlanId();
cte_scan->scan.scanrelid = scan_relid;
cte_scan->ctePlanId = cte_info.plan_id; // 1-based subplan index
cte_scan->cteParam = cte_info.param_id; // PARAM_EXEC slot
TranslatePlanCosts(cte_consumer_dxlnode, plan);
// 翻译投影列
CDXLNode *proj_list_dxlnode = (*cte_consumer_dxlnode)[0];
const ULONG num_cols = proj_list_dxlnode->Arity();
plan->targetlist = NIL;
for (ULONG ul = 0; ul < num_cols; ul++)
{
CDXLNode *proj_elem_dxlnode = (*proj_list_dxlnode)[ul];
CDXLScalarProjElem *sc_proj_elem_dxlop =
CDXLScalarProjElem::Cast(proj_elem_dxlnode->GetOperator());
ULONG colid = sc_proj_elem_dxlop->Id();
CDXLNode *sc_ident_dxlnode = (*proj_elem_dxlnode)[0];
CDXLScalarIdent *sc_ident_dxlop =
CDXLScalarIdent::Cast(sc_ident_dxlnode->GetOperator());
OID type_oid = CMDIdGPDB::CastMdid(sc_ident_dxlop->MdidType())->Oid();
INT typmod = sc_ident_dxlop->TypeModifier();
OID collation = gpdb::TypeCollation(type_oid);
// CteScan 从 scanslot 读取,使用 INDEX_VAR(scanrelid 为 CTE RTE)
// 但 PG18 实际以 OUTER_VAR 引用子计划输出,对于 CteScan 而言
// 使用 INDEX_VAR,attno 对应子计划输出列顺序(1-based)
Var *var = gpdb::MakeVar(INDEX_VAR, (AttrNumber)(ul + 1),
type_oid, typmod, collation, 0);
char *resname = CTranslatorUtils::CreateMultiByteCharStringFromWCString(
sc_proj_elem_dxlop->GetColumnName()->GetBuffer());
TargetEntry *te = gpdb::MakeTargetEntry((Expr *) var,
(AttrNumber)(ul + 1),
resname, false);
plan->targetlist = gpdb::LAppend(plan->targetlist, te);
// 向 output_context 注册此列的映射(colid → TargetEntry)
output_context->InsertMapping(colid, te);
}
plan->qual = NIL;
SetParamIds(plan);
return (Plan *) cte_scan;
}
关于 INDEX_VAR vs OUTER_VAR:PG18 ExecInitCteScan 调用
ExecAssignScanProjectionInfoWithVarno(&css->ss, INDEX_VAR),scan tuple slot 按
INDEX_VAR 投影。targetlist 中的 Var 需使用 INDEX_VAR 且 varattno 对应子计划输出
列的位置(1-based),与 ExecScan 框架一致。
PlannedStmt 组装
TranslateDXLToPlan 中已有:
planned_stmt->subplans = m_dxl_to_plstmt_context->GetSubplanEntriesList();
planned_stmt->paramExecTypes = m_dxl_to_plstmt_context->GetParamTypes();
这两行无需修改——RegisterCTEPlan 调用 AddSubplan 和 GetNextParamId,自动维护这两个列表。
planned_stmt->initPlan 保持 NIL。PG18 的 CteScan 使用 ctePlanId 直接索引
es_subplanstates,不需要 initPlan 机制驱动(executor 在 ExecInitNode 阶段统一初始化
所有 subplans)。
多 CTE / 嵌套 CTE
多个独立 CTE
WITH a AS (...), b AS (...)
SELECT ... FROM a JOIN b ON ...
ORCA 生成:
Sequence
├── CTEProducer(cte_id=0) → subplans[0], param_id=0
├── CTEProducer(cte_id=1) → subplans[1], param_id=1
└── HashJoin
├── CTEConsumer(0) → CteScan(ctePlanId=1, cteParam=0)
└── CTEConsumer(1) → CteScan(ctePlanId=2, cteParam=1)
TranslateDXLSequence 遍历 ul=1..arity-2,依次翻译两个 CTEProducer,各自调用
RegisterCTEPlan,分配独立的 plan_id 和 param_id。
嵌套 CTE
WITH a AS (...), b AS (SELECT ... FROM a ...)
SELECT ... FROM b
ORCA 为嵌套 CTE 生成嵌套 Sequence:
Sequence(outer)
├── CTEProducer(cte_id=0) ← a 的定义
└── Sequence(inner)
├── CTEProducer(cte_id=1) ← b 的定义,内含 CTEConsumer(0)
└── CTEConsumer(1) ← 主查询引用 b
翻译时,outer Sequence 翻译 CTEProducer(0) 后,递归翻译 inner Sequence:
- inner Sequence 翻译 CTEProducer(1) 时遇到 CTEConsumer(0),此时映射已存在,生成
CteScan(ctePlanId=1, cteParam=0),整个 CTEProducer(1) 的子计划含一个 CteScan
- inner Sequence 最后返回主计划(含 CTEConsumer(1) 翻译出的 CteScan(ctePlanId=2, cteParam=1))
无需特殊处理嵌套情况,递归翻译天然正确。
同一 CTE 被多次引用
WITH cte AS (...) SELECT * FROM cte c1, cte c2
ORCA 生成一个 CTEProducer + 两个 CTEConsumer。RegisterCTEPlan 只调用一次(Producer),
两个 Consumer 调用 GetCTEPlanInfo 得到相同的 plan_id 和 param_id,生成两个
CteScan 节点。
PG18 executor 的 leader-follower 机制处理多个 CteScan:第一个初始化的成为 leader 并创建 tuplestore,第二个成为 follower 并分配独立读指针,共享同一 tuplestore,互不干扰。
递归 CTE
递归 CTE(WITH RECURSIVE)是独立问题,不在本次范围内。ORCA 不支持递归 CTE 优化(直接
回退到 standard_planner),此设计不改变这一行为。
需修改/新增的文件
| 文件 | 变更 |
|---|---|
include/gpopt/translate/CContextDXLToPlStmt.h |
删除 SCTEConsumerInfo/HMUlCTEConsumerInfo/m_cte_consumer_info/AddCTEConsumerInfo/GetCTEConsumerList;新增 SCTEPlanInfo/HMUlCTEPlanInfo/m_cte_plan_info/RegisterCTEPlan/GetCTEPlanInfo |
gpopt/translate/CContextDXLToPlStmt.cpp |
实现 RegisterCTEPlan、GetCTEPlanInfo;删除旧 CTE 方法 |
gpopt/translate/CTranslatorDXLToPlStmt.cpp |
重写 TranslateDXLSequence(折叠为主计划)、TranslateDXLCTEProducerToSharedScan(生成子计划)、TranslateDXLCTEConsumerToSharedScan(生成 CteScan) |
不需要新增文件,不需要 CustomScan 注册,不需要修改 pg_orca.cpp。
边界情况与约束
| 场景 | 处理方式 |
|---|---|
| Sequence 只有主计划(arity=2) | ul=1..arity-2 范围为空,直接翻译并返回 child[1] |
| Consumer 翻译时 cte_id 未找到 | GetCTEPlanInfo 中 GPOS_ASSERT 失败,说明 ORCA 生成了非预期的结构 |
| Sequence 的非末尾子节点不是 CTEProducer | GPOS_ASSERT 失败,目前 ORCA 不会生成此类结构 |
| EXPLAIN(不执行) | CteScan 是普通计划节点,EXPLAIN 按标准路径工作,无副作用 |
| Consumer Rescan | 由 nodeCtescan.c 原生处理:tuplestore_select_read_pointer + tuplestore_rescan |
| nParamExec | GetNextParamId 维护 m_param_types_list,planned_stmt->paramExecTypes 自动正确 |
| RTE_CTE 的 ctename | 填写实际 CTE 名称需要从 DXL metadata 获取;V1 可填占位符,不影响执行正确性,仅影响 EXPLAIN 显示 |
| 递归 CTE | 继续回退到 standard_planner,无变化 |