← Все статьи

Как использовать CTE в PostgreSQL для сложных запросов

Если вы работаете с PostgreSQL, то наверняка сталкивались с ситуацией, когда SQL-запрос превращается в многоэтажную конструкцию из вложенных подзапросов, которую невозможно понять через неделю даже вам самим. Проблема не в сложности логики, а в инструменте. Один из самых элегантных способов решить эту проблему — Common Table Expressions, или CTE, которые в PostgreSQL задаются оператором WITH. Это не просто синтаксический сахар, а мощный механизм для структурирования запросов, рекурсивных вычислений и даже оптимизации выполнения.

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

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

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

Сначала определим среднюю зарплату по компании как отдельный блок. Затем рассчитаем среднюю зарплату по отделам. И наконец, соединим эти два блока в финальном отборе.

Такой подход похож на написание программы: вы определяете промежуточные переменные (в нашем случае — наборы данных), а затем оперируете ими. Любой коллега, взглянув на такой запрос, мгновенно поймет его логику.

Однако настоящая магия CTE раскрывается в рекурсивных запросах. Это уникальная возможность PostgreSQL (и некоторых других СУБД), которая позволяет обрабатывать иерархические данные прямо на уровне SQL, без привлечения процедурного кода. Типичные примеры — деревья категорий товаров, организационные структуры или цепочки комментариев.

Рекурсивное CTE состоит из двух частей: якорного (нерекурсивного) члена и рекурсивного члена, которые объединяются оператором UNION ALL. Якорный член выбирает корневые элементы иерархии. Рекурсивный член присоединяется к самому CTE, выбирая дочерние элементы для строк, полученных на предыдущем шаге. Выполнение продолжается до тех пор, пока рекурсивный член не вернет пустой набор.

Допустим, у нас есть таблица employee с полями id, name и manager_id. Нам нужно построить полную цепочку подчинения для конкретного сотрудника, включая всех его начальников до самого верха.

Мы начинаем с якорного члена, выбирающего самого сотрудника. Затем рекурсивный член присоединяет руководителя этого сотрудника, используя manager_id, и так далее, поднимаясь вверх по иерархии. В результате мы получаем плоский список всех руководителей в цепочке.

Важно помнить о потенциальной опасности бесконечной рекурсии, если в данных есть циклические ссылки (например, сотрудник А является менеджером сотрудника Б, а Б — менеджером А). PostgreSQL предоставляет механизмы для защиты от этого, например, максимальную глубину рекурсии или ручную проверку циклов через массив пройденных идентификаторов.

Отдельный важный аспект — материализация CTE. По умолчанию PostgreSQL может «встраивать» содержимое CTE в основной запрос (как если бы это было подзапросом), либо материализовать его — сохранить результат во временной таблице для многократного использования внутри запроса. Поведение можно контролировать с помощью модификаторов MATERIALIZED или NOT MATERIALIZED.

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

Напротив, NOT MATERIALIZED (или поведение по умолчанию во многих случаях) позволяет оптимизатору «развернуть» CTE и перепланровать весь запрос целиком, часто находя более эффективные пути выполнения за счет использования индексов и перестановки условий фильтрации.

Поэтому правило такое: используйте MATERIALIZED явно только тогда, когда уверены в выгоде однократного вычисления тяжелого блока или когда нужно разорвать план выполнения (например, избежать слишком агрессивного гнездового цикла). В остальных случаях доверяйте планировщику PostgreSQL.

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

Рассмотрим еще один практический пример — пошаговую трансформацию данных внутри одной транзакции: очистка сырых данных; агрегация; фильтрация; финальное представление. Каждый шаг оформляется как отдельное CTE с говорящим названием: raw_data_cleaned, daily_totals, filtered_totals. Цепочка таких преобразований читается как техническая документация к процессу обработки данных.

Таким образом Common Table Expressions — это инструмент для мышления и проектирования запроса так же, как для его выполнения. Они превращают SQL из языка однострочных команд в язык декларативного описания потока данных с четкой структурой.

Заключение:

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

💬 Комментарии (13)
👤
michael.miller
20.03.2026 23:44
Статья помогла структурировать знания. Теперь вижу, как разбить один монструозный JOIN на читаемые части.
👤
laura.simpson
24.03.2026 18:37
После вашей статьи переписал несколько отчетов. Запросы не только читабельнее, но и чуть быстрее стали.
👤
michael.miller
25.03.2026 10:25
Полезно, но хотелось бы больше сравнений: когда лучше использовать подзапрос, а когда именно CTE.
👤
maria.garcia23
26.03.2026 17:03
CTE — это мощно, но не забывайте, что до PostgreSQL 12 они были оптимизационными барьерами. Это учтено?
👤
robert.williams
19.03.2026 00:00
Спасибо за комментарий!
👤
david.wilson.apple
27.03.2026 23:08
Использую CTE для построения иерархий (дерево категорий). Код стал намного чище и понятнее коллегам.
👤
webmaster.site
29.03.2026 02:26
CTE — must have для любого разработчика Postgres. Жаль, что не узнал про них раньше.
👤
david.jones-tech
19.03.2026 00:00
Отличный вопрос! Да, это работает.
👤
elizabeth.taylor
30.03.2026 09:50
Хороший материал. А можно подробнее про рекурсивные CTE? Сложно представить практическое применение.
👤
david.wilson.apple
31.03.2026 17:31
Спасибо за объяснение! Раньше боялся использовать WITH, теперь попробую переписать свои подзапросы.
👤
michael.miller
31.03.2026 22:55
А есть ли ограничения по производительности при множественных CTE в одном запросе?
👤
robert.williams
01.04.2026 16:42
Отличная статья! CTE реально спасают от спагетти-кода в сложных запросах, особенно с аналитикой.
👤
tech.support
03.04.2026 08:24
Наконец-то разобрался с отличием CTE от временных таблиц. Спасибо за конкретные примеры из практики!