Skip to content

Chapter 17.1 · Query Planning and Execution · 23 min read

ORCA Optimizer: Entry Points and Fallback Rules

When Cloudberry hands a query to ORCA and when it falls back to the Postgres planner — the entry path, unsupported features, and how to see the decision.

By Tushar Pednekar · · Verified against the Apache Cloudberry source tree, read September 2026

ORCA falling back silently is the most common reason a plan changes for no apparent reason. Here is where the decision is made.

Chapter 15 followed a Query tree into PostgreSQL’s planner, and Chapter 16 watched the distributed layer that is bolted onto it. Cloudberry ships a second, entirely separate optimizer — GPORCA — and on this cluster it is the one that runs: show optimizer reads on, and the tell is the last line of every EXPLAIN, Optimizer: GPORCA. The Ch15–16 demos all had to SET optimizer=off to see Optimizer: Postgres query optimizer instead; from here on the demos run at the default. ORCA is a C++ Cascades-style engine living in src/backend/gporca/, reached through the glue in src/backend/gpopt/. It does not extend the planner — it replaces it. This section is the doorway: which queries are handed over, what the handover costs, how a C Query * becomes work inside a C++ task, and, most useful of all in practice, what happens on the way back out when ORCA declines.

COptTasks OptimizeTask · COptTasks.cpp:884
segment, utility mode, or PARALLEL RETRIEVE CURSOR
coordinator QD and optimizer is on
PlannedStmt
GPOS exception: no plan
1 · metadata: CMDCache, CMDProviderRelcache, CMDAccessor ⟦§17.4⟧
2 · LoadSearchStrategy — the search stages ⟦§17.2⟧
3 · PackConfigParamInBitset, SetTraceflags ⟦§17.6⟧
4 · CTranslatorQueryToDXL — Query tree becomes DXL ⟦§17.5⟧
5 · cost model, plan hints, optimizer config ⟦§17.4 · §17.6⟧
6 · COptimizer::PdxlnOptimize — memo, xforms, search ⟦§17.2 · §17.3⟧
7 · CTranslatorDXLToPlStmt — DXL becomes a PlannedStmt ⟦§17.5⟧
8 · missing-stats NOTICE, reset traceflags, teardown ⟦§17.4⟧
Query tree, after parse analysis and rewrite
the gate in standard_planner

planner.c:398
PostgreSQL planner

Ch15 · Ch16
first call in this backend:

InitGPOPT under the GPORCA

Top-level Memory Context
optimize_query · orca.c:202

copyObject · fold_constants

transformGroupedWindows
CGPOptimizer::GPOPTOptimizedPlan

gpopt/CGPOptimizer.cpp:45

GPOS_TRY — the C++ boundary
COptTasks::Execute · COptTasks.cpp:247

runs the work as a GPOS task
did a plan come back?
post-processing in orca.c

then slices, gangs, dispatch — §13.3
The whole itinerary, and a map of the chapter. A Query tree arrives at standard_planner(); one if decides whether ORCA is even offered the query. If it is, optimize_query() does the pre-processing ORCA needs, CGPOptimizer crosses into C++, and COptTasks::Execute runs the real body — OptimizeTask — as a GPOS task with its own memory pool. Stops 1–8 are that body, in source order; the § chip on each stop names the section of this chapter that opens it up. Either exit converges on the same place: a PlannedStmt to dispatch (§13.3), or NULL and the PostgreSQL planner. This section owns only the outer ring — the gate, the shell, and the two exits.

17.1.1 The fork in standard_planner()

There is exactly one fork in the road, and it is a plain if near the top of standard_planner(). Five conditions must all hold before ORCA is offered the query: the optimizer GUC is on; this backend is dispatching (GP_ROLE_DISPATCH); it is the query dispatcher on the coordinator (IS_QUERY_DISPATCHER()); the caller did not ask to skip foreign partitions (a cursor option COPY TO sets, copyto.c:1249); and this is not a PARALLEL RETRIEVE CURSOR, which ORCA does not support:

