Как найти дубликаты в SQL

Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.

Три основных подхода

  1. GROUP BY + HAVING — простейший способ
  2. ROW_NUMBER + подзапрос — удобно для вывода полных строк
  3. SELF JOIN — через соединение

1. Найти дубликаты по одной колонке

Таблица users(id, email, name).

Найти дублирующиеся email:

SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

Результат:

email                 | cnt
----------------------|----
alice@example.com     |  2
bob@example.com       |  3

2. Найти дубликаты по нескольким колонкам

Дубли по (first_name, last_name, birth_date):

SELECT first_name, last_name, birth_date, COUNT(*) AS cnt
FROM users
GROUP BY first_name, last_name, birth_date
HAVING COUNT(*) > 1;

3. Вывести полные строки дубликатов

Нужны все поля, не только агрегаты:

Через ROW_NUMBER (самый чистый способ)

SELECT *
FROM (
    SELECT
        *,
        ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn,
        COUNT(*) OVER (PARTITION BY email) AS cnt
    FROM users
) t
WHERE cnt > 1;

Возвращает все строки, где email встречается >1 раза.

Через подзапрос + IN

SELECT * FROM users
WHERE email IN (
    SELECT email FROM users
    GROUP BY email
    HAVING COUNT(*) > 1
);

Через EXISTS

SELECT u.* FROM users u
WHERE EXISTS (
    SELECT 1 FROM users u2
    WHERE u2.email = u.email AND u2.id != u.id
);

4. Полные дубликаты строки (все поля одинаковые)

Найти строки, которые дублируются полностью:

SELECT *, COUNT(*) OVER (PARTITION BY col1, col2, col3, ...) AS cnt
FROM my_table
WHERE cnt > 1;

Или через CTE и список всех колонок:

WITH dupes AS (
    SELECT col1, col2, col3, COUNT(*) AS cnt
    FROM my_table
    GROUP BY col1, col2, col3
    HAVING COUNT(*) > 1
)
SELECT t.*
FROM my_table t
JOIN dupes d ON (t.col1 = d.col1 AND t.col2 = d.col2 AND t.col3 = d.col3);

5. Только вторые и последующие вхождения (первый не считать)

SELECT *
FROM (
    SELECT
        *,
        ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
    FROM users
) t
WHERE rn > 1;

rn = 1 — первое вхождение. rn > 1 — последующие (дубликаты).

6. Примеры из практики

Найти дубли заказов по (user_id, product_id, created_date)

Возможно, при сбое запроса создались два заказа:

SELECT user_id, product_id, DATE(created_at) AS d, COUNT(*) AS cnt
FROM orders
GROUP BY user_id, product_id, DATE(created_at)
HAVING COUNT(*) > 1;

Найти дубли event-ов от трекера

Двойные события (клик 2 раза подряд за <1 сек):

WITH numbered AS (
    SELECT
        *,
        LAG(event_at) OVER (PARTITION BY user_id, event_type ORDER BY event_at) AS prev_at
    FROM events
)
SELECT *
FROM numbered
WHERE event_at - prev_at < INTERVAL '1 second';

Найти пользователей, зарегистрировавшихся с одного email

SELECT email, COUNT(DISTINCT user_id) AS uniq_users
FROM users
GROUP BY email
HAVING COUNT(DISTINCT user_id) > 1;

7. Дубликаты с учётом case-insensitive

Email Alice@example.comalice@example.com в строгом сравнении. Но логически — один:

SELECT LOWER(email), COUNT(*) AS cnt
FROM users
GROUP BY LOWER(email)
HAVING COUNT(*) > 1;

8. Дубликаты с учётом trim / whitespace

Иногда дубликаты скрыты пробелами: 'Alice' и 'Alice ':

SELECT TRIM(name), COUNT(*) AS cnt
FROM users
GROUP BY TRIM(name)
HAVING COUNT(*) > 1;

9. Посчитать сколько дубликатов всего

SELECT
    COUNT(*) AS total_rows,
    COUNT(DISTINCT email) AS unique_emails,
    COUNT(*) - COUNT(DISTINCT email) AS extra_duplicates
FROM users;

10. Найти «самые частые» дубликаты

SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1
ORDER BY cnt DESC
LIMIT 10;

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

Ошибка 1. Забыть HAVING

-- вернёт ВСЕ группы, не только дубли
SELECT email, COUNT(*) FROM users GROUP BY email;

-- правильно
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;

Ошибка 2. Не учитывать NULL

Если email IS NULL, GROUP BY группирует NULL вместе:

-- 10 пользователей с NULL email → попадут в «дубликаты»
GROUP BY email HAVING COUNT(*) > 1

Фильтруйте:

SELECT email, COUNT(*)
FROM users
WHERE email IS NOT NULL
GROUP BY email
HAVING COUNT(*) > 1;

Ошибка 3. Дубликаты по case / пробелам

Данные часто неопрятные. Нормализуйте перед сравнением: LOWER(TRIM(col)).

Ошибка 4. SELECT DISTINCT вместо GROUP BY

-- не скажет, сколько дублей
SELECT DISTINCT email FROM users;

-- даст и email, и количество
SELECT email, COUNT(*) FROM users GROUP BY email;

Связанные темы

FAQ

Как быстрее найти дубликаты?

На индексированной колонке — любой из способов быстрый. Без индекса — GROUP BY более-менее стабильный.

GROUP BY или ROW_NUMBER?

GROUP BY + HAVING — чтобы увидеть, сколько дублей. ROW_NUMBER — чтобы получить полные строки дубликатов.

Как найти дубликаты в нескольких таблицах?

UNION ALL двух таблиц, потом GROUP BY.

Что делать после того, как нашли дубликаты?

Разобраться с причиной (bug в приложении, ETL-дубль). Потом удалить → см. как удалить дубликаты.


Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.