Чем отличается подсчёт строк от подсчёта значений столбца, если столбец содержит NULL?

Чем отличается подсчёт строк от подсчёта значений столбца, если столбец содержит NULL?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

COUNT(*) считает все строки группы, включая строки со значением NULL. COUNT(выражение) считает только те строки, в которых результат выражения не равен NULL.

Исторический контекст

Реляционная модель оперирует отношениями, а SQL дополнительно поддерживает неполные данные через специальное значение NULL. Поэтому SQL должен различать количество строк и количество известных значений атрибута.

Агрегатные функции получили правила обработки NULL, чтобы пропуски не трактовались как обычные значения вроде нуля или пустой строки. Это позволяет отдельно считать физическую кардинальность результата и доступные для анализа значения.

Постановка проблемы

Если перепутать COUNT(*) и COUNT(column), отчёт может показать неверное количество данных. Например, три строки могут существовать в таблице, но только две из них содержать известную дату, сумму или идентификатор.

Особенно опасна такая ошибка в метриках заполненности: подсчёт столбца нельзя использовать как подсчёт строк, если столбец допускает NULL.

Подробное решение

COUNT(*) считает строки после применения условий запроса и формирования группы. Он не анализирует значения столбцов, поэтому строка с полностью неизвестными атрибутами всё равно учитывается.

COUNT(expression) вычисляет выражение для каждой строки и учитывает только результаты, отличные от NULL. Это относится не только к простому столбцу, но и к любому выражению, результат которого может стать NULL.

WITH data(value) AS ( VALUES (10), (NULL), (10) ) SELECT COUNT(*) AS rows_count, COUNT(value) AS known_count, COUNT(DISTINCT value) AS distinct_known_count FROM data;

Результат будет равен 3, 2 и 1 соответственно: всего три строки, два известных значения и одно уникальное известное значение. NULL не считается значением для COUNT(expression) и не попадает в COUNT(DISTINCT expression).

Для пустой группы COUNT возвращает ноль. Другие агрегаты, например SUM и AVG, обычно игнорируют NULL, но при отсутствии известных значений возвращают NULL, а не ноль; это нужно явно учитывать при вычислениях и выводе отчётов.

Ситуация из практики

Нужно вывести число заказов и число заказов с указанной стоимостью. Вариант с COUNT(price) короче, но он ошибочно выдаст меньшее число заказов, если стоимость ещё не рассчитана. Вариант с COUNT(*) корректно считает все заказы, а отдельный COUNT(price) показывает заполненность стоимости.

Можно заменить NULL на ноль перед суммированием, но это допустимо только если отсутствие стоимости действительно означает нулевую стоимость. Если NULL означает «данные неизвестны», такая замена скроет проблему качества данных; поэтому в отчёте обычно выводят оба показателя и явно обрабатывают NULL.

Что кандидаты часто упускают

  1. Вопрос: учитывает ли COUNT(expression) строку, если выражение содержит NULL только в одном из операндов?

    Ответ: нет, если результат всего выражения равен NULL. В обычной SQL-арифметике выражение с неизвестным операндом обычно становится неизвестным: например, сумма известного числа и NULL даёт NULL. Поэтому COUNT(price * quantity) не посчитает строку, если price или quantity равен NULL.

  2. Вопрос: почему SUM(column) может вернуть NULL, хотя в группе есть строки?

    Ответ: наличие строк и наличие известных значений — разные свойства. Если все значения столбца в группе равны NULL, агрегат SUM не получает ни одного известного операнда и возвращает NULL. Подмена результата на ноль через обработку NULL допустима только при явно выбранной бизнес-семантике.

  3. Вопрос: почему в отчёте по внешнему соединению COUNT(*) может показать один заказ у клиента без заказов?

    Ответ: внешнее соединение сохраняет строку клиента и добавляет искусственно дополненную строку со значениями правой стороны, равными NULL. COUNT(*) считает эту строку, тогда как COUNT(order_id) её не считает, если order_id находится на правой стороне и стал NULL. Для подсчёта реально найденных заказов обычно используют COUNT по гарантированно ненулевому атрибуту заказа.