SQL · Lesson 11 of 17

CTEs and Recursive Queries

Name your intermediate steps with WITH, then walk a hierarchy with RECURSIVE.

  • Advanced
  • 15 min read
  • 3 objectives

Before this lessonLesson 10: Numbers, Rounding and Ratios

What you will learn

  • Name steps with WITH
  • Chain several CTEs
  • Walk a hierarchy recursively

Your Progress

0 of 17 lessons 0%

  • Lessons0 / 17
  • Completed0
  • Est. time left~ 4 hours

Create a free account to keep your progress on every device.

Tip: pressing Next marks this lesson complete automatically.

A common table expression is a named subquery written before the query that uses it. Nothing it does is impossible with nested subqueries — it just reads top to bottom instead of inside out.

The same query, twice

SELECT name, spent FROM (
  SELECT c.name AS name, SUM(o.total) AS spent
  FROM customers c JOIN orders o ON o.customer_id = c.id
  GROUP BY c.id, c.name
) WHERE spent > 100;
Output
name | spent
-----+------
Ada  | 155.5
WITH totals AS (
  SELECT c.name AS name, SUM(o.total) AS spent
  FROM customers c
  JOIN orders o ON o.customer_id = c.id
  GROUP BY c.id, c.name
)
SELECT name, spent
FROM totals
WHERE spent > 100;
Output
name | spent
-----+------
Ada  | 155.5

Same result, same plan. The second version names the intermediate step, which is the whole point.

Chaining CTEs

One WITH can define several CTEs separated by commas, and each may refer to the ones before it. This is how a four-step calculation stays readable.

WITH per_customer AS (
  SELECT customer_id, SUM(total) AS spent
  FROM orders
  GROUP BY customer_id
),
ranked AS (
  SELECT customer_id, spent,
         (SELECT COUNT(*) FROM per_customer p2 WHERE p2.spent > p1.spent) + 1 AS position
  FROM per_customer p1
)
SELECT c.name, r.spent, r.position
FROM ranked r
JOIN customers c ON c.id = r.customer_id
ORDER BY r.position;
Output
name  | spent | position
------+------+---------
Ada   | 155.5 | 1
Grace | 80    | 2

Referencing a CTE more than once

A subquery you need twice has to be written twice. A CTE is named, so you can join it to itself — handy for comparing each row against the group.

WITH dept AS (
  SELECT department, AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department
)
SELECT e.name, e.department, e.salary, ROUND(d.avg_salary, 0) AS dept_avg,
       e.salary - d.avg_salary AS diff
FROM employees e
JOIN dept d ON d.department = e.department
ORDER BY e.department, diff DESC;
Output
name     | department  | salary | dept_avg | diff
---------+------------+-------+---------+-------------------
Margaret | Design      | 140000 | 120000   | 20000
Alan     | Design      | 110000 | 120000   | -10000
Barbara  | Design      | 110000 | 120000   | -10000
Grace    | Engineering | 190000 | 153333   | 36666.66666666666
Ada      | Engineering | 150000 | 153333   | -3333.333333333343
Linus    | Engineering | 120000 | 153333   | -33333.33333333334

Recursive CTEs

A recursive CTE has two halves joined by UNION ALL: an anchor that produces the starting rows, and a recursive step that refers back to the CTE itself. It repeats until the step returns nothing.

WITH RECURSIVE counter(n) AS (
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM counter WHERE n < 5
)
SELECT n FROM counter;
Output
n
--
1
2
3
4
5

Walking an org chart

The same shape answers the real question: how deep is each person in the reporting tree, and who is above them? Start from the people with no manager, then join children to parents.

WITH RECURSIVE chart(id, name, manager_id, depth, path) AS (
  SELECT id, name, manager_id, 0, name
  FROM employees
  WHERE manager_id IS NULL
  UNION ALL
  SELECT e.id, e.name, e.manager_id, c.depth + 1, c.path || ' > ' || e.name
  FROM employees e
  JOIN chart c ON e.manager_id = c.id
)
SELECT name, depth, path FROM chart ORDER BY path;
Output
name     | depth | path
---------+------+-------------------
Grace    | 0     | Grace
Ada      | 1     | Grace > Ada
Linus    | 1     | Grace > Linus
Margaret | 0     | Margaret
Alan     | 1     | Margaret > Alan
Barbara  | 1     | Margaret > Barbara

Key takeaways

  • WITH name AS (...) names a step so the final query reads top to bottom.
  • Separate several CTEs with commas; later ones can use earlier ones.
  • A CTE can be referenced twice; a subquery must be repeated.
  • Recursive CTEs = anchor + UNION ALL + self-reference, and need a stop condition.
-- Write your solution here

Finished reading? Mark this lesson complete to track your progress.

Up next · Lesson 12Window FunctionsRank, compare to the previous row, and run totals without collapsing the result.