Skip to content
Proposal for a new site by Genesis Innovation, built from his public LinkedIn profile. Not his official site.

Wiki SQL

CTEs (WITH) and recursion

Naming a step of the query makes the SQL readable from top to bottom. With RECURSIVE, it generates sequences and walks hierarchies.

A CTE (Common Table Expression) is a named subquery, declared before the main SELECT. It does not change the result; it changes who can read the query. Each step becomes a named rung.

SQLOutput checked on SQLite
WITH delivered AS (
  SELECT * FROM orders WHERE status = 'delivered'
),
per_customer AS (
  SELECT customer_id, SUM(amount) AS total FROM delivered GROUP BY customer_id
)
SELECT c.name, p.total
FROM per_customer p
JOIN customers c ON c.id = p.customer_id
ORDER BY p.total DESC, c.name;

Output (checked)

name  | total
----- | -----
Ana   | 1130
Bruno | 1050
Diego | 1050
Carla | 180
Elisa | 30

WITH RECURSIVE: a calendar

The recursive part runs until it produces no new row. It is the classic way to generate the date table that "fills the gaps" in a report: here, days without sales show up with zero.

SQLOutput checked on SQLite
WITH RECURSIVE days(day) AS (
  SELECT '2024-06-01'
  UNION ALL
  SELECT date(day, '+1 day') FROM days WHERE day < '2024-06-07'
)
SELECT d.day, COALESCE(SUM(o.amount), 0) AS sold
FROM days d
LEFT JOIN orders o ON o.order_date = d.day AND o.status = 'delivered'
GROUP BY d.day
ORDER BY d.day;

Output (checked)

day        | sold
---------- | ----
2024-06-01 | 1130
2024-06-02 | 180
2024-06-03 | 1050
2024-06-04 | 180
2024-06-05 | 980
2024-06-06 | 0
2024-06-07 | 0

Without the stopping condition (WHERE day < …) the recursion never ends. On the site, the query is interrupted after a few seconds; on a production database, it holds resources until someone kills it.

Hierarchies

SQLOutput checked on SQLite
WITH RECURSIVE team(id, name, manager, level) AS (
  SELECT 1, 'Management', NULL, 0
  UNION ALL
  SELECT t.id, t.name, t.manager, e.level + 1
  FROM (SELECT 2 AS id, 'Data' AS name, 1 AS manager UNION ALL SELECT 3, 'Engineering', 2 UNION ALL SELECT 4, 'BI', 2) t
  JOIN team e ON t.manager = e.id
)
SELECT substr('        ', 1, level * 2) || name AS tree, level FROM team ORDER BY id;

Output (checked)

tree            | level
--------------- | -----
Management      | 0
  Data          | 1
    Engineering | 2
    BI          | 2
  • In Spark SQL, a CTE is not materialized by default: if it is referenced twice, it may be computed twice. To reuse an expensive result, write a table or use cache (see Cache and persist).
  • WITH RECURSIVE only reached Spark SQL and Databricks in recent versions: check the documentation for your version before counting on it. Without it, a hierarchy is solved with a loop of JOINs in PySpark.

Sources

Shared knowledge from Genesis Innovation. Not a claim about the person behind this site.