БэкендData и ИИSQL

SQL COUNT для начинающих: посчитать строки, уникальные значения и не ошибиться с NULL

Разбираем COUNT(*), COUNT(column), COUNT(DISTINCT), условия WHERE и группировку на маленькой таблице заказов. Показываем результаты и типичные ошибки.

Кодик

Автор

5 мин чтения

COUNT(*) считает строки, COUNT(column) считает только строки с непустым значением в колонке, а COUNT(DISTINCT column) оставляет уникальные непустые значения. Эти три формы дают разные числа на одной таблице. Ниже вы создадите пять заказов, получите результаты 5, 4 и 3, а затем добавите условия и группировку.

Ошибка новичка редко находится в синтаксисе COUNT. Запрос выполняется, но отвечает не на тот вопрос. Например, в таблице пять заказов, один email равен NULL и два покупателя повторяются. Подсчёт всех строк, заполненных email и уникальных email должен дать три разных результата. Сначала сформулируйте единицу счёта словами, затем выбирайте выражение.

1Определяем строку

COUNT(*) отвечает, сколько записей прошло фильтр.

2Выбираем поле

COUNT(email) пропускает строки, где email равен NULL.

3Убираем повторы

COUNT(DISTINCT email) считает разные непустые email.

Чем отличаются COUNT(*), COUNT(column) и DISTINCT

Звёздочка в COUNT(*) не означает все колонки. Она означает все строки входного набора. Конкретная колонка меняет правило: NULL не учитывается. DISTINCT сначала убирает повторы выражения, а затем считает оставшиеся непустые значения. На таблице заказов это различие видно сразу.

ВыражениеЧто считаетРезультат примера
COUNT(*)Все пять строк5
COUNT(email)Строки с email не NULL4
COUNT(DISTINCT email)Разные непустые email3
COUNT(*) FILTER (...)Строки по условию в PostgreSQLЗависит от условия

Сравнение COUNT всех строк непустых email и уникальных email
Одна таблица даёт 5, 4 и 3, потому что формы COUNT отвечают на разные вопросы.

NULL не равен пустой строке. COUNT(email) пропустит NULL, но посчитает ''. Если пустая строка тоже означает отсутствие email, очистите данные или добавьте отдельное условие.

Запускаем три подсчёта на одной таблице

В примере CTE orders живёт только внутри запроса, поэтому можно экспериментировать без создания постоянной таблицы. Пять строк содержат четыре заполненных email и три разных email. Запустите запрос целиком и сравните названия колонок с полученными числами.

WITH orders(id, email, status, amount) AS (
  VALUES
    (1, 'ann@example.com',  'paid',    1200),
    (2, 'bob@example.com',  'paid',     800),
    (3, 'ann@example.com',  'new',      500),
    (4, NULL,               'cancelled', 0),
    (5, 'cat@example.com',  'paid',    1500)
)
SELECT
  COUNT(*)              AS all_orders,
  COUNT(email)          AS orders_with_email,
  COUNT(DISTINCT email) AS unique_emails
FROM orders;

-- Результат:
-- all_orders | orders_with_email | unique_emails
--      5     |         4         |       3
Почему получились разные числа
  • COUNT(*) = 5, потому что CTE содержит пять строк независимо от значений.
  • COUNT(email) = 4, потому что у заказа 4 email равен NULL.
  • COUNT(DISTINCT email) = 3, потому что email Ann встречается дважды.

Разбор SQL запроса COUNT по этапам фильтрации и подсчёта
Строки сначала проходят FROM и WHERE, затем попадают в агрегат.

Добавляем WHERE, GROUP BY и условный подсчёт

WHERE отбирает строки до агрегирования. GROUP BY делит их на группы, и COUNT запускается отдельно для каждой группы. Если нужны несколько счётчиков в одной строке, PostgreSQL поддерживает FILTER, а переносимый вариант строится через CASE. Главное: условие должно относиться к тем строкам, которые вы действительно хотите считать.