src/backend/optimizer/plan/planner.c:398

	if (optimizer &&
		GP_ROLE_DISPATCH == Gp_role &&
		IS_QUERY_DISPATCHER() &&
		(cursorOptions & CURSOR_OPT_SKIP_FOREIGN_PARTITIONS) == 0 &&
		(cursorOptions & CURSOR_OPT_PARALLEL_RETRIEVE) == 0)
	{
		...                                     /* one-time InitGPOPT() */
		result = optimize_query(parse, cursorOptions, boundParams,
							   optimizer_options);
		...                                     /* Optimizer Time: ... ms */
		if (result)
			return result;
	}

The long comment above that if is worth reading rather than skipping, because the two role conditions are not arbitrary caution. ORCA is aimed at complex queries, and complex queries touch distributed tables — which a segment slice could not plan usefully anyway, because running such a plan from a segment would mean dispatching a query from inside a query, and Cloudberry forbids that (querytree_safe_for_qe(), src/backend/executor/functions.c:254). The same restriction applies to coordinator slices that are not the QD, which is what IS_QUERY_DISPATCHER() filters. And the planning that does happen on segments is mostly the bodies of pl/language functions, which ORCA does not handle. So the rule is simply: ORCA runs on the coordinator QD, or not at all. Note what is not in the gate — nothing about the query’s shape. Whether ORCA can actually handle this query is discovered much later, by failing (§17.1.5).

The gate seen from the other side. Connecting straight to a segment in utility mode, optimizer still reads on — it is a normal GUC and its value is inherited — yet the plan is unmistakably the PostgreSQL planner’s. The footer is the giveaway.

-- PGOPTIONS='-c gp_role=utility' psql -p 7102 -d postgres
SELECT current_setting('optimizer');
EXPLAIN SELECT count(*) FROM m171_t;
 current_setting 
-----------------
 on
(1 row)

                           QUERY PLAN                           
----------------------------------------------------------------
 Aggregate  (cost=77.58..77.59 rows=1 width=8)
   ->  Seq Scan on m171_t  (cost=0.00..63.66 rows=5566 width=0)
 Optimizer: Postgres query optimizer
(3 rows)

Two consequences are worth stating plainly. First, since only the QD ever plans, the segments only ever receive a finished plan and its slice table (§13.3) — ORCA’s planning time, its memory, and its bugs are a coordinator-side phenomenon, no matter how many segments the plan runs on. Second, that EXPLAIN footer is not a GUC readback: it is plannedstmt->planGen (src/backend/commands/explain.c:858), a field the DXL-to-plan translator stamps as PLANGEN_OPTIMIZER (CTranslatorDXLToPlStmt.cpp:246). It therefore tells you which optimizer actually produced the plan in front of you — which, as we will see, is not always the one you asked for.

17.1.2 One-time init per backend, and what it costs

ORCA is a C++ library with its own memory-pool manager, exception machinery and worker registry, so it must be initialized before first use. That happens lazily, inside the gate, once per backend: a dedicated GPORCA Top-level Memory Context is created under TopMemoryContext, InitGPOPT() runs (gpopt/CGPOptimizer.cpp:163 — it installs CMemoryPoolPallocManager, then gpos_init, gpdxl_init, gpopt_init), and the static optimizer_init flag latches. Because optimizer_use_gpdb_allocators is on, every ORCA memory pool is a plain PostgreSQL AllocSet child of that context (gpopt/gpdbwrappers.cpp:2529) — so ORCA’s memory is visible to, and accounted by, the same machinery as everything else. At backend exit TerminateGPOPT() runs and the whole context is deleted (utils/init/postinit.c:1797).

The one-time init, made visible. A backend started with optimizer=off has never entered the gate, so there is no ORCA memory at all. One ORCA-planned statement later, pg_backend_memory_contexts shows the top-level context plus a pool per ORCA arena — 29 contexts and 3.3 MB that will now live as long as the session does. This is per-backend, not per-query: it is the price of the first ORCA plan in a connection.

