Learning Platform
Урок 10.02 · 20 мин
Средний
WITH RECURSIVEAnchorRecursive termUNION ALLWorking table

В прошлом уроке мы поняли, зачем нужна рекурсия. Сегодня — как она записывается. Это самый важный урок модуля: каркас, который ты будешь набивать в каждом следующем шаге.

Скелет recursive CTE

Синтаксис выглядит так:

WITH RECURSIVE cte_name(col1, col2, ...) AS (
    -- Anchor: стартовое множество кортежей
    SELECT ...
  UNION ALL
    -- Recursive term: ссылается на cte_name
    SELECT ...
    FROM ... JOIN cte_name ON ...
)
SELECT * FROM cte_name;

Три обязательных компонента:

  • WITH RECURSIVE — ключевое слово. Без RECURSIVE в WITH нельзя ссылаться на CTE из самого определения CTE. Это сигнал планировщику: «здесь будет цикл».
  • Anchor (база рекурсии) — первый SELECT, до UNION. Не использует cte_name внутри себя. Это стартовое множество.
  • Recursive term — второй SELECT, после UNION / UNION ALL. Должен ссылаться на cte_name (хотя бы один раз).

Между ними обязательно UNION или UNION ALL. Это не просто оператор — он формально определяет, как объединяются результаты шагов. О различиях между UNION и UNION ALL в рекурсии — отдельный урок 5.

Структура recursive CTE

Anchor задаёт стартовую точку. Recursive term — правило шага. UNION ALL склеивает результат каждой итерации с накопленным.

WITH RECURSIVE subordinates AS (
SELECT ... anchorБазовый случай — кто стартует цикл. Например, сам сотрудник, от которого считаем подчинённых.
WHERE manager_id IS NULL
UNION ALL
SELECT ...recursive termШаг рекурсии — что добавлять. Обязательно ссылается на subordinates внутри FROM.
FROM employees e
JOIN subordinates s ON e.manager_id = s.id
)
SELECT * FROM subordinates;

Минимальный рабочий пример

Найдём всех подчинённых CEO Анны Громовой (id=1), на любой глубине.

Классический top-down обход дерева сотрудников от корня:

PostgreSQL

Что мы видим в результате: 16 строк, начиная с самой Анны (level=0), её прямые подчинённые (level=1), их подчинённые (level=2), и так далее. Уровень level мы накручиваем сами — это не магия СУБД, это обычная колонка в anchor 0, в recursive term s.level + 1.

Как PostgreSQL это исполняет — пошагово

Внутри планировщика recursive CTE превращается в цикл с тремя «буферами»:

  • Result table — финальный результат, который вернётся пользователю.
  • Working table (рабочая) — кортежи, которые будут поданы в recursive term на следующей итерации.
  • Intermediate table (промежуточная) — куда складывается то, что породил recursive term на текущей итерации.

Алгоритм:

  1. Выполнить anchor. Результат → result table и working table.
  2. Пока working table не пуста:
    • Выполнить recursive term, подставляя working table вместо cte_name.
    • Результат → intermediate table.
    • Скопировать intermediate в result.
    • Переписать working table содержимым intermediate (очистить intermediate).
  3. Вернуть result table.
Шаги исполнения recursive CTE

На каждой итерации работаем только с новыми кортежами из предыдущей итерации. Это критично для понимания семантики.

Шаг 0anchor → {Анна}
working{Анна}
result{Анна}
Шаг 1recursive term от {Анна}Применяем правило только к тому, что добавилось на предыдущем шаге, а не ко всему результату.
working{Борис, Виктор}
result{Анна, Борис, Виктор}
Шаг 2recursive term от {Борис, Виктор}
working{Галина, Дмитрий}
result+ Галина, Дмитрий
Шаг 3recursive term от {Галина, Дмитрий}
working{Елена, Феликс, Гриша}
result+ Елена, Феликс, Гриша
...
workingпусто
resultфикс-точка

Ключевое следствие: recursive term на каждом шаге работает не со всем result, а только с новыми кортежами предыдущего шага. Это эквивалентно семантике «фронт волны» в BFS-обходе.

Если в recursive term написать SELECT ... FROM cte_name, ты получаешь именно working table. Кажется, что ссылаешься на «весь CTE», но на самом деле — только на свежий слой. Это не баг — это спецификация SQL:1999, и она интуитивно понятна, как только привык.

Терминатор: зачем уровни и фильтры

