JavaRush /Курсы /SQL SELF /Пример рекурсивных CTE для работы с иерархиями

Пример рекурсивных CTE для работы с иерархиями

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

Представьте: у вас есть интернет-магазин с тысячами товаров, аккуратно разложенных по полочкам — категории, подкатегории, под-подкатегории. На сайте это выглядит как красивое выпадающее меню, но в базе данных превращается в головную боль. Как извлечь всю ветку "Электроника → Смартфоны → Аксессуары" одним запросом? Как посчитать, сколько уровней вложенности у каждой категории? Обычные JOIN-ы тут бессильны — нужна рекурсия!

Построение структуры категорий товаров с помощью рекурсивных CTE

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

Электроника
  └── Смартфоны
      └── Аксессуары
  └── Ноутбуки
      └── Геймерские
  └── Фото и видео

Эта структура легко представляется в интерфейсах интернет-магазинов, но как её хранить в базе данных и извлекать? Здесь на помощь приходят рекурсивные CTE!

Исходная таблица категорий

Вначале создадим таблицу categories, которая будет хранить данные о категориях товаров:

CREATE TABLE categories (
    category_id SERIAL PRIMARY KEY,       -- Уникальный идентификатор категории
    category_name TEXT NOT NULL,          -- Название категории
    parent_category_id INT                -- Родительская категория (NULL для главных категорий)
);

Вот пример данных, которые мы добавим в таблицу:

INSERT INTO categories (category_name, parent_category_id) VALUES
    ('Электроника', NULL),
    ('Смартфоны', 1),
    ('Аксессуары', 2),
    ('Ноутбуки', 1),
    ('Геймерские', 4),
    ('Фото и видео', 1);

Что здесь происходит:

  • Электроника — это главная категория (нет родителя, parent_category_id = NULL).
  • Смартфоны находятся внутри категории Электроника.
  • Аксессуары относятся к категории Смартфоны.
  • Аналогично остальные категории.

Текущая структура данных в таблице categories выглядит следующим образом:

category_id category_name parent_category_id
1 Электроника NULL
2 Смартфоны 1
3 Аксессуары 2
4 Ноутбуки 1
5 Геймерские 4
6 Фото и видео 1

Построение дерева категорий с помощью рекурсивного CTE

Теперь мы хотим получить всю иерархию категорий с указанием уровня вложенности. Для этого используем рекурсивный CTE.

WITH RECURSIVE category_tree AS (
    -- Базовый запрос: выбираем все корневые категории (parent_category_id = NULL)
    SELECT
        category_id,
        category_name,
        parent_category_id,
        1 AS depth -- Первый уровень вложенности
    FROM categories
    WHERE parent_category_id IS NULL

    UNION ALL

    -- Рекурсивный запрос: находим подкатегории для каждой категории
    SELECT
        c.category_id,
        c.category_name,
        c.parent_category_id,
        ct.depth + 1 AS depth -- Увеличиваем уровень вложенности
    FROM categories c
    INNER JOIN category_tree ct
    ON c.parent_category_id = ct.category_id
)
-- Итоговый запрос: извлекаем результаты из CTE
SELECT
    category_id,
    category_name,
    parent_category_id,
    depth
FROM category_tree
ORDER BY depth, parent_category_id, category_id;

Результат:

category_id category_name parentcategoryid depth
1 Электроника NULL 1
2 Смартфоны 1 2
4 Ноутбуки 1 2
6 Фото и видео 1 2
3 Аксессуары 2 3
5 Геймерские 4 3

Что здесь происходит?

  1. Сначала базовый запрос (SELECT … FROM categories WHERE parent_category_id IS NULL) выбирает главные категории. В данном случае это только Электроника с depth = 1.
  2. Затем рекурсивный запрос с помощью INNER JOIN добавляет подкатегории, увеличивая уровень вложенности (depth + 1).
  3. Этот процесс повторяется до тех пор, пока не найдутся все подкатегории для всех уровней.

Полезные доработки

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

  1. Добавление полного пути категории

Иногда полезно отображать полный путь к категории, например: Электроника > Смартфоны > Аксессуары. Это можно реализовать, используя строковую агрегацию:

WITH RECURSIVE category_tree AS (
    SELECT
        category_id,
        category_name,
        parent_category_id,
        category_name AS full_path,
        1 AS depth
    FROM categories
    WHERE parent_category_id IS NULL

    UNION ALL

    SELECT
        c.category_id,
        c.category_name,
        c.parent_category_id,
        ct.full_path || ' > ' || c.category_name AS full_path, -- склеиваем строки
        ct.depth + 1
    FROM categories c
    INNER JOIN category_tree ct
    ON c.parent_category_id = ct.category_id
)

SELECT
    category_id,
    category_name,
    parent_category_id,
    full_path,
    depth
FROM category_tree
ORDER BY depth, parent_category_id, category_id;

Результат:

category_id category_name parentcategoryid full_path depth
1 Электроника NULL Электроника 1
2 Смартфоны 1 Электроника > Смартфоны 2
4 Ноутбуки 1 Электроника > Ноутбуки 2
6 Фото и видео 1 Электроника > Фото и видео 2
3 Аксессуары 2 Электроника > Смартфоны > Аксессуары 3
5 Геймерские 4 Электроника > Ноутбуки > Геймерские 3

Теперь у каждой категории есть полный путь, отображающий вложенность.

  1. Подсчёт количества подкатегорий

