COUNT(*) считает строки, COUNT(column) считает только строки с непустым значением в колонке, а COUNT(DISTINCT column) оставляет уникальные непустые значения. Эти три формы дают разные числа на одной таблице. Ниже вы создадите пять заказов, получите результаты 5, 4 и 3, а затем добавите условия и группировку.
Ошибка новичка редко находится в синтаксисе COUNT. Запрос выполняется, но отвечает не на тот вопрос. Например, в таблице пять заказов, один email равен NULL и два покупателя повторяются. Подсчёт всех строк, заполненных email и уникальных email должен дать три разных результата. Сначала сформулируйте единицу счёта словами, затем выбирайте выражение.
COUNT(*) отвечает, сколько записей прошло фильтр.
COUNT(email) пропускает строки, где email равен NULL.
COUNT(DISTINCT email) считает разные непустые email.
Чем отличаются COUNT(*), COUNT(column) и DISTINCT
Звёздочка в COUNT(*) не означает все колонки. Она означает все строки входного набора. Конкретная колонка меняет правило: NULL не учитывается. DISTINCT сначала убирает повторы выражения, а затем считает оставшиеся непустые значения. На таблице заказов это различие видно сразу.
| Выражение | Что считает | Результат примера |
|---|---|---|
COUNT(*) | Все пять строк | 5 |
COUNT(email) | Строки с email не NULL | 4 |
COUNT(DISTINCT email) | Разные непустые email | 3 |
COUNT(*) FILTER (...) | Строки по условию в PostgreSQL | Зависит от условия |

Одна таблица даёт 5, 4 и 3, потому что формы COUNT отвечают на разные вопросы.
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 | 3COUNT(*) = 5, потому что CTE содержит пять строк независимо от значений.COUNT(email) = 4, потому что у заказа 4 email равен NULL.COUNT(DISTINCT email) = 3, потому что email Ann встречается дважды.

Строки сначала проходят 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(*). Такой перевод защищает от случайного счёта колонки.Практика: строим мини-сводку заказов
Сделайте один запрос, который возвращает статус, число заказов, число известных покупателей и сумму. Затем отсортируйте группы по количеству. В этом упражнении каждое выражение отвечает на отдельный вопрос, поэтому неправильное число легко заметить вручную.
- Скопируйте CTE
ordersиз примера и выполните его в редакторе SQL. - Добавьте
GROUP BY statusи выведитеstatus, COUNT(*). - Рядом добавьте
COUNT(email)и посмотрите, в какой группе пропал NULL. - Добавьте
COUNT(DISTINCT email), чтобы повторы покупателя не увеличивали число людей. - Выведите
SUM(amount)и отсортируйте группы черезORDER BY COUNT(*) DESC. - Сложите числа групп и сверьте с общим количеством пяти строк.
- Статус paid содержит 3 заказа, new содержит 1, cancelled содержит 1.
- В cancelled один заказ, но ноль известных email, потому что значение равно NULL.
- Сумма количества по группам равна общему числу строк, значит ни одна запись не потерялась.

Неожиданное число чаще рождается в NULL, DISTINCT или JOIN, а не в самом COUNT.
Почему COUNT возвращает правильное, но неожиданное число
Если число кажется неверным, временно уберите агрегат и выведите сами строки с тем же FROM, JOIN и WHERE. Частая причина не в COUNT, а в соединении, которое размножило записи. Вторая причина: считалась nullable-колонка, хотя вопрос относился к строкам. Третья: DISTINCT замаскировал плохой JOIN.
COUNT(right_table.id) считает совпавшие строки справа, а COUNT(*) считает все строки результата. Это разные вопросы.
Он может убрать законные повторы или скрыть размножение строк. Сначала выведите пары ключей и найдите источник дублей.
Нулевая сумма является значением и считается. NULL означает отсутствие значения и пропускается формой COUNT(column).
WHERE фильтрует строки до группировки, HAVING фильтрует уже полученные группы.
Что считает COUNT(*)?
Количество строк, которые дошли до агрегирования.
Почему COUNT(email) меньше COUNT(*)?
Строки с email равным NULL не входят в COUNT(email).
Что делает COUNT(DISTINCT email)?
Считает разные непустые значения email.
Когда нужен GROUP BY?
Когда требуется отдельный счётчик для каждого статуса, города или другой группы.
Как проверить странный COUNT после JOIN?
Вывести строки и ключи без агрегата, чтобы увидеть дубли или потери.
В курсе SQL вы сможете сразу выполнить запросы и менять данные. Перед группировкой освежите первые SELECT и WHERE.
Затем решите 10 задач по SQL и держите рядом справочник по основным операциям. Один COUNT быстро превращается в сводку, которую уже можно показать в проекте.