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.
SELECT ( SELECT SUM(cost) FROM mat WHERE part = 'beam' ) AS beams, ( SELECT SUM(cost) FROM mat WHERE part = 'rope' ) AS ropes;
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;
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.
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).