pgconfigurator
pgconfigurator

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

  1. 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.
  2. 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 LATERAL in FROM instead.

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);

See also