В чем разница между рекурсивным и нерекурсивным CTE (Common Table Expression)?

Ответ

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

Нерекурсивное CTE

Используется как временная таблица в рамках одного запроса. Улучшает читаемость.

Пример: выбрать активных пользователей и затем отфильтровать их по имени.

WITH ActiveUsers AS (
    SELECT id, name, email
    FROM users
    WHERE status = 'active'
)
SELECT name, email
FROM ActiveUsers
WHERE name LIKE 'A%';

Рекурсивное CTE

Имеет обязательную структуру из двух частей, объединенных UNION ALL:

  1. Базовый случай (Anchor Member): Начальный запрос, который выполняется один раз.
  2. Рекурсивная часть (Recursive Member): Запрос, который ссылается на само CTE и выполняется до тех пор, пока не вернет пустой результат.

Пример: найти иерархию категорий товаров.

WITH RECURSIVE CategoryTree AS (
    -- 1. Базовый случай: выбираем корневую категорию
    SELECT id, name, parent_id, 0 AS level
    FROM categories
    WHERE parent_id IS NULL

    UNION ALL

    -- 2. Рекурсивная часть: присоединяем дочерние категории
    SELECT c.id, c.name, c.parent_id, ct.level + 1
    FROM categories c
    JOIN CategoryTree ct ON c.parent_id = ct.id
)
SELECT *
FROM CategoryTree;

В Go для выполнения таких запросов используется стандартный пакет database/sql, а результат обрабатывается как обычный SELECT.

Ответ 18+ 🔞

Ну и вот, сидишь ты такой, разбираешься с этими вашими CTE, а там, оказывается, два зверя живут. Один — простой, как валенок, а второй — сам на себя ссылается, как ёбанный нарцисс в зеркале. Сейчас разберём, в чём подвох.

CTE обыкновенный, нерекурсивный

Это просто придуманное на ходу имя для какой-то выборки, чтобы в основном запросе не городить огород. Чисто для красоты и читаемости, чтобы глаза не ебало. Взял кусок запроса, обозвал его и юзаешь дальше.

Смотри, как просто: сначала выцепил всех активных юзеров, а потом из них тех, кто на «А» начинается.

WITH ActiveUsers AS (
    SELECT id, name, email
    FROM users
    WHERE status = 'active'
)
SELECT name, email
FROM ActiveUsers
WHERE name LIKE 'A%';

Всё, пиздец. Никакой магии. Сделал временную табличку в голове у базы и пошёл дальше.

А теперь держись за стул: CTE рекурсивный

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

  1. Якорь (Anchor Member): Это как стартовая точка, основание, с которого всё начинается. Выполняется один раз и всё.
  2. Рекурсивная часть (Recursive Member): А вот это уже запрос, который тыкает пальцем в само CTE и говорит: «Эй, дай-ка я присоединюсь к тому, что у тебя уже есть!». И так по кругу, пока не кончатся строки для присоединения.

Классический пример — разгрести иерархию категорий, где у каждой есть родительская.

WITH RECURSIVE CategoryTree AS (
    -- 1. Якорь: ловим корневые категории, у которых нет родителя
    SELECT id, name, parent_id, 0 AS level
    FROM categories
    WHERE parent_id IS NULL

    UNION ALL

    -- 2. Рекурсия: цепляем к найденным их детей, потом детей детей, и так до упора
    SELECT c.id, c.name, c.parent_id, ct.level + 1
    FROM categories c
    JOIN CategoryTree ct ON c.parent_id = ct.id
)
SELECT *
FROM CategoryTree;

И вот он, пиздец, красота! Запрос будет крутиться, пока не пройдёт все уровни вложенности.

А что в Go, спросишь?

Да хуйня вопрос. В Go для этого дела используется стандартный database/sql. Запускаешь этот свой рекурсивный запрос через db.Query(), как самый обычный SELECT, а потом построчно считываешь результат, пока rows.Next() не вернёт false. Никаких особых плясок с бубном — база данных всю рекурсивную мозгоёбку делает сама, а ты просто получаешь готовый список строк.