ProjectSet
Appears in EXPLAIN asProjectSet
Evaluates set-returning functions in the SELECT list.
What it is
A ProjectSet node evaluates set-returning functions (such as generate_series() or unnest()) that appear in a SELECT list, expanding each input row into zero or more output rows.
When the planner picks it
When the target list contains a set-returning function.
Is it good or bad?
Fine in moderation. A set-returning function that explodes each row into many can blow up downstream work — prefer a LATERAL join to a FROM-clause function when the output is large.
In depth
Set-returning functions in SELECT
A ProjectSet node appears when a set-returning function (SRF) is used
in the target list of a SELECT — not in FROM:
SELECT id, unnest(tags) AS tag FROM articles;
SELECT generate_series(1, 5) FROM users;
For each input row, ProjectSet evaluates the SRF and emits zero or more output rows. A single input row with a 10-element array becomes 10 output rows; an empty array produces nothing.
Two surprises to expect
- Row counts explode. Every other node downstream sees the expanded
stream. Joins or aggregates over
unnest(tags)operate on element-rows, not on the original rows — easy to miss when reading a plan. - The expression order matters. Multiple SRFs in the same SELECT used
to run in lockstep (the so-called "least common multiple" behavior); since
PostgreSQL 10 they produce the cross product. If you genuinely need
per-row paired iteration, use
LATERALinFROMinstead.
Prefer LATERAL when the output is large
ProjectSet over a small number of input rows is fine. When the SRF expands
a lot of rows from many inputs, moving the SRF into FROM … LATERAL often
plans better — the planner sees it as a join with its own row count and can
choose a hash/merge strategy:
SELECT a.id, t.tag
FROM articles a
CROSS JOIN LATERAL unnest(a.tags) AS t(tag);