JavaRush /Курсы /SQL SELF /Типичные ошибки при работе с CTE и как их избежать

Типичные ошибки при работе с CTE и как их избежать

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

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

1. Ошибка: материализация CTE и её последствия

Одна из ключевых особенностей PostgreSQL при работе с CTE — это их материализация по умолчанию. Это означает, что результат CTE обрабатывается и временно сохраняется в памяти (или на диске, если данных слишком много). Если запросов много или объем данных велик, это может значимо замедлить выполнение.

Пример:

WITH heavy_data AS (
    SELECT * FROM large_table
)
SELECT * FROM heavy_data WHERE column_a > 100;

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

Как избежать?

С PostgreSQL версии 12 появилась возможность использовать CTE как inline выражение (аналогично подзапросу), что решает проблему материализации. Для этого достаточно указывать такие CTE, которые используются только один раз и нет необходимости сохранять промежуточные результаты.

Пример оптимизированного подхода:

WITH inline_data AS MATERIALIZED (
    SELECT * FROM large_table
)
SELECT * FROM inline_data WHERE column_a > 100;

Совет: если хотите, чтобы материализация всё же происходила, укажите MATERIALIZED. Если нет — используйте NOT MATERIALIZED.

2. Ошибка: рекурсивные CTE зацикливаются

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

Пример:

WITH RECURSIVE endless_loop AS (
    SELECT 1 AS value

    UNION ALL

    SELECT value + 1
    FROM endless_loop
)
SELECT * FROM endless_loop;

Эта штука будет порождать бесконечное количество строк, поскольку нет условия, которое останавливает рекурсию.

Как избежать?

Добавьте чёткое условие остановки с помощью WHERE. Например:

WITH RECURSIVE limited_loop AS (
    SELECT 1 AS value

    UNION ALL

    SELECT value + 1
    FROM limited_loop
    WHERE value < 10
)
SELECT * FROM limited_loop;

Совет: если вы используете рекурсивные CTE для больших иерархий, ограничьте глубину рекурсии с помощью опции max_recursion_depth PostgreSQL.

3. Ошибка: неправильное использование UNION и UNION ALL

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

Пример:

WITH RECURSIVE employee_hierarchy AS (
    SELECT employee_id, manager_id
    FROM employees
    WHERE manager_id IS NULL

    UNION  -- Здесь лучше использовать UNION ALL

    SELECT e.employee_id, e.manager_id
    FROM employees e
    JOIN employee_hierarchy eh ON e.manager_id = eh.employee_id
)
SELECT * FROM employee_hierarchy;

В приведенном примере UNION может удалить важные строки из иерархии, если они случайно повторяются. А ещё это замедлит запрос!

Как исправить?

Используйте UNION ALL, если у вас нет строгой необходимости удалять дубли:

UNION ALL

4. Ошибка: слишком много CTE в одном запросе

Пытаясь сделать запрос как можно более структурированным, некоторые начинают добавлять десятки CTE. Это не только запутывает код, но и перегружает планировщик запросов PostgreSQL.

Пример:

WITH cte1 AS (...),
     cte2 AS (...),
     cte3 AS (...),
     ...
     cte20 AS (...)
SELECT ...
FROM cte20;

Выглядит как ночной кошмар для любого разработчика.

Как исправить?

— Разделите запрос на несколько более простых. Вместо одного мегазапроса с десятками CTE — сделайте несколько независимых запросов.

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

5. Ошибка: сложные CTE без индексации

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

Пример:

WITH filtered_data AS (
    SELECT * FROM large_table WHERE unindexed_column = 'value'
)
SELECT * FROM filtered_data;

Как исправить?

Перед использованием CTE убедитесь, что ваши таблицы оптимизированы:

CREATE INDEX idx_large_table ON large_table(unindexed_column);

6. Ошибка: попытка использовать CTE для многократного вызова данных

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

Пример:

WITH data AS (
    SELECT x, y FROM some_table
)
SELECT x FROM data
WHERE y > 10;
-- Если ещё раз потребуется пересчитать data, это не произойдёт.

Как исправить?

Если нужна динамика или пересчёт, возможно, CTE — не лучший выбор. Используйте подзапросы.

7. Ошибка: отсутствие комментариев

CTE могут быть потрясающим инструментом, но кому нужен сложный SQL-запрос, который никто, включая вас, не сможет прочитать спустя две недели?

Пример:

WITH data_filtered AS (
    SELECT *
    FROM large_table
    WHERE some_column > 100
)
SELECT * FROM data_filtered;

Через месяц уже никто не вспомнит, зачем фильтровались эти данные!

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

WITH data_filtered AS (
    -- Фильтр данных по условию значений столбца some_column > 100
    SELECT *
    FROM large_table
    WHERE some_column > 100
)
SELECT * FROM data_filtered;

8. Ошибка: злоупотребление CTE вместо временных таблиц

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

Пример:

WITH temp_data AS (
    SELECT * FROM large_table
)
SELECT * FROM temp_data WHERE column_a > 100;
SELECT * FROM temp_data WHERE column_b < 50;

Такой запрос с CTE выполнится дважды, хоть данные не меняются!

Как исправить?

Создайте временную таблицу, если данные используются много раз:

CREATE TEMP TABLE temp_table AS
SELECT * FROM large_table;

SELECT * FROM temp_table WHERE column_a > 100;
SELECT * FROM temp_table WHERE column_b < 50;

Заключительный совет

Как с любой мощной функцией, при работе с CTE важно понимать, что они не всегда лучший инструмент для решения задач. Обдумывайте, зачем и как вы их используете. Подход "чем больше CTE, тем лучше" может серьёзно ухудшить производительность и читабельность кода. И, конечно, не забывайте проводить тесты производительности и оптимизировать запросы.

2
Задача
SQL SELF, 28 уровень, 4 лекция
Недоступна
Использование MATERIALIZED для оптимизации CTE
Использование MATERIALIZED для оптимизации CTE
1
Опрос
Оптимизация запросов, 28 уровень, 4 лекция
Недоступен
Оптимизация запросов
Оптимизация запросов
Комментарии (4)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #3449047 Уровень 61
26 октября 2025
почему в лекции указано, что MATERIALIZED идет после AS: WITH inline_data AS MATERIALIZED, а валидатор просит переставить MATERIALIZED сразу после WITH перед именем CTE. И зачем указывать MATERIALIZED, ведь материализация идет по умолчанию, и для оптимизации нужно указать как раз наоборот NOT MATERIALIZED ?
Vlad Tagunkov Уровень 10
15 января 2026
поставил MATERIALIZED перед AS - проверка прошла но Вебсторм не хочет выполнять говорит ошибка. Выполняется нормально вот так

WITH  amount_over AS MATERIALIZED
так выполняется код в Вебсторме но не проходит валидацию. Просить это чинить бесполезное занятие.
Евгений Уровень 49 Expert
28 августа 2025
Не существует опции по умолчанию max_recursion_depth, и вот тут написано, почему: https://postgrespro.com/list/thread-id/1878458?utm_source=chatgpt.com
Илья Уровень 41
7 февраля 2026
Просто нейросети считают, что такой параметр есть.