JavaRush /Курсы /SQL SELF /Рекурсивные CTE: что это такое и зачем они нужны

Рекурсивные CTE: что это такое и зачем они нужны

SQL SELF
27 уровень , 3 лекция
Открыта

Сегодня мы сделаем еще один шаг вперёд и займёмся магией рекурсии. Если вы уже когда-либо программировали на языке с поддержкой рекурсии (например, тот же Python), то вы примерно понимаете, о чём речь. Но не волнуйтесь, если это звучит как что-то загадочное — мы разберём всё очень подробно.

Рекурсивные CTE — это мощный инструмент для работы с иерархическими, древовидными структурами данных, такими как организационные структуры компаний, семейные древо или каталоги файлов.

Простыми словами, это такие выражения, которые могут "вызвать сами себя", чтобы постепенно обойти и обработать все уровни данных.

Ключевые особенности рекурсивных CTE:

  1. Они используют ключевое слово WITH RECURSIVE.
  2. Рекурсивные CTE состоят из двух частей:
    • Базовый запрос: определяет начальную точку (или "корень") рекурсии.
    • Рекурсивный запрос: обрабатывает оставшиеся данные, используя результат предыдущего шага.

Алгоритм работы рекурсивного CTE похож на то, как вы поднимаетесь по лестнице:

  • Сначала вы становитесь на первую ступеньку (это базовый запрос).
  • Затем вы поднимаетесь на вторую ступеньку, используя результат первой ступеньки (рекурсивный запрос).
  • Этот процесс повторяется, пока ступеньки не закончатся (достижение условия завершения).

Синтаксис рекурсивного CTE

Давайте сразу посмотрим на шаблонный пример:

WITH RECURSIVE cte_name AS (
    -- Базовый запрос
    SELECT column1, column2
    FROM table_name
    WHERE condition_for_base_case

    UNION ALL

    -- Рекурсивный запрос
    SELECT column1, column2
    FROM table_name
    JOIN cte_name ON some_condition
    WHERE stop_condition
)
SELECT * FROM cte_name;

Роль UNION и UNION ALL в рекурсивных CTE

Каждый рекурсивный CTE обязан использовать операторы UNION или UNION ALL между базовой и рекурсивной частью.

Оператор Что делает
UNION Склеивает результат двух запросов и удаляет дубликаты строк
UNION ALL Склеивает и оставляет все строки, включая повторы

Какой оператор выбрать: UNION или UNION ALL?

Если вы не уверены, что использовать — почти всегда выбирайте UNION ALL. Почему? Потому что он работает быстрее: он просто объединяет результаты, не проверяя, есть ли там дубликаты. Это значит — меньше вычислений, меньше ресурсов и быстрее результат.

Особенно это важно в рекурсивных CTE. Когда вы строите иерархии — например, дерево комментариев или структуру подчинённых в компании — UNION ALL нужен почти всегда. Если использовать просто UNION, база данных может случайно посчитать, что какие-то шаги уже были и «отрезать» часть результата. А это сломает всю логику обхода.

Использовать UNION можно, только если вы точно знаете, что дубликаты вредны и их надо убрать. Но помните: это всегда компромисс между чистотой и скоростью.

Пример разных подходов

-- UNION: дубликаты исключаются
SELECT 'A'
UNION
SELECT 'A';     -- Результат: одна строка 'A'

-- UNION ALL: дубликаты сохраняются
SELECT 'A'
UNION ALL
SELECT 'A';     -- Результат: две строки 'A'

В рекурсивных запросах безопаснее всегда использовать UNION ALL, чтобы не потерять важные шаги при обходе структуры.

Рассмотрим типичную задачу: у нас есть таблица сотрудников с колонками employee_id, manager_id и name. Нужно построить иерархию, начиная с директора — человека без начальника (у которого manager_id = NULL).

Допустим у нас есть таблица сотрудников: employees

employee_id name manager_id
1 Eva Lang NULL
2 Alex Lin 1
3 Maria Chi 1
4 Otto Mart 2
5 Anna Song 2
6 Eva Lang 3

Нам нужно понять, кто кому подчиняется, и узнать уровень каждого сотрудника в структуре. Это удобно, когда вы хотите, например, отобразить дерево сотрудников в интерфейсе или подготовить отчёт о командной структуре.

