Modules / Database / CTE Lab

Common Table Expressions in SQL

The WITH clause names a query so you can build on it. Watch a nested subquery unfold into a readable pipeline of steps — then run a recursive CTE one iteration at a time.

Common Table Expressions: Naming Your Steps

A CTE does not make queries more powerful. It makes them readable — and it unlocks recursion.

Quick Context

A Common Table Expression is a named temporary result set defined with WITH, available only to the statement that follows it. Think of it as a variable for a query: compute something once, give it a name, then use that name.

WITH high_earners AS ( SELECT * FROM employees WHERE salary > 70000 ) SELECT dept, COUNT(*) FROM high_earners GROUP BY dept;

Why Not Just Nest Subqueries?

You can — the nested example in this lab returns exactly the same rows. But compare how they read. A nested query is evaluated inside out, so you must find the innermost parenthesis and work outward, holding each layer in your head. A chain of CTEs reads top to bottom, like a recipe.

Each CTE can also reference the ones declared before it, which is what turns a tangle into a pipeline. And a CTE referenced twice is written once, rather than copy-pasted into two places that can drift apart.

Recursive CTEs: The Real Superpower

This is the thing subqueries genuinely cannot do. A recursive CTE has two halves joined by UNION ALL:

  • The anchor — a plain query producing the starting rows. It runs once.
  • The recursive member — a query that references the CTE itself. It runs repeatedly, each time seeing only the rows produced by the previous round, until a round returns nothing.

Press Step in the recursive example and watch one level of the org chart appear per iteration. This is how you query hierarchies of unknown depth: reporting lines, folder trees, bill-of-materials, graph traversal, or simply generating a series of numbers or dates.

Always ensure the recursion terminates. If the recursive member never stops producing new rows you have an infinite loop; most engines cap it, but a cycle in your data (A reports to B, B reports to A) will hit that cap rather than finish.

A Note on Performance

Historically PostgreSQL treated CTEs as an optimisation fence — always materialised, never merged into the outer query, which sometimes made them slower than the equivalent subquery. Since version 12 it inlines them by default, with MATERIALIZED and NOT MATERIALIZED available to force the choice. Most other engines have always inlined them. Treat CTEs as free for readability, but check the plan if a query is unexpectedly slow.

Interactive Exploration Guide

  1. Start with the chained CTEs. Each named step shows its own output, so you can see the data narrowing at every stage.
  2. Switch to the nested subquery version. Identical result, one dense block, nesting depth 3 — and no way to inspect the middle.
  3. Open the recursive example and press Step. Iteration 1 finds the CEO, iteration 2 their direct reports, and so on down the tree.
  4. Keep stepping until it stops. The final iteration returns zero new rows — that empty result is the termination condition, not a row limit.

Key Takeaway

Use CTEs to turn one unreadable query into several named steps that a colleague can follow. Use WITH RECURSIVE when the data is a hierarchy and you do not know how deep it goes — that is a problem plain SQL simply cannot express.

Cheat sheet

Common Table Expressions in SQL

The WITH clause names a query so you can build on it. Watch a nested subquery unfold into a readable pipeline of steps — then run a recursive CTE one iteration at a time.

DATABASE · vizlearn.in/database/common_table_expressions_in_sql.html