DISTINCT и дедупликация в SQL на собеседовании

Почему дедупликация важна для аналитика

Дубликаты в данных — повседневная реальность. Двойные события из-за повторной отправки, дубли записей после неудачной миграции, несколько строк на пользователя в сводной таблице. Аналитик, который не умеет находить и устранять дубликаты, получает неправильные метрики — и принимает решения на основе ложных данных.

На собеседовании дедупликация проверяется в двух форматах: прямой вопрос «как убрать дубликаты» и скрытая ловушка — задача, где без дедупликации результат будет неверным, но ошибка не очевидна.

Каждый третий баг в аналитических отчётах связан с дубликатами. Если вы не проверяете данные на дубли перед агрегацией — метрики врут. Интервьюеры это знают и проверяют.

DISTINCT: базовый инструмент

SELECT DISTINCT убирает дубликаты из результата по всем колонкам. Строки считаются дубликатами, если все значения совпадают. Это самый простой способ получить уникальные записи.

Важный нюанс: DISTINCT применяется ко всей строке, а не к одной колонке. Запрос SELECT DISTINCT city, name FROM users вернёт уникальные пары (city, name), а не уникальные города. Это частая ошибка на собеседовании.

COUNT(DISTINCT column) — подсчёт уникальных значений в одной колонке. COUNT(DISTINCT user_id) — количество уникальных пользователей. Можно использовать внутри агрегаций: GROUP BY date с COUNT(DISTINCT user_id) — DAU по дням.

Ограничение: в стандартном SQL нельзя написать COUNT(DISTINCT col1, col2). Для подсчёта уникальных пар нужен подзапрос с DISTINCT или конкатенация колонок.

DISTINCT ON: мощный инструмент PostgreSQL

DISTINCT ON — расширение PostgreSQL, которого нет в MySQL или BigQuery. Он выбирает одну строку для каждого уникального значения указанной колонки: SELECT DISTINCT ON (user_id) * FROM orders ORDER BY user_id, created_at DESC вернёт последний заказ каждого пользователя.

Ключевое правило: ORDER BY должен начинаться с колонок из DISTINCT ON. Это требование PostgreSQL — без него результат непредсказуем. На собеседовании DISTINCT ON показывает, что вы работали с PostgreSQL на практике.

Дедупликация через ROW_NUMBER

Универсальный подход, работающий в любой СУБД: пронумеровать строки внутри группы и отфильтровать первую. Паттерн:

WITH ranked AS (
  SELECT *, ROW_NUMBER() OVER (
    PARTITION BY user_id ORDER BY created_at DESC
  ) AS rn
  FROM orders
)
SELECT * FROM ranked WHERE rn = 1

Этот подход гибче DISTINCT ON: можно выбирать не только первую, но и любую строку по номеру, использовать сложные условия сортировки, применять разные стратегии для разных групп. На собеседовании интервьюер может попросить сравнить оба варианта: DISTINCT ON короче и быстрее в PostgreSQL, ROW_NUMBER универсальнее и гибче.

Типичные ловушки

  • DISTINCT вместо GROUP BY — SELECT DISTINCT department, COUNT(*) не работает. DISTINCT не группирует, он убирает дубликаты из результата. Для агрегации нужен GROUP BY.
  • Дубликаты при JOIN — LEFT JOIN к таблице с дубликатами ключа размножает строки. Дедупликацию нужно делать до JOIN, а не после — иначе агрегаты будут завышены.
  • COUNT(DISTINCT) с NULL — NULL не учитывается в COUNT(DISTINCT). Если в колонке есть пропуски, COUNT(DISTINCT) покажет на одно значение меньше, чем вы ожидаете.
  • DISTINCT и ORDER BY — в некоторых СУБД ORDER BY должен содержать только колонки из SELECT DISTINCT. Иначе — ошибка.

Правило для собеседования: если в задаче есть JOIN — подумайте о дубликатах. Если считаете пользователей — используйте COUNT(DISTINCT). Если нужна «последняя запись для каждого» — ROW_NUMBER.

Типичные задачи

Задача 1: Найти пользователей с дубликатами. Подход — GROUP BY user_id, email с HAVING COUNT(*) > 1. Простая задача, но интервьюер может попросить оставить только одну строку из каждой группы дублей.

Задача 2: Последняя активность каждого пользователя. Подход — ROW_NUMBER с PARTITION BY user_id ORDER BY activity_date DESC, затем фильтрация rn = 1. Альтернатива в PostgreSQL — DISTINCT ON.

Задача 3: Уникальные посетители по дням с нарастающим итогом. Подход — COUNT(DISTINCT user_id) по дням даёт DAU, но нарастающий итог уникальных пользователей требует подзапроса: для каждого дня считаем пользователей, которые пришли впервые до этого дня включительно.

FAQ

Чем DISTINCT отличается от GROUP BY без агрегатов?

Функционально — результат одинаковый. GROUP BY без агрегатных функций работает как DISTINCT. Но семантически GROUP BY предназначен для агрегации, DISTINCT — для получения уникальных строк. На собеседовании используйте инструмент по назначению.

Когда ROW_NUMBER лучше DISTINCT?

Когда нужно выбрать конкретную строку из группы дубликатов — например, самую свежую запись или запись с максимальной суммой. DISTINCT не даёт контроля над тем, какая строка останется. ROW_NUMBER позволяет задать порядок и выбрать нужную. Подробнее — в статье про оконные функции.

Как дедуплицировать по нескольким колонкам?

DISTINCT работает по всем колонкам в SELECT автоматически. ROW_NUMBER позволяет указать несколько колонок в PARTITION BY. Для подсчёта уникальных комбинаций используйте подзапрос: SELECT COUNT(*) FROM (SELECT DISTINCT col1, col2 FROM table) sub.

Смотрите также