iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

CTEs (WITH)

A Common Table Expression (CTE) is a named subquery that lives for the duration of the statement. CTEs make complex queries readable; RECURSIVE CTEs walk trees and graphs in SQL.

CTE + recursive CTE + UPDATE-FROM-CTE

EXAMPLE
-- 1) Basic CTE — read top-to-bottom
WITH recent_orders AS (
    SELECT id, user_id, total, created_at
    FROM orders
    WHERE created_at >= now() - interval '30 days'
)
SELECT u.email, sum(o.total) AS revenue
FROM recent_orders o
JOIN users u ON u.id = o.user_id
GROUP BY u.email
ORDER BY revenue DESC
LIMIT 50;

-- 2) Multiple CTEs — pipeline of named steps
WITH paid AS (
    SELECT * FROM orders WHERE status = 'paid'
), per_user AS (
    SELECT user_id, count(*) n, sum(total) revenue
    FROM paid
    GROUP BY user_id
), top_50 AS (
    SELECT * FROM per_user ORDER BY revenue DESC LIMIT 50
)
SELECT t.*, u.email FROM top_50 t JOIN users u ON u.id = t.user_id;

-- 3) Recursive CTE — walk a tree (e.g. nested categories)
WITH RECURSIVE descendants AS (
    SELECT id, parent_id, name, 1 AS depth
    FROM categories
    WHERE id = 5                              -- start node
    UNION ALL
    SELECT c.id, c.parent_id, c.name, d.depth + 1
    FROM categories c
    JOIN descendants d ON c.parent_id = d.id
)
SELECT * FROM descendants ORDER BY depth, name;

-- 4) Recursive CTE — generate a series of dates (no need for generate_series)
WITH RECURSIVE days AS (
    SELECT date '2026-06-01' AS d
    UNION ALL
    SELECT d + 1 FROM days WHERE d < date '2026-06-30'
)
SELECT d FROM days;

-- 5) Modifying CTE — INSERT/UPDATE/DELETE inside a WITH and return rows
WITH deactivated AS (
    UPDATE users
    SET status = 'inactive'
    WHERE last_seen < now() - interval '180 days'
    RETURNING id, email
)
INSERT INTO audit_log(action, user_id, payload)
SELECT 'deactivate', id, jsonb_build_object('email', email)
FROM deactivated;

-- 6) MATERIALIZED vs NOT MATERIALIZED (PG 12+) — control inlining
WITH base AS NOT MATERIALIZED (
    SELECT * FROM big_table WHERE region = 'AU'
)
SELECT count(*) FROM base WHERE active;
-- NOT MATERIALIZED lets the planner push predicates IN; better most of the time.

-- 7) Window function combined with CTE
WITH ranked AS (
    SELECT o.*,
           row_number() OVER (PARTITION BY user_id ORDER BY total DESC) rn
    FROM orders o
)
SELECT * FROM ranked WHERE rn <= 3;       -- top-3 orders per user

Why it matters

WITH RECURSIVE turns SQL into a graph language. Walk org-trees, dependency graphs, or generate sequences without leaving the database — one round-trip, one planner pass.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
WITH recent AS (
    SELECT * FROM orders WHERE created_at >= now() - interval '7 days'
)
SELECT user_id, sum(total) AS spent
FROM recent
GROUP BY user_id
ORDER BY spent DESC;
Try it Yourself »

Exercise

Start a Common Table Expression.

recent AS (SELECT * FROM orders)

Test yourself

Q1. A CTE is introduced by the keyword…
Q2. Recursive CTEs need…
Q3. A CTE is visible…

Discussion

Loading…