COUNT DISTINCT: что именно мы считаем?

Девять посещений, восемь известных событий и четыре клиента. Все три числа правильные — но отвечают на разные вопросы.

SQL Studio / Разборы SQL / COUNT и NULL

Главное различие: COUNT(*) считает строки. COUNT(column) считает строки, где выбранный столбец не равен NULL. COUNT(DISTINCT column) считает уникальные ненулевые значения этого столбца. Это три разных показателя.

1. Один SQL-запрос — три разных числа

В учебной базе sessions находится девять посещений. Одно посещение анонимное (customer_id IS NULL), а известные клиенты приходили несколько раз. Выполним:

SELECT
  COUNT(*) AS sessions_n,
  COUNT(customer_id) AS known_sessions_n,
  COUNT(DISTINCT customer_id) AS customers_n
FROM sessions;
Результат на SQL Studio
sessions_nknown_sessions_ncustomers_n
984

Девять — все события. Восемь — события с известным клиентом. Четыре — количество уникальных известных клиентов. Если вам нужен охват людей, сумма событий не подходит; если нужна нагрузка на систему, число людей может быть недостаточным.

2. Почему COUNT(*) при LEFT JOIN иногда даёт единицу вместо нуля

Создадим маленький пример: три клиента, у третьего нет посещений. LEFT JOIN всё равно вернёт строку клиента №3, заполнив правые поля NULL.

WITH c(customer_id) AS (
  VALUES (1), (2), (3)
),
s(session_id, customer_id) AS (
  VALUES (10, 1), (11, 1), (20, 2)
)
SELECT c.customer_id,
       COUNT(*) AS joined_rows,
       COUNT(s.session_id) AS sessions_n
FROM c
LEFT JOIN s ON s.customer_id = c.customer_id
GROUP BY c.customer_id
ORDER BY c.customer_id;
Что произошло с клиентом без посещений
customer_idjoined_rowssessions_n
122
211
310

При подсчёте посещений используйте COUNT(s.session_id), где session_id у существующего события не бывает пустым. Иначе клиент без посещений получит фиктивную единицу.

3. JOIN умножил строки — а сколько заказов было на самом деле?

В SQL Studio один заказ может иметь несколько попыток оплаты. Если соединить orders и payments, на один заказ получится несколько строк:

SELECT
  COUNT(*) AS joined_rows,
  COUNT(DISTINCT o.order_id) AS orders_n
FROM orders AS o
JOIN payments AS p ON p.order_id = o.order_id;

Результат — 8 строк после соединения и 6 уникальных заказов. COUNT(DISTINCT o.order_id) полезен для диагностики, но не исправляет неверную сумму денег: SUM(o.gross_kopecks) после такого JOIN всё ещё завысит итог. Для этого нужна другая структура запроса.

4. Уникальные клиенты по каналам

SELECT channel,
       COUNT(*) AS sessions_n,
       COUNT(DISTINCT customer_id) AS customers_n
FROM sessions
GROUP BY channel
ORDER BY sessions_n DESC, channel;

Этот запрос возвращает посещения и уникальных клиентов в каждом канале отдельно. Клиент, посещавший сайт через email и organic, будет учтён в обоих каналах. Поэтому сумму уникальных клиентов по каналам нельзя автоматически считать общим количеством уникальных клиентов сайта.

Попробуйте на практике

В тренажёре есть задания на уникальные города и количество сессий у каждого клиента. Начните с них, а затем проверьте свой вариант COUNT DISTINCT.

Связанные разборы: как искать дубликаты · почему JOIN увеличивает суммы · GROUP BY и HAVING.


Для последовательной практики на MySQL 8 — курс «SQL для аналитика» на Stepik с итоговой работой и возможностью получить сертификат.