Начать учиться
  • Полезные советы
1 253 просмотра

CTE SQL: что это такое, синтаксис WITH и примеры запросов

Коллега просит добавить в отчёт один фильтр. Вы открываете запрос, а там подзапрос внутри другого подзапроса и ещё парочка в соседнем ряду. Прежде чем менять условие, приходится выяснять, что считает каждый кусок. Самое неловкое: всё это написали вы всего неделю назад.

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

Вы узнаете:

Что такое CTE в SQL и зачем он нужен

CTE (Common Table Expression, или общее табличное выражение) — результат запроса с собственным названием, доступный в пределах одной инструкции SQL. Вы описываете выборку, а затем обращаетесь к ней примерно так же, как к таблице.

Зачем добавлять ещё одну конструкцию? К примеру, вам нужно отобрать записи, посчитать показатели и сравнить их. Если поместить вычисления во вложенные запросы в SQL, придётся разбираться, где заканчивается одна выборка и начинается следующая. Запрос буквально превращается в матрёшку. 

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

Начнём с небольшого запроса (здесь и далее — условные примеры, в своей базе замените их на реальные):

WITH active_users AS (
    SELECT id, name
    FROM users
    WHERE status = 'active'
)
SELECT * FROM active_users;

Внутри скобок отбираем из <users> активных пользователей со столбцами <id> и <name>. Выборку называем <active_users>. Последний SELECT выводит её данные.

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

От понятного примера — к своему запросу

В примере выше всё на месте: таблица известна, условие задано, столбцы уже выбраны. А в рабочей задаче сначала нужно решить, откуда вообще брать информацию и как её связать.

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

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

Оставить заявку на программу


Как работает оператор WITH и базовый синтаксис WITH AS SQL

Логика всегда одна: сначала объявляем CTE, затем пишем основной запрос. В многоточия подставляем свой код:

WITH имя_cte AS (
    SELECT ...
)
SELECT ...
FROM имя_cte;

Что входит в структуру запроса:

WITH — начало объявления. С этого слова стартует вся конструкция.

имя_cte — то, как мы обратимся к результату дальше.

AS ( ... ) — определение запроса (<query_definition>). Внутри скобок живёт логика выборки.

SELECT — основная часть, ради которой всё затевалось. Она обращается к <имя_cte>, будто это настоящая таблица.

Чем это отличается от подзапроса: вложенный запрос — выборка, встроенная внутрь другой, например в FROM, JOIN или WHERE. Этот запрос спрятан внутри и часто дублируется, если нужен в нескольких местах.

CTE же вынесен наверх, объявлен один раз и доступен по имени в рамках текущего запроса. При этом область видимости CTE ограничена только одной инструкцией SQL: он не сохраняется в сессии и не виден в последующих запросах. Проще говоря, появляется разница между «спрятать в скобках» и «дать понятное название и переиспользовать».

Примеры простого CTE от фильтрации до вычислений

Сценарий 1: предварительная агрегация

Допустим, нам нужно найти клиентов с общей суммой покупок выше средней. Сначала посчитаем сумму заказов каждого клиента. Затем вынесем расчёт в CTE, а сравнение оставим снаружи. Через вложенные подзапросы эта логика заняла бы несколько уровней.

WITH total_sales AS (
    SELECT customer_id, SUM(amount) AS total_amount
    FROM orders
    GROUP BY customer_id
)
SELECT customer_id, total_amount
FROM total_sales
WHERE total_amount > (
    SELECT AVG(total_amount)
    FROM total_sales
);

Обратите внимание, что набор <total_sales> мы используем дважды, но описали лишь один раз. Это и есть снижение дублирования логики.

Сценарий 2: вычисляемые столбцы и фильтрация

Добавим вычисляемый столбец <tax> и отберём заказы, в которых его значение больше 100:

WITH order_stats AS (
    SELECT
        order_id,
        amount,
        amount * 0.2 AS tax
    FROM orders
)
SELECT order_id, amount, tax
FROM order_stats
WHERE tax > 100;

Сначала вычисляем налог, затем фильтруем. Сформировали результирующий набор → отобрали нужные строки.

Чем CTE отличается от подзапросов

Оба способа полезны. Вопрос в том, какую запись вам проще читать, проверять и менять в задаче.

Критерий

CTE

Подзапрос

Читаемость

Именованные этапы удобно читать сверху вниз

Глубокую вложенность приходится разбирать изнутри наружу

Переиспользование

Одно определение доступно несколько раз внутри инструкции

В другом месте текст выборки часто приходится повторять

Отладка

Можно вывести результат необходимого этапа

Вложенную часть сначала нужно отделить от окружающего кода

Простые случаи