-- PGOPTIONS='-c optimizer=off' psql -p 7100 -d postgres
SELECT count(*) AS gporca_contexts, coalesce(sum(total_bytes),0) AS bytes
  FROM pg_backend_memory_contexts WHERE name LIKE 'GPORCA%';
SET optimizer=on;
EXPLAIN SELECT count(*) FROM m171_t;      -- first ORCA plan in this backend
SELECT count(*) AS gporca_contexts, sum(total_bytes) AS bytes
  FROM pg_backend_memory_contexts WHERE name LIKE 'GPORCA%';
 gporca_contexts | bytes 
-----------------+-------
               0 |     0
(1 row)

SET
                                     QUERY PLAN                                     
------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=0.00..431.47 rows=1 width=8)
   ->  Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..431.47 rows=1 width=8)
         ->  Partial Aggregate  (cost=0.00..431.47 rows=1 width=8)
               ->  Seq Scan on m171_t  (cost=0.00..431.44 rows=16667 width=1)
 Optimizer: GPORCA
(5 rows)

 gporca_contexts |  bytes  
-----------------+---------
             29 | 3352488
(1 row)

The gate also brackets the call with a timer. gp_log_optimization_time is the single most useful ORCA knob that costs nothing: when it is on, the ORCA branch logs Optimizer Time (planner.c:434) and the PostgreSQL branch logs Planner Time (planner.c:867). Both lines are elog(LOG, ...), so a session needs SET client_min_messages=log to see them. Run the same query both ways and the chapter’s central trade-off appears immediately — §13.1 measured planning as a small slice of a statement’s life under the PostgreSQL planner; under ORCA it is not small:

Same query, both optimizers, one session. ORCA spends 61 ms where the PostgreSQL planner spends 1 ms — a ratio of roughly 60x on a query far too simple to repay it. (The Planner Hook(s) line comes from the outer planner() wrapper at planner.c:347; pg_stat_statements is preloaded here and hooks it.) Note also the cost columns: ORCA’s 0.00..434.30 is on ORCA’s own scale, not PostgreSQL’s, so the two numbers must never be compared.

SET client_min_messages=log;
SET gp_log_optimization_time=on;
EXPLAIN SELECT a, count(*) FROM m171_t WHERE b < 50 GROUP BY a;
SET optimizer=off;                        -- same query, other optimizer
EXPLAIN SELECT a, count(*) FROM m171_t WHERE b < 50 GROUP BY a;
LOG:  Optimizer Time: 61.179 ms
LOG:  Planner Hook(s): 63.363 ms
                                    QUERY PLAN                                     
-----------------------------------------------------------------------------------
 Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..434.30 rows=24995 width=12)
   ->  HashAggregate  (cost=0.00..433.18 rows=8332 width=12)
         Group Key: a
         ->  Seq Scan on m171_t  (cost=0.00..432.11 rows=8332 width=4)
               Filter: (b < 50)
 Optimizer: GPORCA
(6 rows)

LOG:  Planner Time: 0.987 ms
                                     QUERY PLAN                                      
-------------------------------------------------------------------------------------
 Gather Motion 3:1  (slice1; segments: 3)  (cost=272.99..689.58 rows=24995 width=12)
   ->  HashAggregate  (cost=272.99..356.31 rows=8332 width=12)
   ...
 Optimizer: Postgres query optimizer
(6 rows)

That 61 ms is not the steady state. Repeat one query three times in the same session and the cost falls by 6x: the first call pays for InitGPOPT and for filling the metadata cache from the relcache, the later ones reuse both. The warm number is the honest one to quote — and the cold number is the honest one to remember for short-lived connections. (This build is --enable-cassert, so treat all of these as ratios on one machine, not as benchmarks. What the metadata cache holds and when it is invalidated is §17.4.)

