Common table expressions (CTEs)
A CTE (Common Table Expression) is a named temporary result set defined with the WITH keyword. Think of it as giving a subquery a name so you can reference it like a table. The result: dramatically more readable multi-step queries.
In PostgreSQL 12+, non-recursive CTEs are inlined by the optimizer, so they perform identically to subqueries. The benefit is purely readability: named steps are easier to write, debug, and maintain than deeply nested subqueries.
-- Nested subquery: hard to read
SELECT * FROM (
SELECT seller_id, SUM(price) AS revenue
FROM order_items
GROUP BY seller_id
) sub
WHERE revenue > (SELECT AVG(revenue) FROM (
SELECT seller_id, SUM(price) AS revenue
FROM order_items
GROUP BY seller_id
) sub2);
-- Same query as a CTE: much clearer
WITH seller_revenue AS (
SELECT seller_id, SUM(price) AS revenue
FROM order_items
GROUP BY seller_id
)
SELECT *
FROM seller_revenue
WHERE revenue > (SELECT AVG(revenue) FROM seller_revenue);The CTE version defines seller_revenue once and references it twice. No duplicated logic, no deep nesting.
The real power of CTEs comes when you chain multiple together. Each CTE can reference the ones defined before it, creating a clear step-by-step pipeline. The second step below sneaks in RANK() OVER (ORDER BY total_revenue DESC), a window function that numbers rows 1, 2, 3 by revenue. Window functions get their own deep dive later; for now just read revenue_rank as a position number.
-- Multi-CTE pipeline: each CTE builds on the previous
WITH seller_revenue AS (
SELECT
seller_id,
SUM(price) AS total_revenue,
COUNT(*) AS items_sold
FROM order_items
GROUP BY seller_id
),
seller_ranking AS (
SELECT
seller_id,
total_revenue,
items_sold,
RANK() OVER (ORDER BY total_revenue DESC) AS revenue_rank
FROM seller_revenue
)
SELECT *
FROM seller_ranking
WHERE revenue_rank <= 10;Multiple CTEs separated by commas create a pipeline. Each CTE can reference the ones defined before it.
Cte pipeline flow
In PostgreSQL 12+, non-recursive CTEs are inlined by the optimizer, so they perform the same as subqueries. Before PG12, CTEs were always materialized (computed once, stored in memory). You can force materialization with AS MATERIALIZED if needed, but the default (inlined) is usually best.
Code playground: Customer spending tiers
Loading practiceโฆ
Quiz: Quiz
Loading practiceโฆ
AI prompt: Subquery vs cte decision tree
Loading practiceโฆ
Flashcards: Flashcards
Loading practiceโฆ