Для одной короткой выборки может быть избыточен

Даёт быстрый ответ вложенных подзапросов без отдельного объявления CTE


Вывод: если нужен один короткий фильтр — обычный подзапрос отлично справится и не будет перегружать код. А вот когда логика многоступенчатая или промежуточный результат используется повторно, сложные SQL запросы почти всегда чище переписать через WITH.

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

Иногда данные ссылаются сами на себя. Бытовой пример: руководитель → его подчинённые → их подчинённые. Или дерево категорий интернет-магазина. Для таких иерархий и придуманы рекурсивные запросы.

У них две основные части:

  • Якорная — стартовая точка, она же база (например самый верхний руководитель).

  • Рекурсивная — расширяет результат: находит подчинённых, дочерние категории или следующее число в последовательности.

Обе части соединяются через <UNION ALL>. Пример генерации последовательности из документации MySQL:

<cte> — имя выражения, <(n)> — имя столбца. SELECT 1 создаёт начальную строку. Затем рекурсивная часть проверяет n < 5 и возвращает n + 1.

Получатся числа от 1 до 5.

Почему пятёрка тоже попадёт в результат? Потому что проверяется предыдущее значение: четвёрка подходит под n < 5, поэтому запрос возвращает пятёрку. На следующем шаге она проверку уже не пройдёт.

Без WHERE n < 5 в этом примере не останется условия завершения. MySQL прервёт выполнение после достижения лимита рекурсии и вернёт ошибку.

Теперь пример иерархии сотрудников для SQL Server:

WITH employee_hierarchy AS (
    SELECT id, full_name, manager_id
    FROM employees
    WHERE id = 4
    UNION ALL
    SELECT e.id, e.full_name, e.manager_id
    FROM employees e
    JOIN employee_hierarchy h ON e.manager_id = h.id
)
SELECT * FROM employee_hierarchy;

<id> обозначает сотрудника, <manager_id> — его руководителя. Начинаем с <id = 4>: через JOIN находим прямых подчинённых, затем подчинённых этих людей. Результат включает сотрудника №4 и всю найденную ветку ниже него.

Визуально это выглядит примерно так:

Важно: отдельного WHERE для остановки здесь нет. В конечной иерархии запрос завершится, когда JOIN не найдёт подчинённых. Но при цикле в связях он может снова прийти к пройденному сотруднику. Поэтому проверять нужно и код, и данные.

Для защиты от слишком глубокой рекурсии у СУБД есть разные средства:

  • В SQL Server по умолчанию разрешено 100 уровней рекурсии. Лимит можно задать через OPTION (MAXRECURSION N) в конце основного запроса. Значение 0 снимает ограничение, поэтому не используйте его просто ради исчезновения ошибки.

  • В MySQL переменная <cte_max_recursion_depth> по умолчанию равна 1000. Настройка конкретного сервера или сессии может отличаться.

  • В PostgreSQL нет аналогичного автоматического лимита числа уровней. Для иерархий можно отслеживать уже посещённые узлы, а в версиях, которые её поддерживают — использовать конструкцию CYCLE, время выполнения ограничить через <statement_timeout>.

Запомнить: перед запуском проверять не только запрос, но и структуру связей в таблице.

Примеры для PostgreSQL, MySQL, SQL Server и Oracle

Идея CTE одинакова везде, но диалекты имеют нюансы. Разберём каждый.

PostgreSQL

Поддерживает и WITH, и WITH RECURSIVE. Для рекурсивного выражения нужно ключевое слово RECURSIVE. Пример WITH SQL POSTGRESQL:

WITH RECURSIVE nums AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM nums WHERE n < 5
)
SELECT * FROM nums;

Это та же последовательность от 1 до 5. Имя столбца здесь задаёт <AS n> в якорной части.

В PostgreSQL CTE может вычисляться отдельно или встраиваться в общий план запроса — это зависит от версии, структуры запроса и числа обращений к CTE. Поэтому WITH сам по себе не ускоряет код. В сложных случаях поведением управляют через MATERIALIZED и NOT MATERIALIZED, но на начальном этапе достаточно помнить, что CTE нужен ради понятной структуры, а скорость лучше проверить отдельно.

MySQL

В MySQL CTE появились в версии 8.0. Раньше конструкции WITH не было: аналогичную логику приходилось записывать через подзапросы или другие доступные средства.

Синтаксис похож на PostgreSQL: WITH для обычного CTE, WITH RECURSIVE — для рекурсивного:

WITH RECURSIVE nums (n) AS (

    SELECT 1

    UNION ALL

    SELECT n + 1

    FROM nums

    WHERE n < 5

)

SELECT n

FROM nums;