Что если мы хотим узнать, сколько подкатегорий есть у каждой категории?

WITH RECURSIVE category_tree AS (
    SELECT
        category_id,
        parent_category_id
    FROM categories

    UNION ALL

    SELECT
        c.category_id,
        c.parent_category_id
    FROM categories c
    INNER JOIN category_tree ct
    ON c.parent_category_id = ct.category_id
)

SELECT
    parent_category_id,
    COUNT(*) AS subcategory_count
FROM category_tree
WHERE parent_category_id IS NOT NULL
GROUP BY parent_category_id
ORDER BY parent_category_id;

Результат:

parentcategoryid subcategory_count
1 3
2 1
4 1

Таблица показывает, что у Электроники 3 подкатегории (Смартфоны, Ноутбуки, Фото и видео), а у Смартфонов и Ноутбуков по одной.

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

Бесконечная рекурсия: Если данные содержат циклы (например, категория ссылается сама на себя), запрос может уйти в бесконечный цикл. Чтобы этого избежать, можно ограничить глубину рекурсии с помощью WHERE depth < N или лимитов.

Оптимизация работы: Рекурсивные CTE могут быть медленными на больших объёмах данных. Применяйте индексы на parent_category_id для ускорения.

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

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

2
Задача
SQL SELF, 27 уровень, 4 лекция
Недоступна
Построение полного пути для каждой категории
Построение полного пути для каждой категории
1
Опрос
Введение в CTE, 27 уровень, 4 лекция
Недоступен
Введение в CTE
Введение в CTE
Комментарии (10)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Анатолий Уровень 50
13 февраля 2026
❤️
Slevin Уровень 57
19 сентября 2025
6 раз этот шарашкин валидатор не принимал мой ответ, потому что разделитель ' > ' (который кстати написан и в решении) видите ли ему не подходил, то просил убрать пробелы, то добавить, в итоге принял только на этапе ' $gt; '. Это клиника дурдома, JavaRush. --- Тест: В каком стандарте SQL были впервые представлены CTE? Правда? Это ваш вопрос? Нахер он тут нужен? --- Лекция хорошая, валидатор говно, тест - содержит вопросы с размытой формулировкой и неочевидными ответами.
Юрий Уровень 60
10 ноября 2025
Эта игра в угадай в какой символ они имели в виду.

Строка заменить ' > ' на ' > ' в очередной заз показывает качество курса. 
13 !!!! карл попыток. Рекомендую сдавать его через сайт - дает правильную подсказку
Vlad Tagunkov Уровень 13
13 января 2026

' & gt ; '
- только сайт дал эту подсказку(убрать пробеллы оставив первый и последний). Вебсторм гонял с пробелами.
Евгений Уровень 49 Expert
27 августа 2025
Вот тут пишут, что CTE был введён в 1999: https://en.wikipedia.org/wiki/SQL%3A1999.
Евгений Уровень 49 Expert
27 августа 2025
Что касается примера по формированию хлебных крошек, то к реальной жизни он имеет мало отношения. Во-первых не надо формировать строку на бэкенде вообще, а уж тем более в базе данных. Это работа фронтенда, мы ему должны отдать отсортированный список категорий и подкатегорий, т.е. просто:

["Электроника", "Смартфоны", "Аксессуары"]
Тем более, кроме названия категории мы можем передавать ещё какую-то информацию, например ссылку на страницу для каждой из категорий в списке. Поэтому в реальной жизни не надо собирать хлебные крошки прямо в базе данных. Во-вторых в примере неправильно выбран стартовый запрос. Если мы хотим построить хлебные крошки для страницы "Аксессуары", то в начальной части мы должны начинать именно с неё, а потом подниматься выше. А в примере мы начинаем с самой общей категории и зачем-то вытаскиваем всё подряд.
Евгений Уровень 49 Expert
27 августа 2025
Пример подсчёта подкатегорий неправильный, я не понимаю, что он считает. Если мы хотим посчитать количество всех подкатегорий (в том числе вложенных) для каждой категории, то вот правильный запрос:

WITH RECURSIVE category_tree AS (SELECT category_id, category_id AS initial_category_id
                                 FROM categories

                                 UNION ALL

                                 SELECT c.category_id, ct.initial_category_id
                                 FROM categories c
                                          INNER JOIN category_tree ct ON c.parent_category_id = ct.category_id)
SELECT COALESCE(initial_category_id, 0),
       COUNT(*) - 1 AS subcategory_count
FROM category_tree
GROUP BY COALESCE(initial_category_id, 0)
ORDER BY subcategory_count DESC;
А если мы хотим посчитать подкатегории каждой категории, углубляясь лишь на один уровень вложенности, то можно конечно сделать рекурсивный запрос (но мне было лень), только зачем, если можно вот так:

SELECT c.parent_category_id AS category_id, COUNT(*) as subcategory_count
FROM categories AS c
WHERE c.parent_category_id IS NOT NULL
GROUP BY c.parent_category_id
ORDER BY subcategory_count DESC, category_id;
Slevin Уровень 57
19 сентября 2025
Справедливо, они опять не указали, что суть лишь про один уровень вложенности!
Ra Уровень 35 Student
4 августа 2025
Задачу лучше проверять на https://www.db-fiddle.com/ Здесь моё решение проходит, а там нет
Ra Уровень 35 Student
4 августа 2025
Полезная штука. Интересно что в реальных сайтах используется. Вроде есть materialized path