← все задачи

SQL · задача 3 из 10

Найти и удалить дубликаты

Средний 15 минут GROUP BY / HAVINGctidудаление

Условие

В таблице users по ошибке появились дубликаты по email. Сначала покажите все дублирующиеся email с числом копий, затем удалите лишние строки, оставив самую раннюю по id.

Что требуется

  • Сначала отчёт, только потом удаление
  • Оставить ровно одну запись на email — с минимальным id
  • Запрос не должен зависеть от числа дубликатов

Пример

id  email
1   a@x.ru
2   b@x.ru
3   a@x.ru   <- удалить
7   a@x.ru   <- удалить

Сначала уточните

Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.

  • Дубликат по одному полю или по набору полей?
  • Какую копию оставляем: самую раннюю, самую свежую или заполненную полнее?
  • Есть ли внешние ключи на эти строки — тогда удаление затронет связанные данные
Показать решение Скрыть решение

Решение

-- 1. Отчёт: какие email задублировались и сколько раз
SELECT email, COUNT(*) AS copies, MIN(id) AS keep_id
FROM users
GROUP BY email
HAVING COUNT(*) > 1
ORDER BY copies DESC;

-- 2. Удаление лишних: оставляем строку с минимальным id
DELETE FROM users u
WHERE u.id > (
    SELECT MIN(keep.id)
    FROM users keep
    WHERE keep.email = u.email
);

-- 3. Чтобы не повторилось
CREATE UNIQUE INDEX CONCURRENTLY users_email_key ON users (email);

Почему так

Почему HAVING, а не WHERE

  • WHERE фильтрует строки до группировки, а COUNT(*) появляется только после неё
  • HAVING — это фильтр по результату агрегации, поэтому условие на количество живёт именно там
  • Вопрос «в чём разница WHERE и HAVING» задают почти всегда следом

Почему сначала отчёт, потом удаление

  • DELETE по живым данным без предварительного просмотра — это способ потерять данные молча
  • Отчёт показывает масштаб: пять дублей или пять миллионов — это разные операции и разные окна обслуживания
  • На собеседовании эта осторожность читается как опыт: сначала SELECT, потом DELETE, и всё в транзакции

Почему в конце уникальный индекс

  • Чистка без ограничения означает, что через неделю задача повторится: причина не устранена
  • Уникальный индекс превращает проблему данных в явную ошибку записи — сразу и в нужном месте
  • CONCURRENTLY в Postgres строит индекс без долгой блокировки таблицы — на большой таблице это принципиально

Что спросят дальше

  • Спросят про регистр: A@x.ru и a@x.ru — формально разные, для email обычно нужен lower(email)
  • Спросят про вариант с ROW_NUMBER() и DELETE по ctid — он гибче, когда «какую оставить» сложнее, чем MIN(id)
  • Спросят про внешние ключи: связанные строки нужно сначала перевесить на оставшуюся запись

Следующая задача

Клиенты без заказов — Поиск того, чего нет: три способа написать и вопрос, чем они отличаются.