Канва примерно такая:
есть ip - primary key и dup_fld - поле, которое могло дублироваться
1. select dup_fld from table group by dup_fld having count(*)>1 -- это дубликаты как таковые
2. select max(id) from table group by dup_fld having count(*)>1 -- это id (уникальный, последний) в дубликатах
3. само удаление по условию что dup_fld из 1 селекта и id НЕ из 2 селекта
во втором селекте берем самый последний id - подразумевая, что id растут при добавлении и самое последнее добавление - самое верное, но можно брать min - тогда "самое первое оставить" или развивать конструкцию до нужной логики (типа цена ближе всего к среднему и т.п.)
Ну а как реалезовать - вопрос вкуса и компромисса с оптимизатором. Я бы делал 2 cte - первым inner join "урезал" набор до только дубликаты, вторым - left join where cte2.id is null
with cte_dbls(dup_fld) as (select dub_fld from table group by dup_fld having count(dup_fld)>1),
cte_ones( id ) as ( select max(id) from table group by dup_fld having count(dup_fld)>1)
--delete from table where id in (
select
id
from table
inner join cte_dbls on cte_dbls.dup_fld=table.dup_fld
left join cte_ones on cte_ones.id=table.id
where cte_ones.id is null
--)
Для удобства - само удаление закомменчено, чтобы увидеть результат селекта, а так можно избавится из конструкции where in