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.