SET client_min_messages=log;
SET gp_log_optimization_time=on;
EXPLAIN SELECT count(*) FROM m171_t;
EXPLAIN SELECT count(*) FROM m171_t;
EXPLAIN SELECT count(*) FROM m171_t;
LOG:  Optimizer Time: 42.743 ms
...
LOG:  Optimizer Time: 21.739 ms
...
LOG:  Optimizer Time: 7.029 ms
...

17.1.3 optimize_query(): the shell around a foreign optimizer

optimize_query() (src/backend/optimizer/plan/orca.c:202) is the last pure-C stop. It is not an optimizer; it is a shell that prepares a Query tree ORCA can digest and then repatriates what ORCA hands back. Four things happen on the way in. An updatable cursor bails out immediately. ORCA gets its own copy of the query, because it mutates what it is given and the caller may need the original for the fallback path. A guard added for extension safety rejects the rare query whose range table is empty but which contains support functions. Then constant folding runs (with a 100 KB cap on any folded result, GPOPT_MAX_FOLDED_CONSTANT_SIZE in optimizer/clauses.h:25) and transformGroupedWindows() rewrites any query that mixes window functions with aggregates into a subquery, because ORCA expects them separated:

src/backend/optimizer/plan/orca.c:218

	if ((cursorOptions & CURSOR_OPT_UPDATABLE) != 0)
		return NULL;                       /* never even calls ORCA */
	...
	pqueryCopy = (Query *) copyObject(parse);
	if (pqueryCopy->rtable == NULL && query_contains_support_functions(pqueryCopy))
		return NULL;
	pqueryCopy = fold_constants(root, pqueryCopy, boundParams,
								GPOPT_MAX_FOLDED_CONSTANT_SIZE);
	pqueryCopy = (Query *) transformGroupedWindows((Node *) pqueryCopy, NULL);
	result = GPOPTOptimizedPlan(pqueryCopy, &fUnexpectedFailure, options);
	log_optimizer(result, fUnexpectedFailure);
	if (!result)
		return NULL;                       /* caller falls through to Postgres */
Query from rewrite copyObject orca.c:248 fold_constants orca.c:274 transformGroupedWindows orca.c:280 GPOPTOptimizedPlan C++ · GPOS task own memory pools DXL · memo · xforms opaque to the planner PlannedStmt rtable · subplans subplan_sliceIds numSlices · slices dummy PlannerGlobal + PlannerInfo orca.c:225 · orca.c:240 glob->finalrtable, glob->subplans glob->subplan_sliceIds, numSlices copied back · orca.c:305 root, only so folding can record dependencies No RelOptInfo is ever built, no pathlist, no add_path (§15.1) — ORCA builds its own memo (§17.2) from DXL (§17.5), and materializes the slice table itself (§16.3) rather than deriving it from Paths.
What crosses the boundary, and what comes back. The PlannerInfo/PlannerGlobal pair at the bottom left is the striking part: optimize_query builds one (orca.c:225, orca.c:240) and the comment in the source calls it what it is — a dummy. ORCA never sees it. root exists so that fold_constants() has somewhere to record function and relation dependencies, and glob exists so the post-processing steps have somewhere to read the finished plan's range table, subplans and slice table from (orca.c:305). Compare §15.1, where PlannerInfo and its RelOptInfo pathlists are the planner: here the entire structure is scaffolding around an opaque call.

On the way out, the plan is already complete: ORCA filled in the final range table, the subplans and the slice table inside the PlannedStmt itself. The post-processing steps copy those five fields into glob, fake a subroot per subplan so the shared walkers do not choke, and then run the same finishing passes the PostgreSQL planner uses. That is the whole reason the dummy struct exists — not to plan, but to let plan-shaped code that expects a PlannerGlobal keep working.

17.1.4 Crossing into C++

