SubPlan & InitPlan
Appears in EXPLAIN asSubPlan · InitPlan
Sub-SELECTs evaluated per row (SubPlan) or once (InitPlan).
What it is
A SubPlan is a sub-SELECT executed in the context of its parent — often once per outer row (e.g. a correlated subquery or EXISTS). An InitPlan is a sub-SELECT with no outer dependency, so it runs once and its result is reused (e.g. an uncorrelated scalar subquery).
When the planner picks it
For subqueries the planner can't turn into joins: correlated subqueries and some EXISTS/IN become SubPlans; uncorrelated scalar subqueries become InitPlans.
Is it good or bad?
An InitPlan is cheap — computed once. A correlated SubPlan re-run for every outer row can be very expensive; rewriting it as a JOIN or using EXISTS often lets the planner avoid the per-row execution.
In depth
Two flavors of sub-SELECT
A sub-SELECT that the planner couldn't merge into the surrounding query appears as one of two related nodes (really, plan attachments):
InitPlan— uncorrelated. It depends on nothing from the outer query, so it runs once and its result is reused. The poster child is a scalar subquery likeWHERE x = (SELECT MAX(x) FROM t).SubPlan— correlated. It references the outer query's current row, so it's evaluated per outer row (or per distinct value, with caching). A correlatedEXISTSorINthat the planner couldn't transform into a semi-join is the typical case.
EXPLAIN shows them as named blocks attached to the plan ("InitPlan 1 (returns $0)", "SubPlan 2") with their own subtree.
InitPlan: usually cheap
An InitPlan runs once at plan startup. The cost is whatever the subquery costs; the parameter feeds into the outer plan as a constant. Nothing special to tune.
SubPlan: the dangerous one
A correlated SubPlan is the per-row engine that can quietly dominate runtime.
A simple SELECT count(*) FROM t WHERE EXISTS (SELECT 1 FROM big WHERE big.id = t.id) can become a SubPlan over big that runs once per row of
t — a giant nested loop in disguise.
Two angles to attack it:
- Let the planner convert it to a join. EXISTS / IN with non-trivial
subqueries are often transformable into a semi-join — make sure the
subquery isn't fenced by
OFFSET,LIMIT, or volatile functions that block the rewrite. - Rewrite as an explicit join with
EXISTS/LATERAL. Sometimes the easiest cure is to express the subquery as a realLEFT JOINor aLATERALclause; the planner has more freedom to pick a hash or merge join.
Our analyzer flags these via the "Nested Loop rescans …" finding when the inner is a re-executed subplan.
What the analyzer flags here
- Nested Loop rescans — a correlated subplan re-evaluated many times