lenec ru

← все посты

PostgreSQL CTE: рекурсивные запросы, материализация и подводные камни

13K

Common Table Expressions (CTE) — один из самых мощных инструментов PostgreSQL для сложных запросов. Но за удобством скрываются нюансы: материализация может убить производительность, рекурсия — уйти в бесконечность. Разбираем CTE от основ до подводных камней.

CTE basics — WITH для декомпозиции

CTE разбивает сложный запрос на именованные блоки:

WITH active_users AS (
    SELECT id, name FROM users
    WHERE last_login > now() - interval '30 days'
),
user_orders AS (
    SELECT u.id, u.name, count(o.id) AS order_count,
           sum(o.total) AS total_spent
    FROM active_users u
    JOIN orders o ON o.user_id = u.id
    GROUP BY u.id, u.name
)
SELECT name, order_count, total_spent
FROM user_orders
WHERE total_spent > 10000
ORDER BY total_spent DESC;

Преимущества: читаемость, переиспользование (один CTE можно использовать несколько раз), логическая декомпозиция.

Рекурсивные CTE — деревья, графы, ряды

Рекурсивный CTE: anchor (начальное условие) + recursive term (шаг), объединённые через UNION ALL:

-- Обход дерева категорий
WITH RECURSIVE category_tree AS (
    SELECT id, name, parent_id, 0 AS depth,
           name::text AS path
    FROM categories
    WHERE parent_id IS NULL

    UNION ALL

    SELECT c.id, c.name, c.parent_id, ct.depth + 1,
           ct.path || ' > ' || c.name
    FROM categories c
    JOIN category_tree ct ON c.parent_id = ct.id
    WHERE ct.depth < 10
)
SELECT * FROM category_tree ORDER BY path;

Поиск кратчайшего пути в графе:

WITH RECURSIVE path AS (
    SELECT target_id, ARRAY[source_id, target_id] AS route, 1 AS hops
    FROM edges WHERE source_id = 1

    UNION ALL

    SELECT e.target_id, p.route || e.target_id, p.hops + 1
    FROM path p
    JOIN edges e ON e.source_id = p.target_id
    WHERE e.target_id != ALL(p.route)
      AND p.hops < 6
)
SELECT route, hops FROM path
WHERE target_id = 42 ORDER BY hops LIMIT 1;

MATERIALIZED vs NOT MATERIALIZED

До PostgreSQL 12 CTE всегда материализовались. С версии 12 оптимизатор может инлайнить CTE в основной запрос.

-- Материализация: результат вычисляется один раз
WITH active AS MATERIALIZED (
    SELECT id FROM users WHERE status = 'active'
)
SELECT * FROM orders WHERE user_id IN (SELECT id FROM active);

-- Инлайн: CTE подставляется как подзапрос
WITH active AS NOT MATERIALIZED (
    SELECT id FROM users WHERE status = 'active'
)
SELECT * FROM orders WHERE user_id IN (SELECT id FROM active);

MATERIALIZED полезен когда CTE используется несколько раз или нужна optimization fence. NOT MATERIALIZED — когда внешний фильтр должен протолкнуться к индексу.

Разница на EXPLAIN:

-- MATERIALIZED: CTE Scan (rows=500000), Filter: user_id=123
-- NOT MATERIALIZED: Index Scan idx_events_user_type
--   Index Cond: (user_id=123 AND type='click')

CTE vs subquery vs temp table

Бенчмарк на таблице events (10M строк, PostgreSQL 16):

Подход                    | Время  | Buffers
────────────────────────────────────────────────
CTE MATERIALIZED          | 820 ms | 45000
CTE NOT MATERIALIZED      | 2 ms   | 12
Subquery                  | 2 ms   | 12
Temp table + index        | 350 ms | 22000

Правило: CTE используется один раз + внешний фильтр → NOT MATERIALIZED или подзапрос. CTE многократно или нужна изоляция → MATERIALIZED.

Типичные ошибки

Бесконечная рекурсия — если в данных цикл, запрос не завершится:

-- Защита: ограничение глубины
WHERE depth < 100
-- Или проверка циклов
WHERE id != ALL(visited_ids)

Неожиданная материализация — CTE материализуется если: ссылается больше одного раза, содержит DML (INSERT/UPDATE/DELETE), или оптимизатор решил что так дешевле.

CTE в UPDATE/DELETE — мощный паттерн:

-- Удаление дубликатов
WITH duplicates AS (
    SELECT id, row_number() OVER (
        PARTITION BY email ORDER BY created_at DESC
    ) AS rn
    FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM duplicates WHERE rn > 1);

Такой CTE всегда материализуется — гарантирует консистентный снимок для DELETE.

CTE — незаменимый инструмент для сложных запросов. Главное — понимать когда материализация помогает, а когда мешает, и проверять план через EXPLAIN ANALYZE.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.