GPOPTOptimizedPlan is declared extern in orca.c:50 and defined in C++ at src/backend/gpopt/CGPOptimizer.cpp:45. Its whole job is to be a firewall between two error models: PostgreSQL’s ereport/longjmp on one side, GPOS exceptions on the other. Inside GPOS_TRY it calls into COptTasks; inside GPOS_CATCH_EX it makes one decision — is this a real error the user must see, or merely ORCA declining? A genuine GPDB error that surfaced through ORCA (ExmaGPDB / ExmiGPDBError, typically raised by the gpdb:: wrappers) is re-thrown as a PostgreSQL error and the statement fails. Everything else means no plan, and the caller falls back:

src/backend/gpopt/CGPOptimizer.cpp:63

	GPOS_CATCH_EX(ex)
	{
		CHAR *serialized_error_msg =
			gpopt_context.CloneErrorMsg(MessageContext, &clone_failed);
		if (clone_failed || GPOS_MATCH_EX(ex, gpdxl::ExmaGPDB, gpdxl::ExmiGPDBError))
			PG_RE_THROW();                     /* a real error: the user sees it */
		...
		if (optimizer_trace_fallback)          /* CGPOptimizer.cpp:100 */
		{
			errmsg("GPORCA failed to produce a plan, falling back to Postgres-based planner");
			errdetail("%s", serialized_error_msg);
		}
		*had_unexpected_failure = gpopt_context.m_is_unexpected_failure;
	}
	GPOS_CATCH_END;

Note the CloneErrorMsg on the first line: the message is copied into MessageContext before the ORCA context is torn down, which is why a fallback can still name its cause after all of ORCA’s memory is gone. One level deeper, COptTasks::Execute (COptTasks.cpp:247) is what actually runs the work: it initializes DXL, takes a CAutoMemoryPool, fills a gpos_exec_params with the function to run, a stack base, an abort flag wired to IsAbortRequested, and a fixed-size error buffer, then calls gpos_exec (:272) — ORCA runs as a GPOS task, not as a plain function call, which is what gives it its own memory pool, its own worker registration and its own error context. Whatever the task wrote into that error buffer is flushed to the server log on the way out (LogExceptionMessageAndDelete, :280), which is why ORCA’s own trace lines appear in the log even when nobody enabled any ORCA logging:

ORCA’s internal trace, arriving through the error buffer of the GPOS task. Nothing is enabled here except a lowered message level; the THD000 prefix and the wide-character timestamp are GPOS’s own log format, not PostgreSQL’s — a clear sign that this text was produced inside the C++ task and copied out afterwards.

SET client_min_messages=log;   -- everything else at cluster defaults
EXPLAIN WITH RECURSIVE r(n) AS (
          SELECT 1 UNION ALL SELECT n+1 FROM r WHERE n<5) SELECT * FROM r;
LOG:  2026-08-14 00:30:30:686542 CST,THD000,NOTICE,"Falling back to Postgres-based planner because GPORCA does not support the following feature: WITH RECURSIVE",
                         QUERY PLAN                          
-------------------------------------------------------------
 Recursive Union  (cost=0.00..2.69 rows=34 width=4)
   ->  Result  (cost=0.00..0.01 rows=1 width=4)
   ->  WorkTable Scan on r  (cost=0.00..0.23 rows=3 width=4)
         Filter: (n < 5)
 Optimizer: Postgres query optimizer
(5 rows)

17.1.5 Fallback: the exit that matters most

ORCA’s gate says nothing about what ORCA can handle, so fallback is the normal mechanism by which an unsupported query reaches the PostgreSQL planner. Failures are sorted into two classes. Two arrays in libgpos/src/_api.cpp list the exception ids that mean “we know about this one”: unsupported operators and predicates, no plan found, unsatisfied required properties, and — the big one for SQL features — ExmiQuery2DXLUnsupportedFeature, raised while translating the Query tree into DXL. Anything not on those lists is an unexpected failure, i.e. a probable ORCA bug:

src/backend/gporca/libgpos/src/_api.cpp:51

const ULONG expected_dxl_fallback[] = {
	gpdxl::ExmiMDObjUnsupported,             // unsupported metadata object
	gpdxl::ExmiQuery2DXLUnsupportedFeature,  // unsupported feature, algebrization
	gpdxl::ExmiDXL2PlStmtConversion,         // unsupported in PlannedStmt xlation
	...};

