Сопоставьте результаты двух агрегатов. В таблице есть строка с пропущенным значением: что вернёт запрос и почему?
WITH sales(amount) AS (
VALUES (100), (NULL), (250)
)
SELECT
COUNT(*) AS row_count,
COUNT(amount) AS value_count
FROM sales;
Запрос вернёт row_count = 3 и value_count = 2. COUNT(*) считает все строки, а COUNT(amount) — только строки, где вычисленное значение amount не равно NULL.
В реляционной модели отсутствие значения представляется специальным маркером NULL, который не является обычным значением типа. Поэтому SQL различает подсчёт строк и подсчёт имеющихся значений атрибута.
Такой подход нужен для отчётов, где важно отдельно видеть общий объём записей и количество строк с заполненными данными. Например, число заказов и число заказов, у которых указана дата оплаты, — это разные показатели.
Строка с NULL не исчезает из таблицы и учитывается в COUNT(*). Однако у неё нет значения amount, которое можно посчитать через COUNT(amount).
Неверный выбор агрегата может исказить отчёт: система покажет не количество продаж, а количество продаж с заполненной суммой. Особенно опасно это при расчёте долей, полноты данных и контрольных показателей загрузки.
В данном примере есть три строки: со значениями 100, NULL и 250. Поэтому COUNT(*) возвращает 3.
COUNT(amount) логически применяет выражение amount к каждой строке и учитывает только два ненулевых результата. Итог — 2.
Это правило относится не только к простому столбцу: COUNT(expression) также игнорирует строки, для которых выражение возвращает NULL. Например, COUNT(amount * 2) не посчитает строки с amount IS NULL.
Если нужно посчитать все строки, используют COUNT(*). Если нужно посчитать заполненные значения конкретного столбца или выражения, используют COUNT(column) либо COUNT(expression). Фильтр WHERE работает раньше агрегации и удаляет строки из входного набора целиком, тогда как COUNT(column) оставляет строки в наборе, но не учитывает их в конкретном счётчике.
В отчёте по загрузке заказов требовались два показателя: общее число заказов и число заказов с указанной суммой. Вариант с двумя COUNT(*) давал одинаковые значения и скрывал пропуски; вариант с фильтрацией по сумме правильно считал заполненные строки, но одновременно менял набор строк для других агрегатов.
Выбранное решение — использовать COUNT(*) и COUNT(amount) в одном запросе. Оно сохраняет общий набор заказов и позволяет одновременно измерять полноту заполнения суммы, поэтому отдельный запрос или повторную выборку выполнять не нужно.
Если показатель должен считать только строки, удовлетворяющие условию, применяют условное выражение, например COUNT(CASE WHEN amount > 0 THEN 1 END). Такой вариант обычно переносимее, чем диалектные конструкции, но условие нужно формулировать явно и учитывать, что результат CASE без сработавшей ветви может быть NULL.
COUNT(amount) считать нулевые значения?Да. Ноль — обычное значение, а не NULL, поэтому строка с amount = 0 будет учтена. COUNT(amount) исключает именно NULL, а не нули, пустые строки или отрицательные числа.
COUNT(*) и COUNT(column) вернут 0, потому что количество строк и ненулевых значений равно нулю. Это отличается, например, от некоторых других агрегатов: SUM и AVG обычно возвращают NULL, если входной набор пуст или все значения агрегируемого выражения равны NULL.
COUNT(column) от COUNT(DISTINCT column)?COUNT(column) считает все ненулевые значения, включая повторы. COUNT(DISTINCT column) сначала устраняет дубликаты ненулевых значений, а затем считает оставшиеся; NULL также не учитывается. Поэтому при значениях 10, 10, 20, NULL результаты будут соответственно 3 и 2.