Обобщённые табличные выражения, или CTE, являются одной из самых мощных и недостаточно используемых функций SQL. Они позволяют вам определять временные именованные наборы результатов, которые можно использовать в более крупном запросе. В этом уроке мы исследуем, как CTE может сделать ваш SQL код более читаемым, легко поддерживаемым и проще для отладки.
Обобщённое табличное выражение (CTE) — это временный набор результатов, определённый в начале запроса с помощью предложения WITH. Думайте о нём как об именованном подзапросе, который можно использовать несколько раз в одном запросе.
Ключевые преимущества CTE:
Общий синтаксис CTE:
WITH имя_cte AS (
SELECT ...
)
SELECT * FROM имя_cte;
Компоненты:
Давайте начнём с простого примера, который вычисляет расходы клиента:
WITH расходы_клиента AS (
SELECT
customer_id,
SUM(amount) AS всего_потрачено,
COUNT(*) AS количество_платежей,
AVG(amount) AS среднее_платежа
FROM
payment
GROUP BY
customer_id
)
SELECT
customer_id,
всего_потрачено,
количество_платежей,
среднее_платежа
FROM
расходы_клиента
WHERE
всего_потрачено > 100
ORDER BY
всего_потрачено DESC;
Этот CTE:
расходы_клиентаПреимущество здесь в ясности — намерение очевидно: мы работаем с данными расходов клиентов.
Сравним одну логику, используя традиционный подход с подзапросом:
Использование подзапроса:
SELECT
customer_id,
всего_потрачено,
количество_платежей,
среднее_платежа
FROM (
SELECT
customer_id,
SUM(amount) AS всего_потрачено,
COUNT(*) AS количество_платежей,
AVG(amount) AS среднее_платежа
FROM
payment
GROUP BY
customer_id
) AS данные_расходов
WHERE
всего_потрачено > 100
ORDER BY
всего_потрачено DESC;
Использование CTE:
WITH расходы_клиента AS (
SELECT
customer_id,
SUM(amount) AS всего_потрачено,
COUNT(*) AS количество_платежей,
AVG(amount) AS среднее_платежа
FROM
payment
GROUP BY
customer_id
)
SELECT
customer_id,
всего_потрачено,
количество_платежей,
среднее_платежа
FROM
расходы_клиента
WHERE
всего_потрачено > 100
ORDER BY
всего_потрачено DESC;
Ключевые различия:
расходы_клиента), а не просто безымянный подзапросВы можете определить несколько CTE в одном запросе, каждый ссылаясь на предыдущие:
WITH расходы_клиента AS (
SELECT
customer_id,
SUM(amount) AS всего_потрачено
FROM
payment
GROUP BY
customer_id
),
крупные_расходы AS (
SELECT
customer_id,
всего_потрачено
FROM
расходы_клиента
WHERE
всего_потрачено > 150
),
детали_клиента AS (
SELECT
кр.customer_id,
кр.всего_потрачено,
c.first_name,
c.last_name,
c.email
FROM
крупные_расходы кр
JOIN
customer c ON кр.customer_id = c.customer_id
)
SELECT
customer_id,
CONCAT(first_name, ' ', last_name) AS имя_клиента,
email,
всего_потрачено
FROM
детали_клиента
ORDER BY
всего_потрачено DESC;
В этом запросе:
расходы_клиента вычисляет всего потрачено на клиентакрупные_расходы фильтрует клиентов с общими расходами > 150детали_клиента объединяет крупные расходы с информацией о клиентеЭта структура делает логический поток ясным и лёгким для следования.
Мощный аспект CTE — ссылаться на себя несколько раз:
WITH продажи_по_месяцам AS (
SELECT
DATE_TRUNC('month', payment_date) AS месяц,
SUM(amount) AS ежемесячный_итог
FROM
payment
GROUP BY
DATE_TRUNC('month', payment_date)
)
SELECT
m1.месяц AS текущий_месяц,
m1.ежемесячный_итог AS текущие_продажи,
m2.ежемесячный_итог AS продажи_предыдущего_месяца,
ROUND(((m1.ежемесячный_итог - m2.ежемесячный_итог) / m2.ежемесячный_итог * 100), 2) AS процент_изменения
FROM
продажи_по_месяцам m1
LEFT JOIN
продажи_по_месяцам m2 ON m1.месяц = m2.месяц + INTERVAL '1 month'
WHERE
m1.месяц IS NOT NULL
ORDER BY
m1.месяц;
Здесь мы ссылаемся на продажи_по_месяцам дважды — один раз как m1 и один раз как m2. Это потребовало бы двух отдельных подзапросов, если бы мы не использовали CTE.
CTE работают замечательно с оконными функциями:
WITH ранжированные_прокаты AS (
SELECT
customer_id,
rental_date,
return_date,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY rental_date DESC
) AS ранг_проката
FROM
rental
),
последний_прокат AS (
SELECT
customer_id,
rental_date,
return_date
FROM
ранжированные_прокаты
WHERE
ранг_проката = 1
)
SELECT
c.customer_id,
CONCAT(c.first_name, ' ', c.last_name) AS имя_клиента,
пп.rental_date AS дата_последнего_проката,
DATEDIFF(CURDATE(), пп.rental_date) AS дней_с_проката
FROM
customer c
LEFT JOIN
последний_прокат пп ON c.customer_id = пп.customer_id
ORDER BY
дней_с_проката DESC
LIMIT 20;
Этот запрос:
ROW_NUMBER() для выявления самого последнего проката каждого клиентаМодульная структура облегчает понимание и изменение.
CTE отличны для сложных аналитических запросов, таких как анализ когорты:
WITH первый_прокат_клиента AS (
SELECT
customer_id,
MIN(rental_date) AS дата_первого_проката,
DATE_TRUNC('month', MIN(rental_date)) AS месяц_когорты
FROM
rental
GROUP BY
customer_id
),
история_проката_клиента AS (
SELECT
ппк.customer_id,
ппк.месяц_когорты,
DATE_TRUNC('month', r.rental_date) AS месяц_проката,
COUNT(*) AS прокатов_за_месяц
FROM
первый_прокат_клиента ппк
JOIN
rental r ON ппк.customer_id = r.customer_id
GROUP BY
ппк.customer_id,
ппк.месяц_когорты,
DATE_TRUNC('month', r.rental_date)
)
SELECT
месяц_когорты,
месяц_проката,
COUNT(DISTINCT customer_id) AS клиенты,
SUM(прокатов_за_месяц) AS всего_прокатов
FROM
история_проката_клиента
GROUP BY
месяц_когорты,
месяц_проката
ORDER BY
месяц_когорты,
месяц_проката;
Этот сложный анализ становится управляемым благодаря CTE:
| Аспект | CTE | Подзапрос |
|---|---|---|
| Читаемость | Высокая читаемость с именованными наборами | Может быть трудной(вложенные структуры) |
| Переиспользуемость | Легко ссылаться несколько раз | Нужно переопределять каждый раз |
| Отладка | Можно тестировать каждый CTE независимо | Сложно изолировать спецлогику |
| Организация | Логическая структура, сверху вниз | Линейная но иногда запутанная |
| Производительность | Одинаковая или лучше (зависит от оптимизатора) | Может быть менее эффективна при глубокой вложенности |
WITHCTE трансформируют сложные запросы из непонятных вложенных структур в ясный, читаемый, легко поддерживаемый код. Это незаменимый инструмент в арсенале любого аналитика данных.
В следующем уроке мы исследуем рекурсивные CTE — мощную функцию для работы с иерархическими данными.