gpos::BOOL
IsLoggableFailure(gpos::CException &exc)
{
	...
	return (!is_opt_failure_expected && !is_dxl_failure_expected);
}

That verdict travels back as fUnexpectedFailure and reaches log_optimizer() (orca.c:61), which is where the two logging knobs bite. optimizer_log defaults to on, but optimizer_log_failure defaults to unexpected — so by default the server log says nothing about routine fallbacks, and says GPORCA failed to produce plan (unexpected) only for suspected bugs. The user-facing switch is a different one: optimizer_trace_fallback turns the same event into an INFO line with the reason attached, which is the first thing to reach for when a plan looks wrong:

The clean case. optimizer_trace_fallback names the unsupported feature in the DETAIL line, and the footer confirms which optimizer produced the plan you are reading. This is the single most useful diagnostic in the chapter, and it is session-level.

SET optimizer_trace_fallback=on;
EXPLAIN WITH RECURSIVE r(n) AS (
          SELECT 1 UNION ALL SELECT n+1 FROM r WHERE n<5) SELECT * FROM r;
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: WITH RECURSIVE
                         QUERY PLAN                          
-------------------------------------------------------------
 Recursive Union  (cost=0.00..2.69 rows=34 width=4)
   ->  Result  (cost=0.00..0.01 rows=1 width=4)
   ->  WorkTable Scan on r  (cost=0.00..0.23 rows=3 width=4)
         Filter: (n < 5)
 Optimizer: Postgres query optimizer
(5 rows)

The scruffy case, and an instructive one. This falls back through exactly the same exception id as WITH RECURSIVE — it is just as expected — but nobody wrote a friendly name for it, so the DETAIL is the raw parse node the translator choked on. Classification and readability are independent: a node dump does not mean a bug, it means an unhandled node type. Read the :op and the node tag, not the noise.

SET optimizer_trace_fallback=on;
EXPLAIN SELECT xmlelement(name foo, a) FROM m171_t;
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: {XMLEXPR :op 1 :name foo :named_args <> :arg_names <> :args
 ({VAR :varno 1 :varattno 1 :vartype 23 :vartypmod -1 :varcollid 0
 :varnullingrels (b) :varlevelsup 0 :varnosyn 1 :varattnosyn 1 :location 36})
 :xmloption 0 :type 142 :typmod -1 :location 15}
                                    QUERY PLAN                                     
-----------------------------------------------------------------------------------
 Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..898.00 rows=50000 width=32)
   ->  Seq Scan on m171_t  (cost=0.00..231.33 rows=16667 width=32)
 Optimizer: Postgres query optimizer
(3 rows)

The classification, proved. With optimizer_log_failure=all the log carries GPORCA failed to produce plan — with no (unexpected) suffix. Switch to the cluster default, unexpected, and the very same statement logs nothing at all: ORCA knew about this one. That is the trap worth internalizing — a silent log is not evidence that no fallback happened.

SET client_min_messages=log;
SET optimizer_log_failure='all';
EXPLAIN WITH RECURSIVE r(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM r WHERE n<5)
        SELECT * FROM r;
SET optimizer_log_failure='unexpected';   -- the default
EXPLAIN WITH RECURSIVE r(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM r WHERE n<5)
        SELECT * FROM r;
LOG:  2026-08-14 00:30:17:782036 CST,THD000,NOTICE,"Falling back to Postgres-based
      planner because GPORCA does not support the following feature: WITH RECURSIVE",
LOG:  GPORCA failed to produce plan
...
 Optimizer: Postgres query optimizer
(5 rows)

LOG:  2026-08-14 00:30:17:803390 CST,THD000,NOTICE,"Falling back to Postgres-based
      planner because GPORCA does not support the following feature: WITH RECURSIVE",
...
 Optimizer: Postgres query optimizer
