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.
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.
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.
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;
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;
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.
Both open a third-party SQL playground in a new tab. External site terms and privacy policy apply.