Как найти дубликаты в SQL — и не удалить нужные данные

Две строки с одним order_id — ошибка или две попытки оплаты? Покажем, как отличить повтор ключа от настоящего дубля и почему DISTINCT иногда маскирует проблему.

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, по две попытки в каждой. Это повторения номера заказа, а не обязательно ошибочные записи.

Результат на учебных данных SQL Studio
order_idattempts_n
2012
2062

Если уникальность задаётся сочетанием столбцов, группируйте именно по нему: например, по 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;
Часть результата: заказ 201
payment_idorder_idstatusamount_kopecks
401201failed50000
402201captured50000

Если удалить одну из этих строк «для очистки дубликатов», можно потерять историю отказа. В платёжных событиях это существенная потеря информации.

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 разберите оплаченные попытки и запросы с несколькими таблицами. Бесплатные задания проверяются автоматически; описанный здесь выбор записи можно отдельно выполнить в песочнице.

По теме: почему JOIN увеличивает суммы · как выбрать последнюю запись группы.


Для последовательной практики на MySQL 8 — курс «SQL для аналитика» на Stepik с итоговой работой и возможностью получить сертификат.