Задачи на дедупликацию SQL на собеседовании

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

Зачем эти задачи

Дедупликация — одна из самых частых задач на собеседовании middle-аналитика. Вас попросят найти дубли по email, по (user_id, date), удалить лишнее, оставив самую свежую запись, слить «частичные дубликаты» с разными NULL. Знание ROW_NUMBER, GROUP BY + HAVING, DISTINCT ON и ON CONFLICT — обязательно.

На практике дубликаты встречаются везде: один пользователь зарегистрировался дважды, ETL отработал повторно и задвоил транзакции, приёмка файла от партнёра пришла с повторами. Если не умеете это чистить — ваши агрегаты врут в 2 раза.

Ниже — 12 задач с разборами, которые покрывают все типовые кейсы. Решайте по порядку: от «найти дубли по одной колонке» до «merge дубликатов с сохранением свежих значений».

Задача 1. Найти дубликаты по email

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

Задача 2. Оставить одну запись на email (последнюю)

SELECT * FROM (
    SELECT *,
        ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) AS rn
    FROM users
) t
WHERE rn = 1;

Задача 3. Удалить дубликаты из orders по (user_id, product_id, date)

DELETE FROM orders
WHERE order_id IN (
    SELECT order_id FROM (
        SELECT order_id,
            ROW_NUMBER() OVER (
                PARTITION BY user_id, product_id, DATE(created_at)
                ORDER BY created_at
            ) AS rn
        FROM orders
    ) t
    WHERE rn > 1
);

Задача 4. Полные дубли (все колонки одинаковые)

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

Задача 5. Процент дублей

SELECT
    COUNT(*) AS total,
    COUNT(DISTINCT email) AS unique_emails,
    ROUND(100.0 * (COUNT(*) - COUNT(DISTINCT email)) / COUNT(*), 2) AS dup_pct
FROM users;

Задача 6. Дубли с учётом case-insensitive

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

Задача 7. Последний заказ каждого пользователя (dedupe)

SELECT DISTINCT ON (user_id) *
FROM orders
ORDER BY user_id, created_at DESC;
-- Postgres

-- universal
SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
    FROM orders
) t WHERE rn = 1;

Задача 8. Merge дубликатов — взять самое свежее значение

WITH ranked AS (
    SELECT *,
        ROW_NUMBER() OVER (PARTITION BY email ORDER BY updated_at DESC) AS rn
    FROM users
)
SELECT email,
    MAX(CASE WHEN rn = 1 THEN name END) AS latest_name,
    MAX(CASE WHEN rn = 1 THEN phone END) AS latest_phone
FROM ranked
GROUP BY email;

Задача 9. Дубли по «нормализованному» ключу

Учесть пробелы и регистр:

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

Задача 10. CTAS без дубликатов

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

Задача 11. Подсчитать, сколько уникальных пользователей

SELECT COUNT(DISTINCT user_id) FROM events;

Задача 12. INSERT с проверкой дубликата

-- Postgres
INSERT INTO users (email, name)
VALUES ('alice@x.com', 'Alice')
ON CONFLICT (email) DO NOTHING;

-- или UPDATE при конфликте
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;

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

  • DELETE без LIMIT на большой таблице → долго и блокирует.
  • ROW_NUMBER без ORDER BY → недетерминистический dedupe.
  • SELECT DISTINCT не уберёт «почти дубликаты» (с пробелами, регистром).
  • Забыть про FK при DELETE — каскад может удалить больше.

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

FAQ

ROW_NUMBER или DISTINCT?

DISTINCT — если все колонки одинаковые. ROW_NUMBER — когда нужно выбрать одну из «почти дубликатов».

Performance?

Индекс на PARTITION BY колонку помогает. CREATE INDEX ON users(email).


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