Microsoft SQL Server (MS SQL)

Здесь тоже ключевое слово RECURSIVE не пишется. Ссылка CTE на собственное имя и делает его рекурсивным — так устроен пример с сотрудниками выше.

Если перед WITH в том же пакете есть другая инструкция, завершите её точкой с запятой <;WITH>. Точка с запятой закрывает предыдущую инструкцию, для самостоятельного запроса <;WITH> не обязателен:

WITH nums AS (

    SELECT 1 AS n

    UNION ALL

    SELECT n + 1

    FROM nums

    WHERE n < 5

)

SELECT n

FROM nums

OPTION (MAXRECURSION 100);

Oracle

Поддерживает WITH, в том числе рекурсивные выражения. После имени для них обязательно перечисляют столбцы: например <WITH nums (n) AS (...)>. Ключевое слово RECURSIVE тоже не нужно: якорную и рекурсивную части соединяют через <UNION ALL>. Исторически в Oracle иерархии строили через <CONNECT BY>, но современные версии полноценно понимают и рекурсивный CTE.

Для запроса без исходной таблицы используется DUAL:

WITH nums (n) AS (

    SELECT 1

    FROM dual

    UNION ALL

    SELECT n + 1

    FROM nums

    WHERE n < 5

)

SELECT n

FROM nums;

Главный практический совет: всегда проверяйте версию СУБД и диалект перед тем, как копировать чужой запрос.

Несколько CTE в одном запросе: ошибки и ограничения

Если этапов больше одного, не спешите писать новое WITH перед каждым. Просто объявите CTE через запятую:

WITH
sales AS (
    SELECT customer_id, SUM(amount) AS total
    FROM orders
    GROUP BY customer_id
),
big_clients AS (
    SELECT customer_id FROM sales WHERE total > 1000
)
SELECT * FROM big_clients;

Заметьте: второй CTE <big_clients> спокойно ссылается на первый <sales>. Это работает как цепочка — каждый следующий может опираться на предыдущий.

Что чаще всего мешает такой конструкции:


  • Неверный порядок объявлений. Для запроса, который должен работать в разных СУБД, располагайте CTE в порядке зависимостей: сначала выражение, которое готовит данные, затем то, которое к нему обращается.

  • Повторное имя. Два CTE с одинаковым именем — гарантированная ошибка. Имена должны быть уникальными.

  • Отсутствие понятной логики. Если CTE называется <cte1>, <cte2>, <cte3>, вы сами запутаетесь. Давайте осмысленные имена.

Что проверить перед выполнением запроса: уникальность имён, порядок объявлений, наличие условия остановки для рекурсии и поддержку CTE вашей версией СУБД.

Краткий вывод и практические советы

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

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

Мини-чек-лист чтения запроса с WITH:

  • Найдите объявления CTE и посмотрите, какие имена они получают.
  • Прочитайте определения сверху вниз, отслеживая зависимости между этапами.
  • Разберите основной SELECT: откуда он берёт данные и что делает с подготовленными результатами.

Начните с малого: возьмите один свой запутанный запрос с вложенностью и перепишите его через WITH. Разница в читаемости станет очевидной сразу.

Теперь запрос легче читать и менять? Отлично!

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

На курсе «Инструменты аналитики данных + ИИ» вы освоите SQL, Excel, Power BI и Python вместе. Практиковаться будете на обработке таблиц, расчётах, создании интерактивных панелей (дашбордов), а ещё научитесь ускоряться с помощью нейросетей.

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

Записаться на программу


Вопросы и ответы

Когда CTE лучше обычного подзапроса?

Когда отдельное имя помогает разобраться в расчёте или одна выборка нужна несколько раз в пределах инструкции. Для короткой вложенной выборки подзапрос может быть удобнее. По скорости универсального победителя нет.

Можно ли использовать несколько CTE в одном запросе?

Да, если ваша СУБД поддерживает такой синтаксис. Они объявляются через запятую и могут ссылаться друг на друга в заданном порядке — каждый следующий видит предыдущие.

Чем рекурсивный CTE отличается от обычного?

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

Поддерживает ли MySQL оператор WITH?

Да, начиная с MySQL 8.0. Проверьте версию сервера, к которому подключены, именно она определяет доступный синтаксис.

Подпишитесь на рассылку Эдюсон

Будем отправлять вам дайджест с лучшими статьями, бесплатными материалами, скидками на курсы и вакансиями от партнёров Эдюсон

Спасибо за подписку!
Александра Кочанова
Копирайтер

Всегда за неспешные прогулки и вдумчивое чтение (даже статей в digital).

Верит, что постоянное обучение — это не только приятно, но и финансово выгодно.

Подробнее об авторе