← все задачи
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)
- Спросят про внешние ключи: связанные строки нужно сначала перевесить на оставшуюся запись
Следующая задача
Клиенты без заказов — Поиск того, чего нет: три способа написать и вопрос, чем они отличаются.