Untuk mencari duplikat di SQL, kelompokkan baris berdasarkan kolom yang seharusnya unik, lalu ambil grup yang berisi lebih dari satu baris. Window function dan self-join kemudian menampilkan baris duplikatnya sendiri, sehingga Anda bisa memutuskan salinan mana yang dipertahankan. Contoh-contohnya berjalan di PostgreSQL, MySQL, SQLite, dan SQL Server.
Cara mencari duplikat di SQL
GROUP BY mengumpulkan baris dengan nilai yang sama, COUNT(*) menghitung setiap grup, dan HAVING menyisakan grup yang berisi lebih dari satu baris:
SELECT columns, COUNT(*)
FROM table_name
GROUP BY columns
HAVING COUNT(*) > 1;
Misalnya, untuk mencari alamat email yang dipakai oleh lebih dari satu pelanggan:
-- 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
Agar baris dianggap duplikat hanya jika beberapa kolom sekaligus cocok, cantumkan semua kolom tersebut di SELECT dan juga di GROUP BY.
Aturan yang perlu diketahui
-
Setiap kolom yang Anda pilih harus ada di
GROUP BYatau berada di dalam fungsi agregat. Menambahkannameke query di atas tidak akan menampilkan nama-nama duplikatnya. Hasilnya justru error, kecuali di SQLite, yang menampilkan satu nama 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
Untuk melihat setiap baris duplikat, gunakan salah satu query di bagian “Sintaks alternatif”.
-
NULL dikelompokkan bersama, dan huruf besar/kecil berpengaruh. Semua email NULL masuk ke satu grup sehingga tampak seperti duplikat satu sama lain, sementara
Ana@Silva.devdanana@silva.devtidak:
-- 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
Sintaks alternatif
GROUP BY mengembalikan satu baris untuk setiap nilai yang terduplikasi. Untuk mendapatkan baris duplikatnya sendiri, hitunglah melalui window. COUNT(*) OVER (PARTITION BY email) menaruh jumlah setiap email di setiap baris yang memilikinya:
-- 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 melakukan hal yang sama tanpa window function: ambil setiap baris yang memiliki baris lain dengan email yang sama:
-- 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
Kapan Anda perlu mencari duplikat?
Duplikat masuk lewat impor data, klik ganda, percobaan ulang, dan constraint yang tidak ada. Berikut empat tugas yang paling sering perlu Anda lakukan terkait duplikat.
Memastikan kolom bersifat unik
Sebelum menambahkan constraint UNIQUE, atau ketika login atau pencarian mulai mengembalikan dua baris, tampilkan nilai-nilai yang muncul lebih dari sekali. GROUP BY dengan HAVING langsung menjawab hal ini.
Melihat setiap baris yang terlibat
Mengetahui nilai mana yang berulang saja belum cukup untuk memperbaikinya: Anda butuh baris lengkapnya untuk melihat perbedaannya dan mana yang benar. Masukkan kembali nilai-nilai duplikat itu ke query pada tabel yang sama.
Menandai salinan berlebih
Untuk membersihkan data, pertahankan satu baris dari setiap kelompok duplikat dan hapus sisanya. ROW_NUMBER() memberi nomor pada baris di setiap kelompok, sehingga semua baris setelah baris 1 adalah salinan berlebih.
Mendeteksi pengiriman ganda
Tombol yang diklik dua kali atau pembayaran yang dicoba ulang meninggalkan dua baris yang identik kecuali ID-nya. Menggabungkan tabel dengan dirinya sendiri memasangkan keduanya, dan t1.id < t2.id menampilkan setiap pasangan sekali saja.
Contoh nyata mencari duplikat
Mencari kamar hotel yang dipesan ganda
Sebuah kamar hanya bisa dipesan sekali per malam, jadi duplikat berarti kamar dan malam yang sama. Kelompokkan berdasarkan kedua kolom:
-- 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
Menampilkan akun yang memakai alamat email yang sama
Orang sering mendaftar dua kali dengan alamat yang sama tapi huruf besar/kecil berbeda. Partisi berdasarkan LOWER(email) untuk menampilkan kedua akun berdampingan:
-- 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
Menandai orang duplikat sebelum pembersihan
Anggap pengguna dengan nama depan dan belakang yang sama sebagai satu orang, pertahankan data tertua, dan beri label pada sisanya. CTE memisahkan proses penomoran dari pemberian label:
-- 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
Baris yang bertanda Duplicate adalah baris yang perlu digabung atau dihapus. Panduan menghapus duplikat menjelaskan cara menghapusnya.
Mencari pesanan yang tidak sengaja dibuat dua kali
Dua pesanan dari pelanggan yang sama untuk produk yang sama di hari yang sama kemungkinan besar adalah satu pesanan yang terkirim dua kali:
-- 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
Bacaan lanjutan tentang duplikat
Di blog ini:
- Panduan menghapus duplikat di SQL: hapus salinan berlebih setelah Anda menemukannya
- Mendeteksi baris duplikat di tabel SQL Server: masalah yang sama dalam T-SQL
- Panduan SQL COUNT DISTINCT: hitung nilai tanpa menyertakan duplikat
-
Panduan SQL GROUP BY: pengelompokan dan
HAVINGsecara mendalam -
Having vs Where di SQL: mengapa filter jumlah ditaruh di
HAVING - Membuat unique constraint di PostgreSQL: cegah duplikat baru masuk
- Panduan SQL CTE vs subquery: dua cara menyusun query seperti ini
Dokumentasi resmi:
- PostgreSQL: GROUP BY dan HAVING, window function
- MySQL: SELECT, termasuk GROUP BY dan HAVING, ROW_NUMBER()
- SQLite: SELECT, termasuk GROUP BY dan HAVING, window function
- SQL Server: HAVING, ROW_NUMBER
Beekeeper Studio Adalah GUI Database Gratis & Open Source
Tool query & editor SQL terbaik yang pernah saya pakai. Semua yang saya butuhkan untuk mengelola database ada di sini. - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studio cepat, intuitif, dan mudah digunakan. Beekeeper mendukung banyak sekali database, dan berjalan mulus di Windows, Mac, dan Linux.
Apa Kata Pengguna Tentang Beekeeper Studio
"Beekeeper Studio benar-benar menggantikan alur kerja SQL lama saya. Cepat, intuitif, dan membuat kerja dengan database kembali menyenangkan."
"Saya sudah mencoba banyak GUI database, tapi Beekeeper punya keseimbangan sempurna antara fitur dan kesederhanaan. Langsung jalan."