Этот урок открывает серию классических задач с собеседований. Первая из них приходит из жизни: импорт запустили дважды, форма отправилась два раза, и в таблице появились одинаковые строки. Интервьюер хочет увидеть две вещи: что ты умеешь найти повторы и что удаляешь их, не потеряв ни одной нужной строки.
Как спрашивают
В таблице появились дубликаты. Как их найти? Как удалить лишние, оставив по одной строке?
Часто уточняют: «А если у таблицы нет первичного ключа?»
Короткий ответ
Найти повторы помогает GROUP BY по колонкам, которые определяют «одинаковость», с условием HAVING count(*) > 1. Удалить лишние можно через ROW_NUMBER(): пронумеровать строки внутри каждой группы одинаковых и удалить всё, где номер больше единицы. Если ключа нет, в PostgreSQL строку отличает служебная колонка ctid. Удаление делаю в транзакции и сначала смотрю, какие строки уйдут.
Разбор: стопка анкет
Представь, что секретарь принёс стопку анкет, и некоторые люди заполнили анкету дважды. Ты раскладываешь анкеты по фамилиям: где в стопке больше одной анкеты, там дубль. Потом в каждой стопке оставляешь первую анкету, остальные выбрасываешь.
Раскладка по стопкам это GROUP BY, а «оставить первую» это нумерация внутри стопки.
Первое, что нужно решить: что считать дубликатом. В учебной базе блока у Ольги два заказа по 3000. Полностью одинаковыми их не назовёшь: у них разные id и даты. Если бизнес говорит «один клиент, одна сумма это дубль», то группируем по (customer_id, amount):
SELECT customer_id, amount, count(*) AS n
FROM orders
GROUP BY customer_id, amount
HAVING count(*) > 1;
customer_id | amount | n
------------+--------+---
1 | 3000 | 2
HAVING здесь фильтрует группы, а не строки, как в уроке про WHERE и HAVING. Чтобы увидеть сами строки-дубли, соединяем результат с таблицей:
SELECT o.*
FROM orders o
JOIN (
SELECT customer_id, amount
FROM orders
GROUP BY customer_id, amount
HAVING count(*) > 1
) d USING (customer_id, amount)
ORDER BY o.id;
id | customer_id | amount | created_at
---+-------------+--------+-----------
2 | 1 | 3000 | 2026-01-20
4 | 1 | 3000 | 2026-02-15
Удаление через ROW_NUMBER
Пронумеруем строки внутри каждой группы одинаковых. Оконная функция ROW_NUMBER() из урока про ранжирование даёт каждой строке номер в её группе:
SELECT id, customer_id, amount,
row_number() OVER (PARTITION BY customer_id, amount ORDER BY id) AS rn
FROM orders
ORDER BY id;
id | customer_id | amount | rn
---+-------------+--------+---
1 | 1 | 5000 | 1
2 | 1 | 3000 | 1
3 | 2 | 7000 | 1
4 | 1 | 3000 | 2
5 | 3 | 1200 | 1
ORDER BY id внутри окна решает, какую строку оставить: здесь самую раннюю. Всё, где rn > 1, лишнее:
BEGIN;
DELETE FROM orders
WHERE id IN (
SELECT id
FROM (
SELECT id,
row_number() OVER (PARTITION BY customer_id, amount ORDER BY id) AS rn
FROM orders
) t
WHERE rn > 1
);
-- DELETE 1: ушёл заказ 4
SELECT * FROM orders ORDER BY id; -- проверяем, что осталось
ROLLBACK; -- или COMMIT, если всё верно
Оконную функцию нельзя написать прямо в WHERE у DELETE: окна вычисляются на шаге SELECT, как мы видели в уроке про порядок выполнения. Поэтому нумерация живёт в подзапросе.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.