(5 rows)

Three fallbacks, three timing signatures. The recursive query pays for ORCA in full and then throws it away: 22 ms of ORCA followed by 0.5 ms of PostgreSQL planner for the plan that is actually used. The PARALLEL RETRIEVE CURSOR never opens the gate, so there is no Optimizer Time line at all. The updatable cursor opens the gate but is rejected on the first line of optimize_query — and the tell is unmistakable: 0.001 ms. Learning to read these three shapes tells you where a fallback was decided without reading any source.

SET client_min_messages=log;
SET gp_log_optimization_time=on;
EXPLAIN WITH RECURSIVE r(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM r WHERE n<5)
        SELECT * FROM r;
BEGIN;
DECLARE c1 PARALLEL RETRIEVE CURSOR FOR SELECT * FROM m171_t;
DECLARE c2 CURSOR FOR SELECT * FROM m171_t FOR UPDATE;
ROLLBACK;
-- WITH RECURSIVE: ORCA ran, threw, and the work was wasted
LOG:  Optimizer Time: 22.426 ms
LOG:  Planner Time: 0.460 ms

-- PARALLEL RETRIEVE CURSOR: gated at planner.c:398, ORCA never entered
LOG:  Planner Time: 1.311 ms
DECLARE PARALLEL RETRIEVE CURSOR

-- updatable cursor: gate opened, rejected at orca.c:218
LOG:  Optimizer Time: 0.001 ms
LOG:  Planner Time: 0.222 ms
DECLARE CURSOR
What you runWhere the decision happensHow you find out
WITH RECURSIVE …ORCA exception during Query→DXL; expectedINFO + DETAIL: … does not support the following feature: WITH RECURSIVE
SELECT xmlelement(name foo, a) …same exception id; still expectedDETAIL is a raw node dump, {XMLEXPR :op 1 …}
INSERT … ON CONFLICT DO UPDATEQuery→DXL; expectedDETAIL: … the following feature: ON CONFLICT clause
SELECT … TABLESAMPLE SYSTEM (1)Query→DXL; expectedDETAIL: … TABLESAMPLE in the FROM clause
any distributed query with optimizer_enable_motions=offafter the search: nothing satisfies the required propertiesDETAIL: … no plan has been computed for required properties; ORCA’s own trace line arrives at ERROR, not NOTICE
DECLARE c CURSOR FOR … FOR UPDATEorca.c:218, before ORCA is calledno INFO at all; Optimizer Time: 0.001 ms
DECLARE c PARALLEL RETRIEVE CURSOR FOR …the planner.c:398 gate — ORCA never enteredno INFO, and no Optimizer Time line, only Planner Time
anything on a segment, or in utility modethe same gate (Gp_role, IS_QUERY_DISPATCHER())footer Optimizer: Postgres query optimizer while optimizer still reads on

So the diagnostic order is: read the footer first (which optimizer produced this plan), then SET optimizer_trace_fallback=on and re-EXPLAIN (why did ORCA decline), then SET gp_log_optimization_time=on (where the decision was made, and what it cost). Two calibrations from the experiments above. First, do not guess what falls back: on this cluster GROUPING SETS, percentile_cont(...) WITHIN GROUP, to_tsvector, and a plain SELECT … FOR UPDATE with a subquery all stayed on GPORCA — the list of unsupported features is shorter and stranger than folklore suggests. Second, every fallback provoked here was classified expected, which is precisely why the shipped defaults are quiet; the unexpected ones are the interesting ones, and for those ORCA can write a self-contained, replayable dump of the query, its metadata and its configuration — the machinery in libgpopt/src/minidump/, controlled by optimizer_minidump (default onerror). With the doorway mapped, the rest of the chapter goes through it: §17.2 opens the memo and the search, §17.3 the transformations, §17.4 metadata and cost, §17.5 the DXL translators at both ends, and §17.6 how a SET in your session reaches a C++ optimizer that has never heard of a GUC.

More in Query Planning and Execution