Subqueries & CTEs
Concept  ·  SQL & Querying

Whether a CTE is inlined or materialized is engine-dependent — readability isn't free (bridge to P17).

Treat WITH as naming, not caching: it buys readable steps, testable in isolation (run the CTE alone — the debugging habit), and a single definition for reused logic. Whether reuse means recompute or compute-once is engine policy: an inlined CTE referenced twice runs twice; a materialized one computes once but becomes an optimization fence. Hence the MATERIALIZED knob where it exists, or a temp table where it doesn't.

See it break
mat
partcost
beam40
rope5
beam40
The trap
SELECT
  (
    SELECT
      SUM(cost) FROM
      mat
    WHERE
      part = 'beam'
  ) AS beams,
  (
    SELECT
      SUM(cost)
    FROM
      mat
    WHERE
      part = 'rope'
  ) AS ropes;
The fix
WITH
  totals AS (
    SELECT
      part,
      SUM(cost) AS c
    FROM
      mat
    GROUP BY
      part
  )
SELECT
  (
    SELECT
      c
    FROM
      totals
    WHERE
      part = 'beam'
  ) AS beams,
  (
    SELECT
      c
    FROM
      totals
    WHERE
      part = 'rope'
  ) AS ropes;
INPUTYour queryThe fix
8055

Both return the same totals, but the copy-pasted subquery repeats the logic — a CTE names the step once; whether the engine inlines it (runs twice) or materializes it (once) is engine-dependent, decided by the plan.

The rule

CTEs for structure always; materialization decided by the plan, not superstition.

Postgres 12+ inlines by default with MATERIALIZED / NOT MATERIALIZED to force either; older Postgres always materialized (the folklore's origin); most engines treat CTEs as views (inline).

Shows up elsewhere
Reading a plan for two smellsCorrelated vs uncorrelated
Try one →Prove it in practice →