DevLearningTools

LEARN · SQL

Common Table Expressions (CTEs)

A CTE names a subquery with WITH so it can be referenced like a table later in the same query. Learn basic CTEs, chaining multiple CTEs, and recursive CTEs.

A Common Table Expression, a CTE, gives a subquery a name using WITH, so the rest of the query can reference it like a regular table. It doesn't do anything a subquery in FROM couldn't already do, the real benefit is readability: breaking a complex query into named, sequential steps instead of nesting everything inside itself.

Learning Objectives

  • Write a basic CTE with WITH, and reference it in the main query.
  • Chain multiple CTEs together in one query.
  • Write a simple recursive CTE to walk a hierarchy.

WITH name AS (...)

Subquery given a name

Named Result

Available like a table

Main Query

References the CTE by name

Final Result

One readable, step-by-step query

A Basic CTE

high_earners is defined once with WITH, then used in the main query exactly like a real table. This is identical in effect to using the same query as a subquery in FROM, written this way it's just easier to read top to bottom.

MySQL / PostgreSQL / SQLite / SQL Server
WITH high_earners AS (
  SELECT first_name, department, salary
  FROM employees
  WHERE salary > 90000
)
SELECT department, COUNT(*) AS headcount
FROM high_earners
GROUP BY department;

Chaining Multiple CTEs

Separate CTEs with a comma, and each one can reference any CTE defined before it. This query first computes department averages, then uses that result to find employees earning above their own department's average, each step named and readable on its own.

MySQL / PostgreSQL / SQLite / SQL Server
WITH department_averages AS (
  SELECT department, AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department
),
above_average AS (
  SELECT e.first_name, e.department, e.salary
  FROM employees e
  JOIN department_averages d ON e.department = d.department
  WHERE e.salary > d.avg_salary
)
SELECT * FROM above_average;

Recursive CTEs

A recursive CTE references itself, built from a starting point (the "anchor," here, employees with no manager) and a repeating step (UNION ALL, joining back to the CTE itself to find the next level down), until no new rows are found. This is the standard way to walk a hierarchy, an org chart, a category tree, a folder structure, in SQL.

PostgreSQL / SQL Server / SQLite
WITH RECURSIVE org_chart AS (
  SELECT id, first_name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  SELECT e.id, e.first_name, e.manager_id, org_chart.level + 1
  FROM employees e
  JOIN org_chart ON e.manager_id = org_chart.id
)
SELECT * FROM org_chart;
MySQL (8.0+)
WITH RECURSIVE org_chart AS (
  SELECT id, first_name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  SELECT e.id, e.first_name, e.manager_id, org_chart.level + 1
  FROM employees e
  JOIN org_chart ON e.manager_id = org_chart.id
)
SELECT * FROM org_chart;
NOTE

All four databases require the RECURSIVE keyword written explicitly, WITH RECURSIVE, not just WITH, even on databases like SQL Server where it might seem optional. MySQL only gained recursive CTE support in version 8.0 (2018), the same release that added window functions.

Frequently Asked Questions

Is a CTE faster than an equivalent subquery?

Usually about the same, most modern query optimizers produce the same execution plan either way. The main benefit of a CTE is readability, not performance, though PostgreSQL historically treated a CTE as an "optimization fence" in some cases (materializing it rather than inlining it), worth checking current documentation if performance is critical.

Can a CTE be used in an INSERT, UPDATE, or DELETE statement?

Yes, on PostgreSQL, SQL Server, and SQLite, a WITH clause can precede any of these. MySQL supports it too, as of version 8.0.

What stops a recursive CTE from looping forever?

The recursive step naturally stops once a join produces no more new rows, in the org chart example, once every employee's manager chain has been fully walked. Most databases also enforce a maximum recursion depth as a safety net against an accidentally infinite recursive CTE.

Common Beginner Mistakes

Writing WITH without RECURSIVE for a self-referencing CTE

WITH org_chart AS (... JOIN org_chart ...) fails, a CTE can't reference itself unless RECURSIVE is explicitly included: WITH RECURSIVE org_chart AS (...).

Using UNION instead of UNION ALL in a recursive CTE's repeating step

UNION ALL is almost always the right choice here, since UNION's deduplication adds overhead on every recursive step and can even change which rows are considered "new" in a way that breaks the recursion's stopping condition.

Assuming a CTE's result is reusable across separate queries

A CTE only exists for the duration of the single statement it's attached to. Running a second, separate query afterward has no access to a CTE defined earlier, unlike a view, which persists until explicitly dropped.

Best Practices

  • Use a CTE to name each logical step of a complex query, rather than nesting subqueries three levels deep.
  • Always write RECURSIVE explicitly for a self-referencing CTE, it's required syntax on every database in this course, not an optional keyword.
  • Default to UNION ALL inside a recursive CTE's repeating step, not UNION, for both performance and correctness.
  • Reach for a view instead of a CTE when the same named query needs to be reused across multiple separate statements.

Interview Questions

Write a recursive CTE that returns an employee and all of their direct and indirect reports.

WITH RECURSIVE reports AS (SELECT id, first_name, manager_id FROM employees WHERE id = 1 UNION ALL SELECT e.id, e.first_name, e.manager_id FROM employees e JOIN reports ON e.manager_id = reports.id) SELECT * FROM reports; — the anchor starts at the specific employee (id = 1), and the recursive step repeatedly finds anyone whose manager_id matches someone already found.

What's the practical difference between a CTE and a view?

A CTE is scoped to a single statement and disappears once that statement finishes, it's never saved in the database. A view is a saved, named query that persists in the database schema and can be reused across any number of separate queries until it's explicitly dropped.

Why is UNION ALL preferred over UNION inside a recursive CTE?

UNION deduplicates the combined result on every recursive iteration, which adds real overhead at each step and, in cases with legitimately repeated rows, can even terminate the recursion earlier than intended. UNION ALL skips deduplication entirely, which is both faster and matches the actual intent in almost every recursive case.

Summary

This lesson covered naming a subquery with WITH for readability, chaining multiple CTEs where each can reference the ones before it, and recursive CTEs for walking a hierarchy, built from an anchor query and a repeating step joined back to itself.

What's Next?

The next lesson covers set operations, UNION, INTERSECT, and EXCEPT, combining the results of two separate queries vertically.

</>

Ready to Practice SQL?

Write and run real SQL queries online, right in your browser, no install or signup required. Pick a playground based on which database you want to practice on.

SQLite / PostgreSQL

Fast, no server, no ads.

Open DB Fiddle ↗

MySQL / SQL Server

Switch databases with one dropdown.

Open SQL Fiddle ↗

Both open a third-party SQL playground in a new tab. External site terms and privacy policy apply.