WITH RECURSIVE employee_hierarchy AS (
    -- Начинаем с тех, у кого нет начальника
    SELECT
        employee_id,
        name,
        manager_id,
        1 AS level
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- Добавляем подчинённых и увеличиваем уровень
    SELECT
        e.employee_id,
        e.name,
        e.manager_id,
        eh.level + 1
    FROM employees e
    INNER JOIN employee_hierarchy eh
    ON e.manager_id = eh.employee_id
)
SELECT * FROM employee_hierarchy;

Результат будет таким:

employee_id name manager_id level
1 Eva Lang NULL 1
2 Alex Lin 1 2
3 Maria Chi 1 2
4 Otto Mart 2 3
5 Anna Song 2 3
6 Eva Lang 3 3

Этот запрос наглядно показывает, как можно "пройтись" по иерархии сотрудников — от директора до самых младших в структуре. Уровень level удобно использовать для форматирования или визуализации дерева.

Пример: категории товаров

Теперь представьте, что мы работаем с таблицей категорий товаров, где каждая категория может иметь подкатегории, а те, в свою очередь, свои подкатегории. Как нам построить дерево категорий?

Таблица categories

category_id name parent_id
1 Электроника NULL
2 Компьютеры 1
3 Смартфоны 1
4 Ноутбуки 2
5 Периферия 2

Рекурсивный запрос:

WITH RECURSIVE category_tree AS (
    -- Базовый случай: найти корневые категории
    SELECT
        category_id,
        name,
        parent_id,
        1 AS depth
    FROM categories
    WHERE parent_id IS NULL

    UNION ALL

    -- Рекурсивная часть: найти подкатегории текущих категорий
    SELECT
        c.category_id,
        c.name,
        c.parent_id,
        ct.depth + 1
    FROM categories c
    INNER JOIN category_tree ct
    ON c.parent_id = ct.category_id
)
SELECT * FROM category_tree;

Результат:

category_id name parent_id depth
1 Электроника NULL 1
2 Компьютеры 1 2
3 Смартфоны 1 2
4 Ноутбуки 2 3
5 Периферия 2 3

Теперь мы видим дерево категорий с уровнями вложенности.

Почему рекурсивные CTE — это круто?

Рекурсивные CTE — один из самых выразительных и мощных инструментов SQL. Вместо сложной вложенной логики вы просто описываете, с чего начать (базовый случай), и как двигаться дальше (рекурсивную часть) — всё остальное делает PostgreSQL.

Чаще всего такие запросы используют для обхода иерархий: сотрудников, категорий товаров, директорий на диске, графов в соцсетях. Они легко расширяются: если в таблицу добавятся новые данные, запрос подхватит их сам. Это удобно и масштабируемо.

Но есть и подводные камни. Обязательно следите за условиями завершения — без них запрос может уйти в бесконечный цикл. Не забудьте про индексы: в больших таблицах рекурсивные запросы без них могут тормозить. А UNION ALL — почти всегда лучший выбор, особенно в иерархических задачах, иначе вы рискуете потерять шаги рекурсии из-за удаления дубликатов.

Хорошо настроенный рекурсивный CTE позволяет выразить сложную бизнес-логику буквально в нескольких строках — без процедур, циклов и дополнительного кода. Это тот случай, когда SQL работает не только правильно, но и красиво.

Типичные ошибки при работе с рекурсивными CTE

  • Бесконечная рекурсия: если вы не зададите корректное условие завершения (WHERE), запрос может зациклиться.
  • Избыточные данные: неправильное использование UNION ALL добавляет дубликаты.
  • Производительность: рекурсивные запросы могут быть тяжёлыми для большого объема данных. Индексы на ключевых колонках (например, manager_id) помогут ускорить выполнение.

Когда без рекурсивных запросов не обойтись

Иногда кажется, что рекурсивные запросы — это что-то из теории, но на деле они часто встречаются в повседневной разработке. Например:

  • чтобы построить отчёты по структуре компании или классификации товаров;
  • чтобы обойти дерево папок и собрать список всех вложенных директорий;
  • чтобы анализировать графы — социальные связи, маршруты, зависимости между задачами;
  • чтобы просто представить сложные связи между объектами в читаемом виде.