ВопросЗапросЧто вернётся
Сколько оплаченных?COUNT(*) ... WHERE status = 'paid'Одна строка: 3
Сколько каждого статуса?GROUP BY statusПо строке на статус
Сколько дорогих?COUNT(*) FILTER (WHERE amount >= 1000)Одна колонка: 2
Сколько уникальных покупателей оплатили?COUNT(DISTINCT email) WHERE status = 'paid'Три email
Сначала фильтр, потом подсчёт. Фраза «сколько оплаченных заказов» переводится в набор оплаченных строк и только затем в COUNT(*). Такой перевод защищает от случайного счёта колонки.

Практика: строим мини-сводку заказов

Сделайте один запрос, который возвращает статус, число заказов, число известных покупателей и сумму. Затем отсортируйте группы по количеству. В этом упражнении каждое выражение отвечает на отдельный вопрос, поэтому неправильное число легко заметить вручную.

Пять проверок для COUNT
  1. Скопируйте CTE orders из примера и выполните его в редакторе SQL.
  2. Добавьте GROUP BY status и выведите status, COUNT(*).
  3. Рядом добавьте COUNT(email) и посмотрите, в какой группе пропал NULL.
  4. Добавьте COUNT(DISTINCT email), чтобы повторы покупателя не увеличивали число людей.
  5. Выведите SUM(amount) и отсортируйте группы через ORDER BY COUNT(*) DESC.
  6. Сложите числа групп и сверьте с общим количеством пяти строк.
Ожидаемая сводка
  • Статус paid содержит 3 заказа, new содержит 1, cancelled содержит 1.
  • В cancelled один заказ, но ноль известных email, потому что значение равно NULL.
  • Сумма количества по группам равна общему числу строк, значит ни одна запись не потерялась.

Карта диагностики неожиданного результата COUNT в SQL
Неожиданное число чаще рождается в NULL, DISTINCT или JOIN, а не в самом COUNT.

Почему COUNT возвращает правильное, но неожиданное число

Если число кажется неверным, временно уберите агрегат и выведите сами строки с тем же FROM, JOIN и WHERE. Частая причина не в COUNT, а в соединении, которое размножило записи. Вторая причина: считалась nullable-колонка, хотя вопрос относился к строкам. Третья: DISTINCT замаскировал плохой JOIN.

Считается ID после LEFT JOIN

COUNT(right_table.id) считает совпавшие строки справа, а COUNT(*) считает все строки результата. Это разные вопросы.

DISTINCT добавлен наугад

Он может убрать законные повторы или скрыть размножение строк. Сначала выведите пары ключей и найдите источник дублей.

NULL путают с нулём

Нулевая сумма является значением и считается. NULL означает отсутствие значения и пропускается формой COUNT(column).

HAVING заменяют на WHERE

WHERE фильтрует строки до группировки, HAVING фильтрует уже полученные группы.

Покажите входные строки. Перед сложным подсчётом выполните тот же запрос без агрегатов. Если входной набор уже содержит лишние или потерянные строки, COUNT лишь честно посчитает ошибку выше по цепочке. Технические детали сверены с документацией PostgreSQL по агрегатам.
Короткие ответы
Что считает COUNT(*)?

Количество строк, которые дошли до агрегирования.

Почему COUNT(email) меньше COUNT(*)?

Строки с email равным NULL не входят в COUNT(email).

Что делает COUNT(DISTINCT email)?

Считает разные непустые значения email.

Когда нужен GROUP BY?

Когда требуется отдельный счётчик для каждого статуса, города или другой группы.

Как проверить странный COUNT после JOIN?

Вывести строки и ключи без агрегата, чтобы увидеть дубли или потери.

Превратите один счётчик в небольшой отчёт

В курсе SQL вы сможете сразу выполнить запросы и менять данные. Перед группировкой освежите первые SELECT и WHERE.

Затем решите 10 задач по SQL и держите рядом справочник по основным операциям. Один COUNT быстро превращается в сводку, которую уже можно показать в проекте.