Learning Platform
Урок 07.05 · 17 мин
Средний
FILTER clauseConditional aggregationPivotingCASE WHENAggregate filtering

Задача: считать «только определённые» строки в группе

Допустим, у каждого клиента есть несколько заказов с разными статусами: delivered, paid, cancelled, refunded. И нужно построить отчёт: для каждого клиента — сколько заказов каждого статуса.

Через обычный GROUP BY customer_id ты получишь общий счётчик. Через GROUP BY customer_id, status — счётчики в строках разной длины (каждый статус — отдельная строка). А нужен один кортеж на клиента с четырьмя колонками-счётчиками.

Это типичный

conditional aggregation
— агрегация с условием. В Postgres для этого есть элегантный синтаксис agg(...) FILTER (WHERE ...), а в стандартном SQL — приём через CASE.

FILTER (WHERE …) — стандартный SQL, реализован в Postgres

Синтаксис: после любой агрегатной функции можно добавить FILTER (WHERE условие). Агрегат посчитает значения только в тех строках, где условие истинно.

Счётчики заказов по статусу — один кортеж на клиента:

PostgreSQL

Каждый клиент — одна строка. По колонкам — счётчики разных статусов. Это и есть «горизонтальный отчёт», или

pivot
.

FILTER работает со всеми агрегатами — SUM, AVG, MIN, MAX, STRING_AGG, ARRAY_AGG, и так далее:

Сумма стоимости — общая и только delivered:

PostgreSQL

Альтернатива в чистом стандартном SQL: SUM(CASE WHEN …)

До добавления FILTER (он появился в SQL:2003, в Postgres 9.4) этот же приём писался через CASE. Конструкция длиннее, но работает везде, включая старые СУБД и MySQL:

То же самое через SUM(CASE WHEN ... THEN 1 ELSE 0 END):

PostgreSQL

Это работает потому, что CASE возвращает 1 для подходящих строк и 0 для остальных, а SUM всех 1 даёт счётчик. Эквивалентно COUNT(*) FILTER (...).

Для COUNT(col) FILTER (WHERE ...) через CASE надо вернуть NULL для не подходящих (потому что COUNT(col) пропускает NULL):

COUNT(col) через CASE с NULL для пропуска:

PostgreSQL

CASE WHEN ... THEN 1 END без ELSE возвращает NULL для не-delivered строк, а COUNT пропускает NULL — получаем то же, что COUNT(*) FILTER (WHERE status = 'delivered').

Сравнение: FILTER vs CASE

АспектFILTER (WHERE ...)CASE WHEN ... THEN ... ELSE ...
СтандартSQL:2003SQL-92
Postgresда, начиная с 9.4да, всегда
MySQLнет (до 8.0 без поддержки)да
Читаемостьвыше — намерение видно сразуниже — мешается с агрегатом
Семантика NULLпрозрачная — фильтр работает «снаружи»надо думать про END без ELSE и COUNT/SUM

Правило: на Postgres пиши FILTER. На MySQL или для cross-database кода — CASE. Результат идентичен.

Pivot: длинная форма → широкая форма

Главное практическое применение FILTER — это

pivot
, превращение «длинной» формы в «широкую». Это типичный паттерн для дашбордов.

Pivot через FILTER: long → wide

Исходные строки имеют одну колонку status. После pivot — три счётчика-колонки по каждому статусу. FILTER элегантно делает разрезы в одной строке результата.

long: orderscustomer_id, status
строка(1, delivered)
строка(1, delivered)
строка(1, paid)
строка(1, cancelled)
pivotFILTER
wide: один клиентcustomer_id, delivered, paid, cancelled
строка(1, 2, 1, 1)

Расширим пример: pivot по месяцу, считаем выручку:

Выручка по месяцам — pivot через FILTER:

PostgreSQL

Pivot хорош, когда значения колонок известны заранее (5 месяцев, 4 статуса). Если категорий много или они динамические — это уже задача для приложения, а не для SQL.

FILTER с DISTINCT

FILTER сочетается с DISTINCT так же, как обычный агрегат:

Уникальные клиенты с заказом каждого статуса:

PostgreSQL

Сразу три «уникальных пользователя по событию» одним запросом. Это очень полезно для funnel-аналитики.

Подводный камень: NULL внутри FILTER

FILTER (WHERE x = 'delivered') отсеивает строки, где условие не TRUE. То есть NULL и FALSE отбрасываются одинаково. Это по умолчанию правильно — но если ты ожидаешь, что NULL должен учитываться, добавь явное IS NULL в условие:

Считаем клиентов с известным и неизвестным годом рождения:

PostgreSQL

Заметь, что FILTER (WHERE birth_year = NULL) дал бы 0 — потому что любое сравнение с NULL даёт NULL, и FILTER отбросит такие строки. Только IS NULL сработает.

FILTER vs WHERE — не путать

Это распространённая ошибка новичков: пытаются заменить WHERE на FILTER или наоборот.

  • WHERE отсекает строки до агрегации. После него ничего не помнит про отсечённое.
  • FILTER отсекает строки внутри каждого агрегата, при этом другие агрегаты в том же запросе продолжают видеть все строки.

То есть SELECT COUNT(*), COUNT(*) FILTER (WHERE status='delivered') FROM orders даёт два разных числа в одной строке — общее число заказов и число delivered. Сделать то же через WHERE без подзапросов невозможно.

Проверка знанийKnowledge check
Запрос SELECT SUM(amount) FILTER (WHERE type = 'income') AS income, SUM(amount) FILTER (WHERE type = 'expense') AS expense, SUM(amount) AS net FROM transactions. Что вернёт net, если в таблице 5 income по 100 и 3 expense по 50?
ОтветAnswer
income = 500 (5 × 100, отфильтровано по type='income'). expense = 150 (3 × 50, отфильтровано по type='expense'). net = 650 (всё суммируется, ведь к net FILTER не применён). Если предполагалось net = income − expense = 350, надо было бы хранить expense с отрицательным знаком ИЛИ написать SUM(CASE WHEN type='income' THEN amount ELSE -amount END) — FILTER здесь не подходит, потому что он не меняет знак, а только пропускает строки.
Когда какой тест в dbt: staging vs marts, keys vs measures

Чек-лист

  • agg(...) FILTER (WHERE условие) — стандартный SQL:2003 синтаксис для conditional aggregation. В Postgres работает с 9.4.
  • Полностью эквивалентно SUM(CASE WHEN ... THEN 1 ELSE 0 END) для COUNT и COUNT(CASE WHEN ... THEN col END) для COUNT(col).
  • Главное применение — pivot: длинная форма → широкая форма, по одной строке на ключ группировки, по колонке на каждое значение.
  • Можно сочетать с DISTINCT (COUNT(DISTINCT x) FILTER (WHERE ...)) — частый паттерн для funnel-аналитики.
  • NULL внутри FILTER отсекается так же, как FALSE — используй IS NULL явно, если нужно учесть NULL.
  • WHERE и FILTER — не одно и то же: WHERE режет до агрегации (на всём запросе), FILTER — внутри отдельного агрегата (остальные агрегаты в том же SELECT его не видят).

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Какая SQL-конструкция эквивалентна COUNT(*) FILTER (WHERE status = 'delivered')?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6