Puede unirse a la misma tabla en los campos que se duplicarían y luego unirse en el campo id. Seleccione el campo id del primer alias de la tabla (tn1) y luego use la función array_agg en el campo id del segundo alias de la tabla. Finalmente, para que la función array_agg funcione correctamente, agrupará los resultados por el campo tn1.id. Esto producirá un conjunto de resultados que contiene la identificación de un registro y una matriz de todas las identificaciones que se ajustan a las condiciones de unión.
select tn1.id,
array_agg(tn2.id) as duplicate_entries,
from table_name tn1 join table_name tn2 on
tn1.year = tn2.year
and tn1.sid = tn2.sid
and tn1.user_id = tn2.user_id
and tn1.cid = tn2.cid
and tn1.id <> tn2.id
group by tn1.id;
Obviamente, los id que estarán en la matriz duplicate_entries para un id, también tendrán sus propias entradas en el conjunto de resultados. Tendrá que usar este conjunto de resultados para decidir qué identificación desea que se convierta en la fuente de la 'verdad'. El único registro que no debería eliminarse. Tal vez podrías hacer algo como esto:
with dupe_set as (
select tn1.id,
array_agg(tn2.id) as duplicate_entries,
from table_name tn1 join table_name tn2 on
tn1.year = tn2.year
and tn1.sid = tn2.sid
and tn1.user_id = tn2.user_id
and tn1.cid = tn2.cid
and tn1.id <> tn2.id
group by tn1.id
order by tn1.id asc)
select ds.id from dupe_set ds where not exists
(select de from unnest(ds.duplicate_entries) as de where de < ds.id)
Selecciona las ID de números más bajos que tienen duplicados (suponiendo que la ID está aumentando int PK). Estas serían las identificaciones que conservaría.