← Назад к списку
ПрограммированиеBackendJunior

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

Короткий ответ

  • Поиск: GROUP BY email + HAVING COUNT(*) > 1
  • Удаление: оставить MIN(id) в каждой группе
  • Вариант через самосоединение или подзапрос с группировкой
  • В Postgres удобен DELETE ... USING
  • После чистки — уникальный индекс, чтобы дубли не вернулись
  • На проде сначала SELECT и бэкап, потом DELETE

Дубли ищутся группировкой с HAVING, удаляются с сохранением MIN(id), а рецидив закрывается уникальным индексом.

Как сказать вслух

пример ответа

Найти дубли просто: группирую по email и беру группы, где больше одной записи, — это условие идёт в HAVING, потому что оно на агрегат. Для удаления оставляю в каждой группе запись с минимальным id, а остальные удаляю — в Postgres это удобно писать через DELETE USING с условием «email совпадает, а id больше». И обязательный финальный шаг: повесить уникальный индекс на email, иначе дубли появятся снова.

Подробный ответ

Основной ответ

Первый запрос — агрегация: GROUP BY email с HAVING COUNT(*) > 1 возвращает сами значения и число повторов. Удаление строится на выборе «канонической» строки в группе — обычно MIN(id) или самая ранняя created_at. Способы: DELETE с подзапросом, где id NOT IN (SELECT MIN(id) ... GROUP BY email) — работает, но на больших таблицах медленный; самосоединение DELETE u1 USING users u2 WHERE u1.email = u2.email AND u1.id > u2.id — читабельно и быстро в PostgreSQL; через оконную функцию ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) с удалением rn > 1 — универсально. Завершить нужно созданием UNIQUE-ограничения на email; если в живой системе вставки продолжаются, чистку и создание индекса делают согласованно, иначе между ними успеют вставить новый дубль.

Ключевые моменты

  • HAVING для агрегата. COUNT(*) > 1 нельзя поставить в WHERE — агрегат вычисляется после группировки.
  • Критерий выжившей строки. MIN(id) — простой выбор, но бизнес может требовать оставить запись с последней активностью — критерий стоит уточнить.
  • Закрепить уникальность. Без UNIQUE-индекса чистка бессмысленна; в Postgres его можно строить CONCURRENTLY, чтобы не блокировать таблицу.

Практический контекст

Типичная задача при наведении порядка в данных перед добавлением ограничения уникальности — встречается в реальных миграциях постоянно. Интервьюер смотрит на владение GROUP BY/HAVING, аккуратность с DELETE (сначала проверить SELECT-ом, что попадает под удаление) и на то, вспомнит ли кандидат про уникальный индекс как закрепление результата — это признак продакшен-мышления.

Пример кода

-- 1. Найти дубликаты
SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

-- 2. Удалить, оставив самую раннюю запись (PostgreSQL)
DELETE FROM users u1
USING users u2
WHERE u1.email = u2.email
  AND u1.id > u2.id;

-- 3. Закрепить уникальность
CREATE UNIQUE INDEX CONCURRENTLY users_email_uniq ON users (email);

Частые ошибки

  • Пишут COUNT(*) > 1 в WHERE вместо HAVING
  • Удаляют все строки группы, включая ту, что надо оставить
  • Чистят дубли, но забывают про уникальный индекс — проблема возвращается

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru