FILTER в PostgreSQL: несколько условных агрегатов в одном запросе

FILTER в PostgreSQL: несколько условных агрегатов в одном запросе
Содержание статьи

FILTER (WHERE условие) передаёт конкретной агрегатной функции только подходящие строки. Так в одном SELECT можно посчитать общее число заявок, число завершённых и сумму по отдельному статусу. Обычный WHERE ограничивает общий набор строк для всех агрегатов, а FILTER задаёт отдельное условие для каждого.

В разборе Mentoring.digital используем учебные строки в CTE. Запрос можно выполнить без создания постоянной таблицы. Значения служат иллюстрацией синтаксиса, а не статистикой реальной компании.

Несколько показателей из одного набора строк

WITH orders(id, status, amount) AS (
  VALUES
    (1, 'paid', 100),
    (2, 'new', 80),
    (3, 'paid', NULL),
    (4, 'cancelled', 50)
)
SELECT
  count(*) AS all_orders,
  count(*) FILTER (WHERE status = 'paid') AS paid_orders,
  count(amount) FILTER (WHERE status = 'paid') AS known_paid_amounts,
  sum(amount) FILTER (WHERE status = 'paid') AS paid_total
FROM orders;

Для этих строк ожидаются значения 4, 2, 1, 100. Завершённых заявок две, но известная сумма есть только у одной. count(*) считает строки, а count(amount) пропускает NULL. Это различие сохраняется и при использовании FILTER.

Если условие фильтра даёт NULL, строка не проходит его: агрегату передаются строки, для которых условие истинно. При nullable-статусах отдельно решите, нужно ли считать неизвестный статус в собственной колонке.

Чем FILTER отличается от WHERE и HAVING

Если добавить WHERE status = 'paid' перед агрегацией, общий count(*) уже посчитает только оплаченные заявки. Показатель «все заявки» потеряет исходный смысл. FILTER позволяет оставить общий набор и получить отдельный показатель внутри него.

HAVING решает другую задачу: отбирает группы после группировки. Например, можно вывести только дни, где число оплаченных заявок больше нуля. Не заменяйте FILTER через HAVING, если нужна колонка с условным количеством в каждой группе.

Что получится, если подходящих сумм нет

WITH orders(status, amount) AS (
  VALUES ('new', 80), ('cancelled', 50)
)
SELECT
  count(*) FILTER (WHERE status = 'paid') AS paid_orders,
  sum(amount) FILTER (WHERE status = 'paid') AS paid_total,
  coalesce(sum(amount) FILTER (WHERE status = 'paid'), 0) AS display_total
FROM orders;

Ожидаемый результат: ноль, NULL и ноль. Пустой count даёт 0, а пустой sum даёт NULL. coalesce уместен, если по условиям отчёта отсутствие суммы должно отображаться как ноль. Иначе он способен скрыть различие между отсутствующими данными и настоящей нулевой суммой.

Сравнение с CASE и проверка на ошибку

Условный подсчёт можно записать как count(CASE WHEN status = 'paid' THEN 1 END). Но вариант count(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) посчитает и остальные строки: и 1, и 0 не являются NULL. FILTER часто делает намерение заметнее, но сам по себе не гарантирует ускорение. Производительность проверяют на реальных данных и плане запроса.

Перед использованием в отчёте проверьте смешанные статусы, отсутствие подходящих строк, неизвестную сумму и пустой вход. При GROUP BY проверьте каждый показатель внутри группы. FILTER не создаёт отсутствующие группы: чтобы показать день без единой заявки, понадобится отдельный набор дней и соединение.

Такие запросы можно отработать на курсах SQL с практическими заданиями. Смысл показателей и пограничные случаи удобно обсудить с наставником по SQL.

Похожие статьи по данной теме