After the optimizer has found the cheapest Path tree, it must convert it into a Plan tree that the executor can run. This is a mostly mechanical translation, but with important post-processing: target-list construction, qual ordering for security, and – critically – fixing up all Var references so that each plan node references the output columns of its child nodes rather than the original range table. These steps are handled by createplan.c and setrefs.c.
Plan creation has two phases:
Path-to-Plan conversion (createplan.c): recursively walk the Path tree, creating the corresponding Plan nodes. Each Path type maps to a Plan node type (SeqScan, IndexScan, NestLoop, HashJoin, Sort, etc.). The translation fills in executor-specific details that Paths omit: target lists, qual expression trees, plan node parameters.
Reference fixing (setrefs.c): a final pass over the complete Plan tree that replaces table-level Var references with INNER_VAR / OUTER_VAR references to child plan node output columns, resolves regproc OIDs for operators, and performs other executor-oriented cleanups.
| File | Purpose |
|---|---|
src/backend/optimizer/plan/createplan.c |
create_plan(), create_plan_recurse(), and all per-node-type creators |
src/backend/optimizer/plan/setrefs.c |
set_plan_references(), Var fixup, subplan linking |
src/backend/optimizer/plan/planner.c |
subquery_planner() calls create_plan() then set_plan_references() |
src/include/nodes/plannodes.h |
Plan, SeqScan, IndexScan, NestLoop, HashJoin, etc. |
src/include/nodes/pathnodes.h |
Path types that are converted |
The entry point is create_plan() in createplan.c, which calls create_plan_recurse():
Plan *
create_plan(PlannerInfo *root, Path *best_path)
{
Plan *plan;
plan = create_plan_recurse(root, best_path, CP_EXACT_TLIST);
/* Post-creation adjustments */
...
return plan;
}
create_plan_recurse() dispatches on the Path node type:
create_plan_recurse(root, path, flags):
switch path->pathtype:
T_SeqScan: create_seqscan_plan()
T_IndexScan: create_indexscan_plan()
T_IndexOnlyScan: create_indexonlyscan_plan()
T_BitmapHeapScan: create_bitmap_subplan()
T_TidScan: create_tidscan_plan()
T_SubqueryScan: create_subqueryscan_plan()
T_FunctionScan: create_functionscan_plan()
T_NestLoop: create_nestloop_plan()
T_MergeJoin: create_mergejoin_plan()
T_HashJoin: create_hashjoin_plan()
T_Material: create_material_plan()
T_Memoize: create_memoize_plan()
T_Sort: create_sort_plan()
T_IncrementalSort: create_incrementalsort_plan()
T_Group: create_group_plan()
T_Agg: create_agg_plan()
T_WindowAgg: create_windowagg_plan()
T_Unique: create_unique_plan()
T_Gather: create_gather_plan()
T_GatherMerge: create_gather_merge_plan()
T_SetOp: create_setop_plan()
T_LockRows: create_lockrows_plan()
T_ModifyTable: create_modifytable_plan()
T_Limit: create_limit_plan()
T_Result: create_result_plan()
T_Append: create_append_plan()
T_MergeAppend: create_merge_append_plan()
T_RecursiveUnion: create_recursiveunion_plan()
Each plan node needs a target list (plan->targetlist) specifying which columns it outputs. The function build_path_tlist() creates this from the Path’s pathtarget:
static List *
build_path_tlist(PlannerInfo *root, Path *path)
{
/* Convert each expression in path->pathtarget->exprs
* into a TargetEntry */
}
For scan nodes, use_physical_tlist() checks whether we can use a “physical” tlist that simply returns all columns of the table (avoiding projection overhead). This is possible when no expressions need to be computed and when the node does not need to match a specific column layout.
order_qual_clauses() sorts the qual list for each plan node. The ordering respects:
For join nodes, the creator functions do the following:
create_nestloop_plan():
create_hashjoin_plan():
create_mergejoin_plan():
After the Plan tree is built, set_plan_references() performs a critical transformation: it converts all Var nodes from table-level references to execution-level references.
During planning, a Var node like {varno=2, varattno=3} means “column 3 of range-table entry 2.” But the executor doesn’t scan range-table entries directly – it receives tuples from child plan nodes. A join node’s inner child outputs some columns, its outer child outputs others, and the join node must know which of its children provides each column.
set_plan_references() walks the Plan tree bottom-up. For each plan node, it:
Builds an indexed target list for each child plan node, mapping (varno, varattno) pairs to output column positions.
OUTER_VAR references (varno = 65001). Vars from the inner child become INNER_VAR references (varno = 65000).Resolves operator OIDs. During planning, operators may be referenced by name or opclass. setrefs.c ensures all OpExpr nodes have concrete opfuncid values (regproc OIDs) for the executor.
Before setrefs:
HashJoin
qual: Var(t1.x) = Var(t2.y) -- table-level references
-> SeqScan on t1
targetlist: Var(t1.x), Var(t1.z)
-> SeqScan on t2
targetlist: Var(t2.y), Var(t2.w)
After setrefs:
HashJoin
qual: Var(OUTER_VAR, 1) = Var(INNER_VAR, 1) -- positional references
-> SeqScan on t1
targetlist: Var(1, 1), Var(1, 3) -- (rtindex, attnum)
-> SeqScan on t2
targetlist: Var(2, 2), Var(2, 4)
typedef struct Plan
{
NodeTag type;
Cost startup_cost; /* cost before first tuple */
Cost total_cost; /* total cost */
double plan_rows; /* estimated output rows */
int plan_width; /* estimated average row width */
bool parallel_aware;
bool parallel_safe;
int plan_node_id; /* unique ID within PlannedStmt */
List *targetlist; /* target list of TargetEntry nodes */
List *qual; /* filter conditions (implicit AND) */
Plan *lefttree; /* outer (left) input plan */
Plan *righttree; /* inner (right) input plan */
List *initPlan; /* init SubPlan nodes */
Bitmapset *extParam; /* external PARAM_EXEC parameters needed */
Bitmapset *allParam; /* all PARAM_EXEC parameters */
} Plan;
The top-level output of the planner:
typedef struct PlannedStmt
{
NodeTag type;
CmdType commandType; /* SELECT, INSERT, UPDATE, DELETE */
bool hasReturning;
bool hasModifyingCTE;
bool canSetTag;
bool transientPlan; /* should plan be cached? */
bool dependsOnRole;
bool parallelModeNeeded;
int jitFlags; /* JIT compilation flags */
Plan *planTree; /* the actual plan tree */
List *rtable; /* range table */
List *permInfos; /* permission check info */
List *resultRelations;
List *appendRelations;
List *subplans; /* list of SubPlan plan trees */
Bitmapset *rewindPlanIDs; /* plans needing rewind capability */
List *rowMarks; /* FOR UPDATE/SHARE info */
List *relationOids; /* OIDs of relations referenced */
List *invalItems; /* plan cache invalidation triggers */
int nParamExec; /* number of PARAM_EXEC parameters */
} PlannedStmt;
| Path Type | Plan Type |
|---|---|
Path (T_SeqScan) |
SeqScan |
IndexPath |
IndexScan or IndexOnlyScan |
BitmapHeapPath |
BitmapHeapScan + BitmapIndexScan/BitmapAnd/BitmapOr |
NestPath |
NestLoop |
MergePath |
MergeJoin (+ Sort nodes if needed) |
HashPath |
HashJoin + Hash |
SortPath |
Sort |
AggPath |
Agg |
GroupPath |
Group |
WindowAggPath |
WindowAgg |
LimitPath |
Limit |
MaterialPath |
Material |
MemoizePath |
Memoize |
GatherPath |
Gather |
GatherMergePath |
GatherMerge |
AppendPath |
Append |
ModifyTablePath |
ModifyTable |
Path Tree (optimizer output) Plan Tree (executor input)
================================ ================================
HashPath HashJoin
pathtype: T_HashJoin jointype: JOIN_INNER
total_cost: 192.75 hash_qual: t2.y = t1.x
pathkeys: NIL ┌───────────┴───────────┐
┌────────┴────────┐ v v
v v SeqScan on t2 Hash
Path(T_SeqScan) Path(T_SeqScan) targetlist: ┌──┘
parent: t2 parent: t1 [t2.y, t2.w] v
SeqScan on t1
targetlist:
[t1.x, t1.z]
qual: t1.z > 100
After setrefs:
HashJoin
hash_qual: OUTER_VAR.1 = INNER_VAR.1
┌───────────┴───────────┐
v v
SeqScan on t2 Hash
tlist: [col2, col4] |
SeqScan on t1
tlist: [col1, col3]
qual: col3 > 100
If a plan node needs to compute expressions (not just pass through columns), and its child node cannot do the projection, a Result node is inserted as a projection step. The CP_EXACT_TLIST flag in create_plan_recurse() controls when this is necessary.
If a qual can be determined false at plan creation time (e.g., contradictory EquivalenceClass constants), a Result node with a One-Time Filter: false is inserted. This short-circuits execution immediately, returning no rows.
set_plan_references() finalizes the connection between SubPlan nodes in the main plan and their plan trees in PlannedStmt.subplans. Each SubPlan references its plan by index into the subplans list.
For parallel plans, create_gather_plan() or create_gather_merge_plan() wraps the partial plan. The Gather node specifies num_workers and whether the leader participates. set_plan_references() ensures that parallel-safe expressions are used throughout.
subquery_planner()
│
├── preprocessing (prepjointree, prepqual, etc.)
├── grouping_planner()
│ ├── query_planner()
│ │ └── make_one_rel() -> best Path tree
│ └── handle upper-level processing (agg, sort, limit)
│ └── final best Path
│
├── create_plan(best_path) <-- THIS SECTION
│ └── create_plan_recurse()
│ ├── create_seqscan_plan()
│ ├── create_hashjoin_plan()
│ │ ├── recurse for outer
│ │ └── recurse for inner (wrapped in Hash)
│ └── ... other plan types ...
│
└── set_plan_references(plan) <-- THIS SECTION
├── fix Var references (OUTER_VAR, INNER_VAR)
├── resolve operator OIDs
├── link SubPlan references
└── return PlannedStmt
│
v
Executor
| Subsystem | Relationship |
|---|---|
| Path Generation | Produces the Path tree that plan creation converts |
| Join Ordering | Determines which Path tree wins (cheapest at top level) |
| Cost Model | Plan nodes carry the cost estimates from their source Paths |
| Executor | Consumes the Plan tree; each Plan node type has a corresponding executor node |
| Caches | The finished PlannedStmt may be stored in the plan cache for reuse |
| Preprocessing | SubPlan nodes originate from sublinks that could not be pulled up |