В нашем примере мы не писали никакого WHERE level < 100 — рекурсия остановилась сама. Почему?

Потому что данные конечны. После того как мы дошли до листьев дерева (сотрудников без подчинённых), recursive term ничего не возвращает: нет такого e, чтобы e.manager_id был в working table — working table содержит только листья, на которых ничего не висит. Working table становится пустой, цикл прекращается.

В дереве это работает автоматически. Но в графе с циклами (A→B→A) — нет: working table будет вечно подкидывать что-то новое. Об этом — урок 4. Сейчас запомни: в дереве терминатор не нужен, в графе — нужен.

Один практический совет: даже в дереве часто полезно ограничить глубину. Это страховка от испорченных данных. Допустим, кто-то по ошибке поставил Анне manager_id = 6 — теперь у нас цикл. Без терминатора рекурсия будет крутиться, пока не упрётся в work_mem или таймаут.

Та же рекурсия с явным ограничением глубины — страховка от runaway:

PostgreSQL

Один прыжок: посчитаем суммарную зарплату ветки

Чтобы убедиться, что результат CTE — это обычное отношение (его можно агрегировать, джойнить, оборачивать в подзапрос), посчитаем суммарную зарплату всех подчинённых VP Engineering Галины Сидоровой (id=4).

Recursive CTE + агрегат: посчитать payroll ветки:

PostgreSQL

После того как CTE собрал всех «потомков» Галины (включая её саму), мы просто агрегируем — как с любой другой таблицей. Для остальной части запроса CTE — это коробка с готовыми кортежами; не важно, что внутри был цикл.

Правила, которые нельзя нарушать

PostgreSQL накладывает несколько ограничений на recursive term. Если их нарушить — будет ошибка:

  1. Между anchor и recursive term — только UNION или UNION ALL. INTERSECT, EXCEPT запрещены.
  2. Recursive term не может содержать:
    • LEFT/RIGHT/FULL OUTER JOIN с cte_name (только INNER JOIN или ссылка в FROM).
    • Агрегатные функции от cte_name.
    • DISTINCT (на уровне recursive term).
    • Подзапросы, в которых cte_name появляется в WHERE NOT IN () или коррелированно.
  3. Anchor не должен ссылаться на сам CTE.
  4. Recursive term должен ссылаться на CTE (иначе зачем RECURSIVE).

Эти ограничения связаны с тем, что recursive term должен быть монотонным — каждый шаг только добавляет кортежи, не отбирает уже добавленные. Только при этом условии фикс-точка достижима.

Проверка знанийKnowledge check
В recursive term ты написал SELECT ... FROM employees e JOIN my_cte m ON e.manager_id = m.id. На шаге 3 ты ожидаешь, что my_cte уже содержит результат шагов 0, 1, 2. На самом деле, что туда подставится?
ОтветAnswer
На шаге 3 в my_cte подставится working table — только те кортежи, которые были добавлены на шаге 2, а не весь накопленный результат шагов 0–2. Это семантика SQL-стандарта: recursive term применяется к «фронту волны», а не ко всему собранному. Уже найденные кортежи лежат в result, но recursive term их не видит — это бы порождало повторы и не сходилось бы. Если тебе нужно ссылаться на весь накопленный результат, придётся переложить логику: либо тащить нужное в working table, либо после рекурсии делать ещё один проход.

Что дальше

В следующем уроке применим этот каркас к двум главным задачам: иерархии сотрудников и категориям товаров. Разберём top-down vs bottom-up обход, как считать глубину и как красиво выводить «отступы» уровней.

Recursive CTE в Trino — аналог и ограничения

Чек-лист урока

  • WITH RECURSIVE name AS (anchor UNION ALL recursive_term) SELECT ...
  • Anchor — старт без ссылок на сам CTE. Recursive term — обязательно ссылается на CTE.
  • Между ними — UNION (с дедупликацией) или UNION ALL (без). Урок 5 — про различия.
  • PostgreSQL исполняет это итерациями: на каждом шаге recursive term работает с working table (только новыми кортежами с прошлого шага), не со всем накопленным результатом.
  • В дереве рекурсия останавливается сама (working table пустеет). В графе — нужен явный терминатор.
  • Несколько синтаксических ограничений на recursive term: только INNER JOIN, без агрегатов от CTE, без DISTINCT.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какие два обязательных компонента есть в любом рекурсивном CTE?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6