Урок 4.4: Условная агрегация
Условная агрегация в SQL позволяет считать разные метрики в одном запросе, не делая несколько отдельных выборок. Идея проста: внутри агрегатной функции (SUM, COUNT, AVG) используется условное выражение (чаще всего CASE, но в некоторых СУБД это может быть и другой условный оператор), которое включает в расчет только те строки, что соответствуют условию.
Этот подход особенно полезен для отчетов, дашбордов и аналитики, где нужно получить несколько показателей сразу: количества, суммы, доли, разбивки по статусам и т.д.
В этом уроке разберем:
- как работает условная агрегация;
- как считать условные количества, суммы и средние значения;
- как строить pivot-отчеты (поворот строк в колонки) через
CASE.
Базовая идея
Классический шаблон условной агрегации:
AGGREGATION_FUNCTION(CASE WHEN condition THEN value ELSE 0 END)
или короткий вариант:
AGGREGATION_FUNCTION(CASE WHEN condition THEN 1 END)
Что происходит:
CASEвозвращает значение в зависимости от условия. В коротком варианте если условие не выполняется - возвращаетсяNULL;- агрегатная функция аккумулирует результат по группе;
- на выходе вы получаете метрику по условию.
Условная сумма
Пример: суммы продаж по сотрудникам с разбивкой по категориям суммы
SELECT
staff_id,
SUM(CASE WHEN amount < 2 THEN amount ELSE 0 END) AS low_amount_total,
SUM(CASE WHEN amount BETWEEN 2 AND 6 THEN amount ELSE 0 END) AS medium_amount_total,
SUM(CASE WHEN amount > 6 THEN amount ELSE 0 END) AS high_amount_total
FROM payment
GROUP BY staff_id;
Результат: один запрос возвращает три разных суммы по каждому сотруднику.
Условное среднее
Пример: средняя сумма крупных платежей по сотрудникам
SELECT
staff_id,
AVG(CASE WHEN amount >= 5 THEN amount END) AS avg_big_payment
FROM payment
GROUP BY staff_id;
Результат: для каждого сотрудника считается средняя сумма только тех платежей, где amount >= 5.
Почему здесь обычно не нужен ELSE 0:
AVGвычисляется как сумма значений, деленная на их количество;- если подставить
0для строк, которые не подходят под условие, эти нули попадут в расчет и занизят среднее; - поэтому для условного
AVGобычно используютELSE NULLили не указываютELSEсовсем.
Условный подсчет количества
Пример: сколько платежей в каждом диапазоне суммы
SELECT
customer_id,
COUNT(CASE WHEN amount < 2 THEN 1 END) AS low_payments,
COUNT(CASE WHEN amount BETWEEN 2 AND 6 THEN 1 END) AS medium_payments,
COUNT(CASE WHEN amount > 6 THEN 1 END) AS high_payments
FROM payment
GROUP BY customer_id;
Результат: для каждого клиента выводится число «низких», «средних» и «высоких» платежей.
Почему здесь не нужен ELSE:
- если условие истинно,
CASEвозвращает1; - если условие ложно и
ELSEне указан,CASEвозвращаетNULL; COUNT(expression)считает только не-NULL, поэтому учитываются только строки, где условие выполнилось.
Важно: не стоит писать ELSE 0 в таком шаблоне для COUNT, потому что 0 тоже не NULL, и тогда COUNT начнет считать почти все строки.
Пример: подсчет возвращенных и невозвращенных аренд
SELECT
staff_id,
COUNT(return_date) AS returned_count,
COUNT(CASE WHEN return_date IS NULL THEN 1 END) AS not_returned_count
FROM rental
GROUP BY staff_id;
Что здесь происходит:
COUNT(return_date)считает только не-NULLзначения, то есть количество возвращенных аренд;COUNT(CASE WHEN return_date IS NULL THEN 1 END)считает только строки, где дата возврата отсутствует, то есть невозвращенные аренды;GROUP BY staff_idформирует отдельные счетчики для каждого сотрудника.
Итог: в одном запросе вы получаете обе метрики по каждому сотруднику.
Pivot-техника через CASE
Что такое pivot в SQL
Pivot (поворот) — это преобразование строк в колонки. Обычно исходные данные содержат категории в строках, а в отчете нужно видеть эти категории как отдельные столбцы.
Во многих СУБД есть специальный оператор PIVOT, но универсальный и переносимый способ — условная агрегация с CASE.
Базовый шаблон pivot
SELECT
group_column,
SUM(CASE WHEN pivot_key = 'A' THEN measure ELSE 0 END) AS col_a,
SUM(CASE WHEN pivot_key = 'B' THEN measure ELSE 0 END) AS col_b,
SUM(CASE WHEN pivot_key = 'C' THEN measure ELSE 0 END) AS col_c
FROM source_table
GROUP BY group_column;
Пример: pivot по рейтингу фильмов
Ниже пример, где для каждой категории фильмов считаем количество фильмов по рейтингам в отдельных столбцах:
SELECT
c.name AS category,
COUNT(CASE WHEN f.rating = 'G' THEN 1 END) AS g_films_count,
AVG(CASE WHEN f.rating = 'G' THEN length ELSE 0 END) AS g_films_average_length,
COUNT(CASE WHEN f.rating = 'PG' THEN 1 END) AS pg_films_count,
AVG(CASE WHEN f.rating = 'PG' THEN length ELSE 0 END) AS pg_films_average_length,
COUNT(CASE WHEN f.rating = 'PG-13' THEN 1 END) AS pg13_films_count,
AVG(CASE WHEN f.rating = 'PG-13' THEN length ELSE 0 END) AS pg13_films_average_length,
COUNT(CASE WHEN f.rating = 'R' THEN 1 END) AS r_films_count,
AVG(CASE WHEN f.rating = 'R' THEN length ELSE 0 END) AS r_films_average_length,
COUNT(CASE WHEN f.rating = 'NC-17' THEN 1 END) AS nc17_films_rating,
AVG(CASE WHEN f.rating = 'NC-17' THEN length ELSE 0 END) AS nc17_films_average_length
FROM film f
JOIN film_category fc ON f.film_id = fc.film_id
JOIN category c ON fc.category_id = c.category_id
GROUP BY c.name
ORDER BY c.name;
Результат: каждая строка — это категория, а столбцы показывают количество фильмов каждого рейтинга и их среднюю продолжительность.
Практические рекомендации
- Для
SUMобычно используютELSE 0, чтобы строки вне условия давали нулевой вклад. - Для
COUNT(CASE ...)ELSEобычно не нужен:COUNTи так игнорируетNULL. - Для
AVG(CASE ...)чаще используютELSE NULLили вариант безELSE, чтобы не занизить среднее. - Если условных метрик много, давайте столбцам понятные алиасы (
*_count,*_total). - Проверяйте, что условия в
CASEне пересекаются, если категории должны быть взаимоисключающими (комбинация условий из CASE и GROUP BY должна обеспечивать уникальность). - Для больших запросов сначала отладьте логику на маленьком наборе данных или с
LIMIT.
Практическое применение
Pivot по дням недели:
SELECT MONTH(rental_date) AS rental_month, SUM(CASE WHEN DAYNAME(rental_date) = 'Monday' THEN 1 ELSE 0 END) AS monday_rentals, SUM(CASE WHEN DAYNAME(rental_date) = 'Tuesday' THEN 1 ELSE 0 END) AS tuesday_rentals, SUM(CASE WHEN DAYNAME(rental_date) = 'Wednesday' THEN 1 ELSE 0 END) AS wednesday_rentals, SUM(CASE WHEN DAYNAME(rental_date) = 'Thursday' THEN 1 ELSE 0 END) AS thursday_rentals, SUM(CASE WHEN DAYNAME(rental_date) = 'Friday' THEN 1 ELSE 0 END) AS friday_rentals, SUM(CASE WHEN DAYNAME(rental_date) = 'Saturday' THEN 1 ELSE 0 END) AS saturday_rentals, SUM(CASE WHEN DAYNAME(rental_date) = 'Sunday' THEN 1 ELSE 0 END) AS sunday_rentals FROM rental GROUP BY MONTH(rental_date);Такой запрос показывает, сколько аренд было оформлено в каждом месяце по дням недели.
Расчет долей по условию:
SELECT customer_id, SUM(CASE WHEN amount >= 5 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) AS high_payment_share FROM payment GROUP BY customer_id;
Коротко про синтаксис FILTER
В некоторых СУБД (например, PostgreSQL) условие можно вынести из CASE в FILTER:
COUNT(*) FILTER (WHERE condition)
SUM(amount) FILTER (WHERE condition)
Смысл здесь тот же, что и у условной агрегации через CASE: агрегатная функция обрабатывает не все строки, а только те, которые прошли условие в WHERE внутри FILTER.
Такой синтаксис часто читается проще, особенно если в одном SELECT нужно посчитать несколько разных метрик с разными условиями.
Например:
SELECT
customer_id,
COUNT(*) AS total_payments,
COUNT(*) FILTER (WHERE amount >= 5) AS big_payments_count,
SUM(amount) FILTER (WHERE amount >= 5) AS big_payments_total
FROM payment
GROUP BY customer_id;
В этом примере:
COUNT(*)считает все платежи клиента;COUNT(*) FILTER (WHERE amount >= 5)считает только «крупные» платежи;SUM(amount) FILTER (WHERE amount >= 5)суммирует только такие платежи.
То есть FILTER делает ту же работу, что и CASE, но в более компактной форме. При этом важно помнить, что этот синтаксис поддерживается не всеми СУБД.
Основные выводы из этого урока
- Условная агрегация — это агрегатная функция + условное выражение, чаще всего
CASE. - Через
SUM(CASE ...),COUNT(CASE ...)иAVG(CASE ...)можно получать несколько метрик в одном запросе. - Pivot-техника через
CASE— универсальный способ развернуть строки в колонки. - Этот подход хорошо подходит для аналитических отчетов и дашбордов.
Освоив условную агрегацию, вы сможете писать более компактные и выразительные SQL-запросы для бизнес-аналитики.