Menu

CTE в SQLite: как работает оператор WITH

Разбираем Common Table Expressions в SQLite: как через WITH давать имена подзапросам, объединять несколько CTE и писать запросы, читаемые сверху вниз.

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

CTE — это именованный подзапрос

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

Структура всегда одна и та же:

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

Тот же запрос без CTE

А вот та же логика, записанная через подзапрос — чтобы было видно, что именно заменяет CTE:

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

Несколько CTE в одном запросе

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

Один WITH, дальше определения CTE через запятую. Второй CTE (big_spenders) обращается к первому (customer_totals) так же, как к обычной таблице. Главный SELECT идёт после последнего определения CTE.

Типичная промашка — написать WITH ещё раз перед вторым CTE. Не надо — это ошибка синтаксиса. Одного WITH хватает на все определения сразу.

Несколько обращений к одному CTE

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

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

CTE с INSERT, UPDATE и DELETE

CTE — это не только про SELECT. Конструкцию WITH можно поставить перед INSERT, UPDATE или DELETE, чтобы использовать именованный подзапрос в запросе на изменение данных:

CTE описывает, какие строки нужно пометить, а INSERT ... SELECT берёт его как источник данных. Тот же приём отлично работает и с DELETE FROM ... WHERE id IN (SELECT id FROM cte) — удобно для пакетных удалений, когда логика отбора нетривиальная.

Когда стоит использовать CTE

Несколько практических ориентиров:

  • В запросе больше одного логического шага. Сначала агрегируем, потом фильтруем по агрегату, потом джойним результат — это конвейер, и отдельный CTE на каждый шаг делает его читаемым.
  • Иначе пришлось бы повторять один и тот же подзапрос. Опишите его один раз — обращайтесь сколько угодно.
  • Подзапросу нужно имя. Если над подзапросом так и просится комментарий с пояснением, что он вообще считает, — имя CTE и есть этот комментарий, только закреплённый синтаксисом.
  • Вы собираетесь писать рекурсивный запрос. Без WITH RECURSIVE это попросту невозможно — об этом подробно в следующем разделе.

А когда CTE — это излишество:

  • Один простой подзапрос, который используется в единственном месте. WHERE id IN (SELECT id FROM ...) и так нормально читается.
  • Запросы, где важна производительность, и вы уже убедились, что встроенная логика работает быстрее. SQLite обычно меньше других СУБД относится к CTE как к барьеру для оптимизатора, но для горячих путей всё же стоит проверить план через EXPLAIN QUERY PLAN.

Разбираем пример целиком

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

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

Дальше: рекурсивные CTE

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

Часто задаваемые вопросы

Что такое CTE в SQLite?

Common Table Expression (CTE) — это именованный подзапрос, который объявляется в начале SELECT, INSERT, UPDATE или DELETE. Пишете ключевое слово WITH, задаёте имя — и дальше обращаетесь к нему в основном запросе так, будто это обычная таблица. CTE здорово упрощает чтение сложных запросов: результат собирается шаг за шагом, а не одной громоздкой конструкцией.

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

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

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

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

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