Chapter 17.5 · Query Planning and Execution · 17 min read
DXL: Translating Queries Between ORCA and Cloudberry
The DXL interchange format — query to DXL, ORCA's plan back to a PlStmt, and how to dump and replay a minidump to reproduce a plan offline.
By Tushar Pednekar · · Verified against the Apache Cloudberry source tree, read September 2026
A minidump is the closest thing MPP has to a reproducible bug report. This is the format it is written in.
ORCA does not speak PostgreSQL. It has never seen a Query node, it cannot follow a varno into a range table, and it will not hand you a PlannedStmt. What it speaks is DXL — Data eXchange Language, an XML dialect that carries query trees, plan trees and metadata. So between standard_planner() (§17.1) and the memo (§17.2) there is a membrane, and it has two faces: CTranslatorQueryToDXL going in, CTranslatorDXLToPlStmt coming out. Everything in between is a different type system. That is the cost of the design; the reward is that a query plus its metadata can be written to a single file and re-optimized on a laptop, which is what the last part of this section demonstrates.
17.5.1 Three libraries, three jobs
The tree under src/backend/gporca/ is not one library but four, and the split is worth internalising because it explains the naming you will meet everywhere. libgpos is an operating-system abstraction: pools, exceptions, tasks. libnaucrates is serialization and metadata interchange — DXL lives here. libgpopt is the optimizer proper. libgpdbcost is the cost model (§17.4).
| Library | Holds | Scale (.cpp / .h) | Why you keep seeing it |
|---|---|---|---|
libgpos | src/{memory,error,task,io,string,common} | 98 / 145 | memory pools (CMemoryPool, CAutoMemoryPool), the exception machinery, CTask — the environment libgpopt runs inside instead of PostgreSQL’s |
libnaucrates | src/{operators,parser,md,statistics,xml} | 414 / 454 | DXL: 41 CDXLPhysical*, 19 CDXLLogical*, 60 CDXLScalar* operator classes, 42 metadata classes, and 185 CParseHandler* SAX handlers that read DXL back in |
libgpopt | src/{base,engine,search,xforms,operators,mdcache,minidump,hints} | 440 / 520 | the optimizer: memo and search (§17.2), transformations (§17.3), metadata accessor (§17.4), minidump (below) |
libgpdbcost | CCostModelGPDB.cpp + params | 3 / 2 | the cost model, kept deliberately pluggable — §17.4 |
The
GPOS_*macros arelibgpos’s replacements for language features PostgreSQL’s C world does differently.GPOS_NEW(mp)is placement-new into a memory pool (libgpos/include/gpos/memory/CMemoryPool.h:311) and must be freed withGPOS_DELETE;GPOS_RAISE(…)isCException::Raise(FILE, LINE, …)(libgpos/include/gpos/error/CException.h:22), which is why an ORCA error message ends in a source location like (init.cpp:58);GPOS_TRY/GPOS_CATCH_EX(ex)/GPOS_CATCH_END(:35,:49) wrap C++try/catchso an error handler can be attached to the enclosing task. §17.1 showed the outermost of thoseGPOS_TRYblocks — the one that turns any escaping exception into fall back to the PostgreSQL planner.
17.5.2 Query → DXL: losing the range table on purpose
COptTasks::OptimizeTask builds the translator with a factory function, CTranslatorQueryToDXL::QueryToDXLInstance (src/backend/gpopt/translate/CTranslatorQueryToDXL.cpp:243), which allocates a CContextQueryToDXL — the per-query state shared by every nested translation level — and then calls TranslateQueryToDXL() (:813). That driver is a four-way switch on commandType, dispatching to TranslateSelectQueryToDXL, TranslateInsertQueryToDXL, TranslateDeleteQueryToDXL or TranslateUpdateQueryToDXL; a SELECT whose parentStmtType is not PARENTSTMTTYPE_NONE goes to TranslateCTASToDXL instead. Scalar expressions are handed to a companion, CTranslatorScalarToDXL.cpp, and a few constructs are rewritten before translation by CQueryMutators.cpp. Alongside the DXL tree the translator hands back four things the caller needs: GetQueryOutputCols() (:590), GetCTEs() (:604), and the two distribution facts HasDistributedTables() / GetDistributionHashOpsKind() (src/include/gpopt/translate/CTranslatorQueryToDXL.h:439 and :446).
The single most consequential thing this translator does is destroy the range table. A PostgreSQL Var is a coordinate — (varlevelsup, varno, varattno) — meaningless without the rtable it indexes into. DXL has no range table; it has flat integer colids. CMappingVarColId performs the collapse, keying a hash map on a CGPDBAttInfo built from the absolute query level and the Var’s coordinates:
src/backend/gpopt/translate/CMappingVarColId.cpp:67
// absolute query level of var
ULONG abs_query_level = current_query_level - var->varlevelsup;
// extract varno
ULONG var_no = var->varno;
…
CGPDBAttInfo *gpdb_att_info =
GPOS_NEW(m_mp) CGPDBAttInfo(abs_query_level, var_no, var->varattno);
CGPDBAttOptCol *gpdb_att_opt_col_info =
m_gpdb_att_opt_col_mapping->Find(gpdb_att_info);
GetColId() (:125) is the thin public face of that lookup. Once it has run, every column in the query is a bare integer, and ORCA can reason about equivalence classes, functional dependencies and required output columns without ever resolving a relation — which is exactly what makes the optimizer independent of PostgreSQL’s parse representation. The reverse trip (§17.5.5) has to rebuild the coordinates from scratch.
The flattened column space, seen from inside ORCA. This build is —enable-cassert, which is why the developer print GUCs emit at all.
SET client_min_messages=log;
SET optimizer_print_query=on;
EXPLAIN SELECT t1.b, t2.c FROM m175_t t1 JOIN m175_t t2
ON t1.a=t2.a WHERE t1.b=3;
LOG: 2026-08-14 00:31:57:075798 CST,THD000,TRACE,"
Algebrized query:
+--CLogicalSelect
|--CLogicalNAryJoin
| |--CLogicalGet "t1" ("t1"), Columns: ["a" (0,Used), "b" (1,Used), "c" (2,Unused), … "gp_foreign_server" (10,Unknown)] Key sets: {[3,9]}
| |--CLogicalGet "t2" ("t2"), Columns: ["a" (11,Used), "b" (12,Unused), "c" (13,Used), … "gp_foreign_server" (21,Unknown)] Key sets: {[3,9]}
| +--CScalarCmp (=)
| |--CScalarIdent "a" (0,Used)
| +--CScalarIdent "a" (11,Used)
+--CScalarCmp (=)
|--CScalarIdent "b" (1,Used)
+--CScalarConst (3)
",
Two tables, twenty-two columns, one flat namespace 0…21 — including the system columns, which ORCA carries because a DML plan may need ctid and gp_segment_id. Note also what optimizer_print_query actually prints: not the DXL, but the CExpression tree that ORCA built after reading the DXL back in. There are therefore two colid spaces, and they differ by one here: the translator numbers from GPDXL_COL_ID_START = 1 (src/include/gpopt/translate/CContextQueryToDXL.h:24, used by the CIdGenerator in CContextQueryToDXL.cpp:25), while CTranslatorDXLToExpr mints fresh CColRefs from 0 as it parses. t1.b is colid 2 in the query DXL and colid 1 in everything the optimizer prints. Both are flat integers; neither is a coordinate.
17.5.3 Where “GPORCA does not support …” actually comes from
§17.1 showed the user-visible fallback. Its source is here. The query translator raises ExmiQuery2DXLUnsupportedFeature in 52 places; CTranslatorScalarToDXL.cpp adds 20 more and CTranslatorUtils.cpp four. The shortest example is a static table of node tags checked before translation even starts:
src/backend/gpopt/translate/CTranslatorQueryToDXL.cpp:292
static const SUnsupportedFeature unsupported_features[] = {
{T_RowExpr, GPOS_WSZ_LIT("ROW EXPRESSION")},
{T_RowCompareExpr, GPOS_WSZ_LIT("ROW COMPARE")},
{T_FieldStore, GPOS_WSZ_LIT("FIELDSTORE")},
{T_CoerceToDomainValue, GPOS_WSZ_LIT("COERCETODOMAINVALUE")},
{T_GroupId, GPOS_WSZ_LIT("GROUPID")},
{T_CurrentOfExpr, GPOS_WSZ_LIT("CURRENT OF")},
};
Two rows of that table, made visible. The DETAIL text is the string literal from the array.
SET optimizer_trace_fallback=on;
EXPLAIN SELECT ROW(a,b) FROM m175_t;
EXPLAIN SELECT * FROM m175_t WHERE (a,b) < (5,5);
INFO: GPORCA failed to produce a plan, falling back to Postgres-based planner
DETAIL: Falling back to Postgres-based planner because GPORCA does not support the following feature: ROW EXPRESSION
…
Optimizer: Postgres query optimizer
INFO: GPORCA failed to produce a plan, falling back to Postgres-based planner
DETAIL: Falling back to Postgres-based planner because GPORCA does not support the following feature: ROW COMPARE
The sentence itself is not in the translator; it is the message template attached to the exception minor code in libnaucrates/src/exception.cpp:115, declared at severity ExsevNotice with one %ls parameter — the feature name. That is the whole mechanism: raise a typed exception carrying a wide string, let the boundary in CGPOptimizer.cpp decide whether it is an expected fallback or a real error (§17.1). The same table also drives the second escape hatch — GetDistributionHashOpsKind() and HasDistributedTables() are consulted right after translation and converted into trace flags that reshape the search space before it exists:
src/backend/gpopt/utils/COptTasks.cpp:978
BOOL is_master_only =
!optimizer_enable_motions ||
(!optimizer_enable_motions_masteronly_queries &&
!query_to_dxl_translator->HasDistributedTables());
// See NoteDistributionPolicyOpclasses() in .../CTranslatorQueryToDXL.cpp
BOOL use_legacy_opfamilies =
(query_to_dxl_translator->GetDistributionHashOpsKind() ==
DistrUseLegacyHashOps);
CAutoTraceFlag atf1(EopttraceDisableMotions, is_master_only);
CAutoTraceFlag atf2(EopttraceUseLegacyOpfamilies,
use_legacy_opfamilies);
A query touching only coordinator-local tables therefore runs with Motion enforcement switched off entirely, and a table created under the legacy hash opfamilies (NoteDistributionPolicyOpclasses, CTranslatorQueryToDXL.cpp:3452) makes ORCA hash the same way the old code did, so that a redistribution actually lands rows where the storage expects them. §17.6 covers the trace-flag mechanism in general; these two are set by the translator, not by a GUC.
17.5.4 Seeing the DXL
DXL is not a debug format — it is the interface — so you can ask for it directly. EXPLAIN has an undocumented DXL option (src/backend/commands/explain.c:268) whose handler ExplainDXL (:427) redoes the pre-processing of §17.1 and then calls SerializeDXLPlan (:490), which lands in COptTasks::Optimize and prints the serialized physical plan instead of a plan tree.
The physical plan, in the form ORCA actually emits it. Trimmed; the Properties/ProjList blocks repeat at every level.
EXPLAIN (DXL on) SELECT t1.b, t2.c FROM m175_t t1 JOIN m175_t t2
ON t1.a=t2.a WHERE t1.b=3;
<?xml version="1.0" encoding="UTF-8"?>
<dxl:DXLMessage xmlns:dxl="http://greenplum.com/dxl/2010/12/">
<dxl:Plan Id="0" SpaceSize="8">
<dxl:GatherMotion InputSegments="0,1,2" OutputSegments="-1">
<dxl:Properties>
<dxl:Cost StartupCost="0" TotalCost="862.227940" Rows="285.997152" Width="9"/>
</dxl:Properties>
<dxl:ProjList>
<dxl:ProjElem ColId="1" Alias="b">
<dxl:Ident ColId="1" ColName="b" TypeMdid="0.23.1.0"/>
</dxl:ProjElem>
…
</dxl:ProjList>
<dxl:Filter/>
<dxl:SortingColumnList/>
<dxl:HashJoin JoinType="Inner">
…
<dxl:HashCondList>
<dxl:Comparison ComparisonOperator="=" OperatorMdid="0.96.1.0">
<dxl:Ident ColId="11" ColName="a" TypeMdid="0.23.1.0"/>
<dxl:Ident ColId="0" ColName="a" TypeMdid="0.23.1.0"/>
</dxl:Comparison>
</dxl:HashCondList>
<dxl:TableScan>
…
<dxl:TableDescriptor Mdid="6.34547.1.0" TableName="t2" LockMode="1" AclMode="2">
<dxl:Columns>
<dxl:Column ColId="11" Attno="1" ColName="a" TypeMdid="0.23.1.0" ColWidth="4"/>
…
</dxl:Columns>
</dxl:TableDescriptor>
</dxl:TableScan>
…
</dxl:HashJoin>
</dxl:GatherMotion>
</dxl:Plan>
</dxl:DXLMessage>
Everything in the memo dump of §17.2 is recognisable here, spelled differently: CPhysicalMotionGather(master) is dxl:GatherMotion with OutputSegments="-1" (segment −1 being the coordinator); CPhysicalInnerHashJoin is dxl:HashJoin with its HashCondList; CPhysicalTableScan is dxl:TableScan plus a TableDescriptor. The costs are ORCA’s own numbers, unchanged — 862.227940 is the cost=0.00..862.23 the EXPLAIN footer will show. And the Attno attribute inside dxl:Column is the only place a PostgreSQL attribute number survives in the plan: it is the crumb the reverse translator uses to rebuild Vars for base relations.
The
Mdidattributes are ORCA’s metadata identifiers and they decode cleanly (libnaucrates/include/naucrates/md/IMDId.h:71): the first field is the mdid kind — 0EmdidGeneral, 1EmdidColStats, 2EmdidRelStats, 6EmdidRel— then the object OID, then a version pair. So6.34547.1.0is relation 34547 (m175_t),0.23.1.0is type 23 (integer),0.96.1.0is operator 96 (=), and1.34547.1.0.1is the column statistics of attribute 1. Every one of those is a request the MD accessor of §17.4 satisfied from the relcache.
Redistribute Motion, and the hash expression list that decides where each row goes. Note the slice numbers.
EXPLAIN SELECT count(*) FROM m175_t t1 JOIN m175_t t2 ON t1.b=t2.b;
EXPLAIN (DXL on) SELECT count(*) FROM m175_t t1 JOIN m175_t t2 ON t1.b=t2.b;
Finalize Aggregate (cost=0.00..862.97 rows=1 width=8)
-> Gather Motion 3:1 (slice1; segments: 3) (cost=0.00..862.97 rows=1 width=8)
-> Partial Aggregate (cost=0.00..862.97 rows=1 width=8)
-> Hash Join (cost=0.00..862.97 rows=190477 width=1)
Hash Cond: (t1.b = t2.b)
-> Redistribute Motion 3:3 (slice2; segments: 3) ...
Hash Key: t1.b
-> Seq Scan on m175_t t1 ...
-> Hash (cost=431.03..431.03 rows=667 width=4)
-> Redistribute Motion 3:3 (slice3; segments: 3) ...
Hash Key: t2.b
-> Seq Scan on m175_t t2 ...
Optimizer: GPORCA
<dxl:RedistributeMotion InputSegments="0,1,2" OutputSegments="0,1,2">
…
<dxl:HashExprList>
<dxl:HashExpr Opfamily="0.1977.1.0">
<dxl:Ident ColId="1" ColName="b" TypeMdid="0.23.1.0"/>
</dxl:HashExpr>
</dxl:HashExprList>
Opfamily="0.1977.1.0" is integer_ops — the redistribution names the opfamily whose hash function must be used, which is the DXL-level echo of the legacy-opfamily flag above and the reason ORCA can be trusted to co-locate rows with a hash-distributed table. Hash Key: t1.b in the EXPLAIN is this element, one translation later.
17.5.5 DXL → PlannedStmt: rebuilding PostgreSQL
The way back is COptTasks::ConvertToPlanStmtFromDXL (src/backend/gpopt/utils/COptTasks.cpp:305), which creates three CIdGenerators — plan ids from 0, motion ids from 1, param ids from 0 — wraps them in a CContextDXLToPlStmt and calls CTranslatorDXLToPlStmt::GetPlannedStmtFromDXL (src/backend/gpopt/translate/CTranslatorDXLToPlStmt.cpp:168). This is ORCA’s create_plan and its set_plan_references (§15.4) in one recursive pass, and its hardest job is the inverse of §17.5.2: turning colids back into coordinates. CMappingColIdVarPlStmt::VarFromDXLNodeScId (CMappingColIdVarPlStmt.cpp:127) tries the base-table context first, and failing that searches the child translation contexts — found in the first child means OUTER_VAR, in the second means INNER_VAR — then builds the node:
src/backend/gpopt/translate/CMappingColIdVarPlStmt.cpp:223
Var *var = gpdb::MakeVar(varno, attno,
CMDIdGPDB::CastMdid(dxlop->MdidType())->Oid(),
dxlop->TypeModifier(),
0 // varlevelsup
);
// set varnoold and varoattno since makeVar does not set them properly
var->varnosyn = varno_old;
var->varattnosyn = attno_old;
OUTER_VAR / INNER_VAR and the varnosyn/varattnosyn bookkeeping should look familiar — this is the same representation set_plan_references leaves behind in a PostgreSQL plan, which is why EXPLAIN can print t2.a = t1.a for an ORCA plan at all. The range table is built rather than consumed: each base-table translation appends an RangeTblEntry through CContextDXLToPlStmt::AddRTE (CContextDXLToPlStmt.cpp:201), and the assembly at the end of GetPlannedStmtFromDXL walks that list to collect relationOids, then fills rtable, permInfos, subplans, paramExecTypes, resultRelations and planGen = PLANGEN_OPTIMIZER into a fresh PlannedStmt (:244–:266).
The CBDB-critical part is that the same pass builds the slice table. GetPlannedStmtFromDXL starts by allocating slice 0 — sliceIndex = 0, parentIndex = -1, gangType = GANGTYPE_UNALLOCATED — and making it current (:176–:191). Then every DXL Motion encountered during the recursion opens a new slice for its sending side before descending into its child:
src/backend/gpopt/translate/CTranslatorDXLToPlStmt.cpp:2502
PlanSlice *sendslice = (PlanSlice *) gpdb::GPDBAlloc(sizeof(PlanSlice));
memset(sendslice, 0, sizeof(PlanSlice));
sendslice->sliceIndex = m_dxl_to_plstmt_context->AddSlice(sendslice);
sendslice->parentIndex = recvslice->sliceIndex;
m_dxl_to_plstmt_context->SetCurrentSlice(sendslice);
AddSlice simply assigns list_length(m_slices_list) and appends (CContextDXLToPlStmt.cpp:323), so slice numbers are the depth-first discovery order of the Motions — which is precisely the slice1/slice2/slice3 numbering in the session above, and why the Gather is always slice 1. The gang type is decided from the Motion’s InputSegments: one input segment means a singleton or entry-DB reader, several means GANGTYPE_PRIMARY_READER across m_num_of_segments (:2510–:2545); motion->motionID = sendslice->sliceIndex (:2569) ties the executor’s Motion node to its slice. At the end, planned_stmt->slices = m_dxl_to_plstmt_context->GetSlices(&planned_stmt->numSlices) (:262) materialises the flat array, and direct dispatch is stamped onto it from the DXL’s DirectDispatchInfo. The result is the same PlannedStmt shape §16.3 produced and the same slice table §13.3’s dispatcher consumes.
It really is the same shape but a completely separate route. ORCA plans never see
set_plan_references()orcdbllize_build_slice_table():standard_planner()returns immediately after a successful ORCA call (src/backend/optimizer/plan/planner.c:437,if (result) return result;) and those two functions are only reached further down, at:767and:788, on the PostgreSQL-planner path. Two independent implementations of slice construction, one output format — a duplication worth remembering when a slice-related bug reproduces under only one optimizer.
17.5.6 Minidump: the whole optimization in one file
Because the input, the metadata and the output all have a serialized form, an entire optimization can be written to one file and replayed. That file is a minidump, and it is assembled inside COptimizer::PdxlnOptimize (libgpopt/src/optimizer/COptimizer.cpp:230) by declaring four RAII “serializable” objects on the stack — CSerializableStackTrace, CSerializableOptimizerConfig, CSerializableMDAccessor, CSerializableQuery (:273–:277) — plus CSerializablePlan once a plan exists (:341). Each one registers with a CMiniDumperDXL; CMinidumperUtils::Finalize then walks them and writes the document. Crucially CSerializableMDAccessor dumps what the optimizer actually asked for, so the file carries exactly the metadata the search consumed — the same MD-provider seam as §17.4, only readable from a file instead of the relcache.
Producing one. The name comes from CMinidumperUtils::GenerateMinidumpFileName (CMinidumperUtils.cpp:143) and the template in CUtils::GenerateFileName (CUtils.cpp:3522): minidumps/Minidump_%04d%02d%02d_%02d%02d%02d_sessionid_cmdid.mdp, relative to the backend’s cwd — the coordinator data directory.
-- session-level only; the default is optimizer_minidump = onerror
SET optimizer_minidump=always;
EXPLAIN SELECT t1.b, t2.c FROM m175_t t1 JOIN m175_t t2
ON t1.a=t2.a WHERE t1.b=3;
$ ls -la $COORDINATOR_DATA_DIRECTORY/minidumps/
Gather Motion 3:1 (slice1; segments: 3) (cost=0.00..862.23 rows=286 width=9)
-> Hash Join (cost=0.00..862.22 rows=96 width=9)
…
Optimizer: GPORCA
total 44
drwx------ 2 gpadmin gpadmin 49 Aug 14 00:34 .
drwx------ 23 gpadmin gpadmin 4096 Aug 14 00:34 ..
-rw------- 1 gpadmin gpadmin 40929 Aug 14 00:34 Minidump_20260814_003424_6153_1.mdp
Forty kilobytes for a two-table join, and no newlines — it is one long line of XML. Unpacked, the order of sections is the order the serializable objects were declared, and it is a complete recipe:
The anatomy of a .mdp, extracted from the real file. Long attribute lists trimmed.
$ head -c 260 Minidump_20260814_003424_6153_1.mdp
$ python3 -c "..." # locate each section in the single-line document
<?xml version="1.0" encoding="UTF-8"?><dxl:DXLMessage xmlns:dxl="http://greenplum.com/dxl/2010/12/"><dxl:Thread Id="0"><dxl:OptimizerConfig><dxl:EnumeratorConfig Id="0" PlanSamples="0" CostThreshold="0"/><dxl:StatisticsConfig DampingFactorFilter="0.750000" …
119 <dxl:OptimizerConfig> … CostModelConfig CostModelType="1" SegmentsForCosting="3", Hint, WindowOids
930 <dxl:TraceFlags Value="102001,102002,…,103001,103014,…,106000"/>
1145 <dxl:Metadata SystemIds="0.GPDB">
1178 <dxl:Type Mdid="0.16.1.0" Name="bool" IsRedistributable="true" IsHashable="true" …/>
6455 <dxl:ColumnStatistics Mdid="1.34547.1.0.1" Name="b" Width="4.000000" NdvBySeg="21.000000">
<dxl:StatsBucket Frequency="0.14249856770038605" DistinctValues="1.0"> … (107 buckets)
31729 <dxl:RelationStatistics Mdid="2.34547.1.0" Name="m175_t" Rows="2000.000000" RelPages="3"/>
31859 <dxl:Relation Mdid="6.34547.1.0" Name="m175_t" StorageType="Heap"
DistributionPolicy="Hash" DistributionColumns="0" Keys="9,3">
33268 <dxl:GPDBScalarOp Mdid="0.96.1.0" Name="=" ComparisonType="Eq" …/>
33955 <dxl:Query><dxl:OutputColumns>…</dxl:OutputColumns><dxl:CTEList/>
<dxl:LogicalSelect><dxl:Comparison ComparisonOperator="="…>
<dxl:LogicalJoin JoinType="Inner">
<dxl:LogicalGet HasSecurityQuals="false"><dxl:TableDescriptor …TableName="t1">
<dxl:LogicalGet HasSecurityQuals="false"><dxl:TableDescriptor …TableName="t2">
36820 <dxl:Plan Id="0" SpaceSize="0"><dxl:GatherMotion InputSegments="0,1,2" …
There is the logical side of the membrane at last: dxl:LogicalSelect over dxl:LogicalJoin over two dxl:LogicalGets — the input to §17.2’s memo, in which Group 0 and Group 1 were exactly those two Gets. And there is the reason the file is a complete bug report. dxl:OptimizerConfig pins the cost model and the damping factors; dxl:TraceFlags records the 24 flags that were live, including 103001 (EopttraceMinidump itself) and the 102xxx band, which is EopttraceDisableXformBase + xformID — the exact set of transformations that had been switched off (libnaucrates/include/naucrates/traceflags/traceflags.h:92); the metadata section carries every type, operator, relation and histogram the optimizer touched; and dxl:Plan records what it decided. Replaying it cannot drift, because nothing is read from a live catalog.
That replay is not hypothetical. src/backend/gporca/server/ is the offline harness — a standalone binary, its own CMakeLists.txt, a DXL XML schema in server/dxl.xsd (1747 lines), and 25 minidump test classes whose engine is CMinidumperUtils::PdxlnExecuteMinidump (CICGTest.cpp:208). Its corpus is src/backend/gporca/data/dxl/minidump/: 1187 checked-in .mdp files, each one a query, its metadata and its expected plan. ORCA’s regression suite is therefore a pile of minidumps optimized without a database — which is the practical payoff of paying for the membrane, and something no part of the PostgreSQL planner can do.
Two caveats the source makes plain. First,
optimizer_minidumpis an enum GUC read through abool *(src/backend/gpopt/config/CConfigParamMapping.cpp:92— the comment there admits it is “a bit dirty”), soonerror= 0 leavesEopttraceMinidumpclear andalways= 1 sets it (src/backend/utils/misc/guc_gp.c:518,:5211). Second, the dumper is created insidePdxlnOptimize, so a failure raised earlier — anything inCTranslatorQueryToDXL, which is most fallbacks — cannot be captured. Empirically: aWITH RECURSIVEfallback under the default setting created nominidumps/directory at all. If you are chasing an ORCA bug, setoptimizer_minidump=alwaysand expect a file per statement.
17.5.7 What the membrane costs
The bill comes due when the output translator refuses. CTranslatorDXLToPlStmt has its own unsupported-feature raises, and they fire after the search has already finished:
A fallback discovered on the way out. The optimizer had a finished plan; the reverse translator would not build it.
CREATE FUNCTION m175_trg() RETURNS trigger AS
$$ BEGIN RETURN NEW; END $$ LANGUAGE plpgsql;
CREATE TRIGGER m175_upd BEFORE UPDATE ON m175_t
FOR EACH ROW EXECUTE FUNCTION m175_trg();
SET optimizer_trace_fallback=on;
SET gp_log_optimization_time=on;
EXPLAIN UPDATE m175_t SET c = 'x' WHERE b = 3;
LOG: ...,THD000,NOTICE,"Falling back to Postgres-based planner because GPORCA does not
support the following feature: UPDATE on a table with UPDATE triggers",
INFO: GPORCA failed to produce a plan, falling back to Postgres-based planner
DETAIL: ... GPORCA does not support the following feature: UPDATE on a table with UPDATE triggers
LOG: Optimizer Time: 45.605 ms
LOG: Planner Time: 0.884 ms
Update on m175_t (cost=0.00..9.33 rows=0 width=0)
-> Seq Scan on m175_t (cost=0.00..9.33 rows=95 width=42)
Filter: (b = 3)
Optimizer: Postgres query optimizer
Forty-five milliseconds of memo, transformations and costing, thrown away by a one-line guard at CTranslatorDXLToPlStmt.cpp:5312 — while the PostgreSQL planner needed 0.9 ms to do the whole job. That is the honest shape of the trade. The membrane costs an extra serialization boundary, a second type vocabulary (mdids, colids, DXL operators) that must be kept in step with PostgreSQL’s, and a class of failures that are only discovered on the way out. What it buys is an optimizer that owes nothing to the surrounding server: it can be handed a file instead of a database, its search space can be pinned by a trace-flag list, and its bugs travel as forty-kilobyte attachments. Chapter 18 runs the PlannedStmt that came out the top of the sandwich; §17.6 turns to the last question — how a SET in your session reaches a C++ optimizer that has never heard of a GUC.
More in Query Planning and Execution
- From SQL to Segments: Cloudberry Query Processing
- Statistics and Selectivity Estimation in Cloudberry
- The Postgres Planner in Cloudberry, Step by Step
- Distributed Planning: Locus, Motion and Direct Dispatch
- ORCA Optimizer: Entry Points and Fallback Rules
- Inside ORCA's Cascades Engine: Groups and Jobs