COUNT с CASE WHEN: подсчет некоторых строк без фильтрации остальных
Вам нужны отправленные заказы, отмененные заказы и общее количество заказов в одном результате. Выполнение трех запросов и вставка ответов вместе работает и не сохраняется при изменении диапазона дат. Версия с одним запросом помещает условие внутри агрегата.
Что вы делаете: COUNT(CASE WHEN condition THEN 1 END), без ELSE. Для восьми заказов ниже это возвращает 5 отправленных. Добавлять ELSE 0 и тот же запрос возвращает 8.
Краткая версия. COUNT подсчитывает значения и игнорирует NULL. Этот единственный факт и составляет всю технику.
Стол
CREATE TABLE orders(order_id INT, customer_id INT, amount INT, status TEXT);
INSERT INTO orders VALUES
(101,1,120,'shipped'),(102,1,80,'shipped'),(103,2,200,'cancelled'),
(104,2,50,'shipped'),(105,3,300,'shipped'),(106,4,90,'pending'),
(107,4,60,'shipped'),(108,5,150,'cancelled');
Восемь заказов: пять отправлено, два отменены, один находится на рассмотрении. Общая сумма 1050.
Запрос
SELECT
COUNT(*) AS all_orders,
COUNT(CASE WHEN status = 'shipped' THEN 1 END) AS shipped,
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) AS cancelled,
SUM(CASE WHEN status = 'shipped' THEN amount ELSE 0 END) AS shipped_value
FROM orders;
| все_ордеры | отправленный | отменен | отправленное_значение |
|---|---|---|---|
| 8 | 5 | 2 | 610 |
Один проход по столу, четыре ответа, и каждый из них уважает то, что WHERE пункт, который вы добавите позже. В этом заключается практическое преимущество перед тремя отдельными запросами: фильтры никогда не разойдутся.
ELSE, которое ломает это
COUNT(CASE WHEN status = 'shipped' THEN 1 END) -- 5, correct
COUNT(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END) -- 8, wrong
A CASE без совпадения WHEN и нет ELSE возвращает NULL и COUNT пропускает NULL. Писать ELSE 0 и каждая строка теперь дает ноль, который является значением, поэтому COUNT считает все восемь.
Никаких ошибок, никаких предупреждений, и число по-прежнему правдоподобно. Это наиболее распространенная ошибка при условном агрегировании, и она легко выдерживает проверку, поскольку ELSE 0 выглядит как хорошая оборонительная практика.
| написано | возвращает | Почему |
|---|---|---|
COUNT(CASE WHEN c THEN 1 END) | 5 | Несовпадающие строки имеют значение NULL и пропускаются. |
COUNT(CASE WHEN c THEN 1 ELSE 0 END) | 8 | 0 является значением и учитывается |
SUM(CASE WHEN c THEN 1 ELSE 0 END) | 5 | Добавляет по одному за матч |
SUM(CASE WHEN c THEN 1 END) | 5 | SUM также игнорирует NULL |
Трое из этих четырех правы. Рекомендуется выбрать одну форму и использовать ее последовательно, потому что тогда случайное ELSE выглядит неправильно с первого взгляда, вместо того, чтобы выглядеть внимательно.
ELSE.Цены в том же запросе
SELECT
COUNT(*) AS orders,
COUNT(CASE WHEN status = 'shipped' THEN 1 END) AS shipped,
ROUND(100.0 * COUNT(CASE WHEN status = 'shipped' THEN 1 END) / COUNT(*), 1)
AS shipped_pct
FROM orders;
-- 8, 5, 62.5
тот 100.0 имеет значение. В базах данных с целочисленным делением 5 / 8 равно 0, и скорость возвращается к нулю для каждой строки. Сначала умножьте на десятичную дробь или отбросьте одну сторону.
Берегите также знаменатель. COUNT(*) не может быть нулевым, если строка существует, но может быть условным знаменателем, а деление на ноль либо приводит к ошибке, либо возвращает NULL в зависимости от механизма. NULLIF(denominator, 0) делает поведение везде одинаковым.
В сочетании с GROUP BY
SELECT customer_id,
COUNT(*) AS orders,
COUNT(CASE WHEN status = 'shipped' THEN 1 END) AS shipped,
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) AS cancelled
FROM orders
GROUP BY customer_id
ORDER BY customer_id;
Это форма, которая делает условные подсчеты достойными изучения: одна строка на каждого клиента, несколько показателей по столбцам, отсутствие самосоединений или повторяющихся подзапросов. Это SQL-эквивалент сводной таблицы, и именно так на самом деле создается большинство таблиц отчетов.
Предложение FILTER, где оно у вас есть.
SELECT
COUNT(*) AS orders,
COUNT(*) FILTER (WHERE status = 'shipped') AS shipped,
SUM(amount) FILTER (WHERE status = 'shipped') AS shipped_value
FROM orders;
Стандартный SQL, поддерживаемый PostgreSQL и SQLite. Он говорит именно то, что означает, и не имеет никакого значения. ELSE ошибиться. SQL Server и MySQL его не поддерживают, поэтому CASE форма остается портативным ответом, и в любом случае стоит свободно владеть им.
Как применить это в своей работе
- Найдите отчет, построенный на основе нескольких почти идентичных запросов, и сверните его в один с условными агрегатами.
- Найдите в сохраненных запросах
COUNT(CASEи проверь каждого на наличиеELSE. - Добавьте проверку работоспособности: количество категорий должно суммироваться с общей суммой.
- Умножить на
100.0в каждой ставке и заключим знаменатели вNULLIF. - Использовать
FILTERгде ваша база данных поддерживает это, и отметьте в комментарии, почему другие запросы этого не поддерживают.
Единственная привычка, которую нужно сохранить
Спросите, что дает несовпадающая строка. NULL и пропускается, или 0 и засчитывается. Каждый условный агрегат в SQL сводится к этому вопросу, и ответ меняется в зависимости от функции, в которую вы его включили.
Есть ли в ваших отчетах условное количество, равное общему количеству строк?
SQL для аналитиков — это 458 страниц, которые считывают запросы построчно в повседневных словах, поэтому ошибочный результат нужно где-то отслеживать, а не перепечатывать, пока он не станет выглядеть лучше.
SQL для аналитиков, 19 $ →СЧЕТ в SQL охватывает саму функцию, Выражение CASE покрывает другую половину, и SQL-практикум передает вам один выполняемый запрос за раз.
Открыть SQL-детализацию →