Per trovare i duplicati in SQL, raggruppa le righe in base alle colonne che dovrebbero essere univoche e tieni i gruppi con più di una riga. Le window function e i self-join ti mostrano poi le righe duplicate vere e proprie, così puoi decidere quale copia tenere. Gli esempi funzionano in PostgreSQL, MySQL, SQLite e SQL Server.
Come trovare i duplicati in SQL
GROUP BY mette insieme le righe con lo stesso valore, COUNT(*) conta ogni gruppo e HAVING tiene i gruppi con più di una riga:
SELECT columns, COUNT(*)
FROM table_name
GROUP BY columns
HAVING COUNT(*) > 1;
Per esempio, per trovare gli indirizzi email usati da più di un cliente:
-- Input: customers
-- id | name | email
-- ---+----------------+----------------------
-- 1 | Maria Lopez | maria.lopez@gmail.com
-- 2 | Sam Patel | sam@patel.dev
-- 3 | M. Lopez | maria.lopez@gmail.com
-- 4 | Kenji Watanabe | kenji@watanabe.jp
SELECT email, COUNT(*) AS copies
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;
-- Output:
-- email | copies
-- ----------------------+-------
-- maria.lopez@gmail.com | 2
Per considerare duplicate le righe solo quando più colonne coincidono tutte, elencale tutte sia in SELECT sia in GROUP BY.
Regole da conoscere
-
Ogni colonna selezionata deve essere in
GROUP BYo dentro un'aggregazione. Aggiungerenamealla query qui sopra non mostra i nomi dei duplicati. È un errore, tranne in SQLite, che mostra un solo nome per email:
-- Input: the customers table above
SELECT name, email, COUNT(*) AS copies
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;
-- ERROR: column "customers.name" must appear in the GROUP BY clause or be used in an aggregate function
Per vedere ogni riga duplicata, usa una delle query in “Sintassi alternativa”.
-
I NULL finiscono nello stesso gruppo e le maiuscole contano. Tutte le email NULL finiscono in un unico gruppo, quindi sembrano duplicate tra loro, mentre
Ana@Silva.deveana@silva.devno:
-- Input: users
-- id | email
-- ---+--------------
-- 1 | ana@silva.dev
-- 2 | NULL
-- 3 | Ana@Silva.dev
-- 4 | NULL
SELECT email, COUNT(*) AS copies
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
-- Output:
-- email | copies
-- ------+-------
-- NULL | 2
-- MySQL and SQL Server ignore case by default, so they also return ana@silva.dev | 2
-- Skip the NULLs and ignore case:
SELECT LOWER(email) AS email, COUNT(*) AS copies
FROM users
WHERE email IS NOT NULL
GROUP BY LOWER(email)
HAVING COUNT(*) > 1;
-- Output:
-- email | copies
-- --------------+-------
-- ana@silva.dev | 2
Sintassi alternativa
GROUP BY restituisce una riga per ogni valore duplicato. Per ottenere le righe duplicate vere e proprie, conta invece su una window. COUNT(*) OVER (PARTITION BY email) riporta il conteggio di ogni email su ogni riga che la contiene:
-- Input: the customers table above
SELECT id, name, email, copies
FROM (
SELECT id, name, email, COUNT(*) OVER (PARTITION BY email) AS copies
FROM customers
) AS c
WHERE copies > 1
ORDER BY id;
-- Output:
-- id | name | email | copies
-- ---+-------------+-----------------------+-------
-- 1 | Maria Lopez | maria.lopez@gmail.com | 2
-- 3 | M. Lopez | maria.lopez@gmail.com | 2
EXISTS fa lo stesso senza window function: tiene ogni riga per cui esiste un'altra riga con la stessa email:
-- Input: the customers table above
SELECT id, name, email
FROM customers c
WHERE EXISTS (
SELECT 1 FROM customers other
WHERE other.email = c.email AND other.id <> c.id
);
-- Output:
-- id | name | email
-- ---+-------------+----------------------
-- 1 | Maria Lopez | maria.lopez@gmail.com
-- 3 | M. Lopez | maria.lopez@gmail.com
Quando conviene cercare i duplicati?
I duplicati entrano tramite import, doppi clic, tentativi ripetuti e vincoli mancanti. Ecco i quattro compiti che ti capiterà più spesso di dover affrontare.
Verificare che una colonna sia univoca
Prima di aggiungere un vincolo UNIQUE, o quando un login o una ricerca iniziano a restituire due righe, elenca i valori che compaiono più di una volta. GROUP BY con HAVING risponde direttamente.
Vedere tutte le righe coinvolte
Sapere quale valore si ripete non basta per sistemarlo: ti servono le righe complete per vedere in cosa differiscono e quale è quella giusta. Passa i valori duplicati a una query sulla stessa tabella.
Segnare le copie in eccesso
Per fare pulizia, tieni una riga per ogni gruppo di duplicati e rimuovi le altre. ROW_NUMBER() numera le righe all'interno di ogni gruppo, quindi tutto ciò che viene dopo la riga 1 è una copia in eccesso.
Individuare gli invii doppi
Un pulsante cliccato due volte o un pagamento ripetuto lasciano due righe identiche in tutto tranne che nell'ID. Unire la tabella con se stessa le accoppia, e t1.id < t2.id elenca ogni coppia una sola volta.
Esempi reali di ricerca dei duplicati
Trovare camere d'albergo prenotate due volte
Una camera può essere prenotata una sola volta per notte, quindi un duplicato è la stessa camera nella stessa notte. Raggruppa per entrambe le colonne:
-- Input: bookings
-- booking_id | room | night | guest
-- -----------+------+------------+----------
-- 1 | 204 | 2026-11-06 | R. Okafor
-- 2 | 204 | 2026-11-07 | R. Okafor
-- 3 | 310 | 2026-11-06 | L. Chen
-- 4 | 204 | 2026-11-06 | P. Novak
SELECT room, night, COUNT(*) AS bookings
FROM bookings
GROUP BY room, night
HAVING COUNT(*) > 1;
-- Output:
-- room | night | bookings
-- -----+------------+---------
-- 204 | 2026-11-06 | 2
Elencare gli account che condividono un indirizzo email
Capita che le persone si registrino due volte con lo stesso indirizzo scritto con maiuscole diverse. Partiziona per LOWER(email) per affiancare i due account:
-- Input: accounts
-- id | username | email
-- ---+----------+----------------
-- 1 | jkim | jin@kim.kr
-- 2 | olivia_r | olivia@reyes.mx
-- 3 | jin.kim | Jin@Kim.kr
-- 4 | noah_b | noah@baker.uk
SELECT id, username, email, accounts_with_email
FROM (
SELECT id, username, email,
COUNT(*) OVER (PARTITION BY LOWER(email)) AS accounts_with_email
FROM accounts
) AS a
WHERE accounts_with_email > 1
ORDER BY id;
-- Output:
-- id | username | email | accounts_with_email
-- ---+----------+------------+--------------------
-- 1 | jkim | jin@kim.kr | 2
-- 3 | jin.kim | Jin@Kim.kr | 2
Segnalare le persone duplicate prima di una pulizia
Considera gli utenti con lo stesso nome e cognome come un'unica persona, tieni il record più vecchio ed etichetta gli altri. Una CTE tiene separata la numerazione dall'etichettatura:
-- Input: users
-- id | first_name | last_name | email
-- ---+------------+-----------+----------------
-- 1 | Priya | Shah | priya@shah.net
-- 2 | Tom | Becker | tom@becker.de
-- 3 | Priya | Shah | pshah@work.com
-- 4 | Lena | Kowalski | lena@kowal.pl
-- 5 | Tom | Becker | tbecker@mail.de
WITH numbered AS (
SELECT id, first_name, last_name, email,
ROW_NUMBER() OVER (PARTITION BY first_name, last_name ORDER BY id) AS copy_number
FROM users
)
SELECT id, first_name, last_name, email,
CASE WHEN copy_number > 1 THEN 'Duplicate' ELSE 'Original' END AS status
FROM numbered
ORDER BY id;
-- Output:
-- id | first_name | last_name | email | status
-- ---+------------+-----------+-----------------+----------
-- 1 | Priya | Shah | priya@shah.net | Original
-- 2 | Tom | Becker | tom@becker.de | Original
-- 3 | Priya | Shah | pshah@work.com | Duplicate
-- 4 | Lena | Kowalski | lena@kowal.pl | Original
-- 5 | Tom | Becker | tbecker@mail.de | Duplicate
Le righe segnate come Duplicate sono quelle da unire o eliminare. La guida alla rimozione dei duplicati mostra come eliminarle.
Trovare ordini effettuati due volte per errore
Due ordini dello stesso cliente per lo stesso prodotto nello stesso giorno sono probabilmente un unico ordine inviato due volte:
-- Input: orders
-- order_id | customer_id | product | order_date
-- ---------+-------------+-----------+-----------
-- 1001 | 42 | Desk Lamp | 2026-03-02
-- 1002 | 42 | Desk Lamp | 2026-03-02
-- 1003 | 42 | Desk Lamp | 2026-03-09
-- 1004 | 57 | Desk Lamp | 2026-03-02
SELECT o1.order_id AS first_order, o2.order_id AS repeat_order,
o1.customer_id, o1.product
FROM orders o1
JOIN orders o2
ON o1.customer_id = o2.customer_id
AND o1.product = o2.product
AND o1.order_date = o2.order_date
AND o1.order_id < o2.order_id;
-- Output:
-- first_order | repeat_order | customer_id | product
-- ------------+--------------+-------------+----------
-- 1001 | 1002 | 42 | Desk Lamp
Approfondimenti sui duplicati
Su questo blog:
- Guida alla rimozione dei duplicati in SQL: elimina le copie in eccesso una volta trovate
- Rilevare righe duplicate in una tabella SQL Server: lo stesso problema in T-SQL
- Guida a SQL COUNT DISTINCT: conta i valori escludendo i duplicati
-
Guida a SQL GROUP BY: raggruppamento e
HAVINGin dettaglio -
Having vs Where in SQL: perché il filtro sul conteggio va in
HAVING - Creare vincoli unique in PostgreSQL: impedisci l'ingresso di nuovi duplicati
- Guida a CTE vs subquery in SQL: i due modi per strutturare queste query
Documentazione ufficiale:
- PostgreSQL: GROUP BY e HAVING, window function
- MySQL: SELECT, inclusi GROUP BY e HAVING, ROW_NUMBER()
- SQLite: SELECT, inclusi GROUP BY e HAVING, window function
- SQL Server: HAVING, ROW_NUMBER
Beekeeper Studio è una GUI per database gratuita & open source
Il miglior strumento per query & editor SQL che abbia mai usato. Offre tutto ciò che mi serve per gestire il mio database. - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studio è veloce, intuitivo e facile da usare. Beekeeper supporta tantissimi database e funziona alla grande su Windows, Mac e Linux.
Cosa dicono gli utenti di Beekeeper Studio
"Beekeeper Studio ha sostituito completamente il mio vecchio flusso di lavoro SQL. È veloce, intuitivo e rende di nuovo piacevole lavorare con i database."
"Ho provato molte GUI per database, ma Beekeeper trova il perfetto equilibrio tra funzionalità e semplicità. Funziona e basta."