Free — no signup required

Common Table Expressions (CTEs)

2 min read

Writing SQL That Humans Can Read

As queries grow in complexity, nesting subqueries inside subqueries creates code that is nearly impossible to debug or maintain:

-- Hard to read: what does this even do?
SELECT region FROM (
    SELECT region, SUM(amount) AS total
    FROM (
        SELECT * FROM orders WHERE status = 'completed'
    ) AS completed_orders
    GROUP BY region
) AS regional_totals
WHERE total > 100000;

Common Table Expressions (CTEs), defined with the WITH keyword, solve this by letting you name intermediate result sets and reference them by name — like defining variables at the top of a function.

WITH completed_orders AS (
    SELECT * FROM orders WHERE status = 'completed'
),
regional_totals AS (
    SELECT region, SUM(amount) AS total
    FROM   completed_orders
    GROUP BY region
),
top_regions AS (
    SELECT region
    FROM   regional_totals
    WHERE  total > 100000
)
SELECT * FROM top_regions;

This reads like a story: "First, get completed orders. Then, sum them by region. Then, keep only the top regions. Finally, show me the result."

Recursive CTEs

CTEs have a powerful variant: recursive CTEs, which allow a query to reference itself. This is the standard SQL way to traverse hierarchical data — org charts, file system trees, bill-of-materials structures.

-- Walk an org chart: find all reports under manager_id = 1
WITH RECURSIVE org_tree AS (
    -- Base case: start with the top-level manager
    SELECT id, name, manager_id, 1 AS depth
    FROM   employees
    WHERE  id = 1

    UNION ALL

    -- Recursive case: find employees who report to someone already in the tree
    SELECT e.id, e.name, e.manager_id, ot.depth + 1
    FROM   employees e
    JOIN   org_tree ot ON e.manager_id = ot.id
)
SELECT * FROM org_tree ORDER BY depth;

The recursive CTE has two parts separated by UNION ALL: the base case (the starting row) and the recursive case (how to find the next level). The database repeats the recursive case until no new rows are added.

A critical safety note: always include a depth counter or a WHERE depth < N guard in recursive CTEs. A cycle in the data (employee A reports to B, B reports to A) will cause infinite recursion and crash the query.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free