Если нужно пройтись по структуре, в которой одно зависит от другого, — почти наверняка пригодится WITH RECURSIVE.

2
Задача
SQL SELF, 27 уровень, 3 лекция
Недоступна
Построение иерархии сотрудников
Построение иерархии сотрудников
Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ra Уровень 35 Student
5 августа 2025
Из доков: Recursive Query Evaluation Evaluate the non-recursive term. For UNION (but not UNION ALL), discard duplicate rows. Include all remaining rows in the result of the recursive query, and also place them in a temporary working table. So long as the working table is not empty, repeat these steps: Evaluate the recursive term, substituting the current contents of the working table for the recursive self-reference. For UNION (but not UNION ALL), discard duplicate rows and rows that duplicate any previous result row. Include all remaining rows in the result of the recursive query, and also place them in a temporary intermediate table. Replace the contents of the working table with the contents of the intermediate table, then empty the intermediate table.
Ra Уровень 35 Student
5 августа 2025
Как работает рекурсивный CTE: простыми словами Шаг 1. Старт Запрос начинается с нерекурсивной части (базовый случай). В нашем примере это поиск сотрудника с ID=1:

employee_hierarchy
employee_id 	name	  manager_id	level
1	            Eva Lang  NULL	        1
Шаг 2. Первая рекурсия (дальше работает уже только рекурсивная часть запроса) Теперь запрос ищет всех, у кого менеджер — это сотрудник из текущих результатов (т.е. manager_id=1): Создаётся временная табица и в неё добавляются строки:

employee_id 	name	  manager_id	level
 2           Alex Lin    1            2 
 3           Maria Chi   1            2 
и строка, которая была в рабочей таблице (с Евой). Далее содержимое рабочей таблицы полностью заменяется содержимым временной таблицы , и временная частица полностью очищается:

employee_hierarchy
employee_id 	name	  manager_id	level
1            Eva Lang       NULL          1 
2            Alex Lin        1            2 
3            Maria Chi       1            2 
Шаг 3. Следующая рекурсия Запрос снова проверяет ВСЕХ текущих сотрудников в CTE (Eva, Alex, Maria) и ищет их подчинённых, и так далее. Главные правила: Не "дополнение", а пересборка На каждом шаге CTE полностью пересчитывается. JOIN проверяет ВСЕ строки Условия применяются ко всем существующим записям в CTE, а не только к последним добавленным. Остановка рекурсии Процесс завершается, когда: Нет новых подчинённых (JOIN не даёт результатов) Срабатывает ограничение (например, WHERE level < 3) Аналогия с поиском в ширину (BFS) Представьте, что CTE — это очередь: Сначала добавляем начальника (Eva). Для каждого человека в очереди ищем подчинённых и добавляем их в конец. Повторяем, пока очередь не опустеет.
Slevin Уровень 7
19 сентября 2025
Немного дополню. Как я понял, общаясь с ChatGPT и угрожая сжечь ее ДатаЦентр: Происходит не пересборка - а склейка таблицы из базового случая с временной из рекурсионного шага - именно это и делают UNION и UNION ALL: Которые можно использовать и в обычных запросах, без всяких рекурсий и СТЕ - они просто склеивают строки, причем UNION склеивает удаляя дубликаты строк, а UNION ALL - просто склеивает без проверки на дубликаты:

-- Таблица 1
-- id | name
-- 1  | Alice
-- 2  | Bob

-- Таблица 2
-- id | name
-- 2  | Bob
-- 3  | Carol

-- UNION (дубликаты убираются)
SELECT id, name FROM table1
UNION
SELECT id, name FROM table2;

-- Результат:
-- 1 | Alice
-- 2 | Bob
-- 3 | Carol

-- UNION ALL (дубликаты сохраняются)
SELECT id, name FROM table1
UNION ALL
SELECT id, name FROM table2;

-- Результат:
-- 1 | Alice
-- 2 | Bob
-- 2 | Bob
-- 3 | Carol
А вот для следующего рекурсионного шага используется именно Временная таблица (результат прошлого шага), а не общая объединённая. И это у вас так и написано.