SQL Studio / Разборы SQL / Как найти дубликаты в SQL — и не удалить нужные данные
Короткий ответ: чтобы найти повторяющиеся значения ключа, сгруппируйте строки и оставьте группы с COUNT(*) > 1. Но сначала определите, что именно должно быть уникальным: номер заказа, платёжная попытка или вся запись. Разные определения дадут разные «дубликаты».
1. Найти повторяющиеся значения через GROUP BY и HAVING
В учебной базе SQL Studio таблица payments хранит попытки оплаты. Один заказ может иметь неудачную и успешную попытки, поэтому order_id не уникален — и это нормально.
SELECT order_id, COUNT(*) AS attempts_n
FROM payments
GROUP BY order_id
HAVING COUNT(*) > 1
ORDER BY attempts_n DESC, order_id;
На маленьком наборе тренажёра запрос вернёт две группы: order_id = 201 и order_id = 206, по две попытки в каждой. Это повторения номера заказа, а не обязательно ошибочные записи.
| order_id | attempts_n |
|---|---|
| 201 | 2 |
| 206 | 2 |
Если уникальность задаётся сочетанием столбцов, группируйте именно по нему: например, по customer_id и дате события. HAVING фильтрует уже сформированные группы; WHERE COUNT(*) > 1 здесь работать не будет.
2. Увидеть сами повторяющиеся строки
Первый запрос нашёл проблемные ключи, но не показал сами события. Выведем все попытки этих заказов:
SELECT payment_id, order_id, status, amount_kopecks
FROM payments
WHERE order_id IN (
SELECT order_id
FROM payments
GROUP BY order_id
HAVING COUNT(*) > 1
)
ORDER BY order_id, payment_id;
| payment_id | order_id | status | amount_kopecks |
|---|---|---|---|
| 401 | 201 | failed | 50000 |
| 402 | 201 | captured | 50000 |
Если удалить одну из этих строк «для очистки дубликатов», можно потерять историю отказа. В платёжных событиях это существенная потеря информации.
3. Проверить, не создал ли дубликаты JOIN
Таблица orders хранит одну строку на заказ, а payments — несколько попыток на заказ. После соединения заказ 201 встречается дважды. Проверить размножение строк можно без удаления данных:
SELECT o.order_id, COUNT(*) AS joined_rows
FROM orders AS o
JOIN payments AS p ON p.order_id = o.order_id
GROUP BY o.order_id
HAVING COUNT(*) > 1
ORDER BY o.order_id;
Для финансового отчёта это опасно. Если сложить все суммы попыток одного заказа, неудачная попытка тоже попадёт в итог:
SELECT
SUM(amount_kopecks) / 100.0 AS all_attempts_rub,
SUM(CASE WHEN status = 'captured'
THEN amount_kopecks ELSE 0 END) / 100.0 AS captured_rub
FROM payments
WHERE order_id = 201;
На заказе 201 получим 1 000 ₽ по всем попыткам и 500 ₽ по операциям captured. Для итогового платежного показателя нужны правильные статусы и отдельная проверка возвратов, а не слепой DISTINCT.
4. Когда использовать ROW_NUMBER
Иногда для отчёта действительно нужна одна предпочтительная запись на заказ. В примере ниже выбираем сначала успешную попытку, если она есть, затем попытку с наибольшим ID. Это заданное правило выбора, а не утверждение, что ID отражает время события.
WITH chosen AS (
SELECT payment_id, order_id, status,
ROW_NUMBER() OVER (
PARTITION BY order_id
ORDER BY CASE WHEN status = 'captured' THEN 0 ELSE 1 END,
payment_id DESC
) AS rn
FROM payments
)
SELECT order_id, payment_id, status
FROM chosen
WHERE rn = 1
ORDER BY order_id;
PARTITION BY order_id создаёт отдельную нумерацию внутри каждого заказа. Условие rn = 1 оставляет по одной выбранной строке на группу. Сами исходные записи не удаляются.
5. Почему SELECT DISTINCT не заменяет диагностику
SELECT DISTINCT order_id
FROM payments
ORDER BY order_id;
Здесь DISTINCT корректно выводит уникальные номера заказов. Но если добавить в SELECT разные payment_id, обе попытки снова появятся. DISTINCT убирает одинаковые строки результата, а не решает вопрос о том, какие записи бизнес считает повторными.
Попробуйте на реальных заданиях
В SQL Studio разберите оплаченные попытки и запросы с несколькими таблицами. Бесплатные задания проверяются автоматически; описанный здесь выбор записи можно отдельно выполнить в песочнице.
Для последовательной практики на MySQL 8 — курс «SQL для аналитика» на Stepik с итоговой работой и возможностью получить сертификат.