🧚 잠깐만요! Beekeeper Studio는 빠르고 모던한 오픈 소스 데이터베이스 GUI예요 다운로드
2026년 1월 9일 작성자: Matthew Rathbone

SQL에서 중복을 찾으려면 고유해야 하는 컬럼으로 행을 그룹화하고, 행이 2개 이상인 그룹만 남기면 돼요. 그런 다음 윈도우 함수와 셀프 조인으로 중복된 행 자체를 확인하고 어떤 행을 남길지 결정할 수 있어요. 예제는 PostgreSQL, MySQL, SQLite, SQL Server에서 동작해요.

SQL에서 중복을 찾는 방법

GROUP BY는 같은 값을 가진 행을 하나로 묶고, COUNT(*)는 각 그룹의 행 수를 세며, HAVING은 행이 2개 이상인 그룹만 남겨요:

SELECT columns, COUNT(*)
FROM table_name
GROUP BY columns
HAVING COUNT(*) > 1;

예를 들어, 2명 이상의 고객이 사용하는 이메일 주소를 찾으려면 이렇게 해요:

-- 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

여러 컬럼이 모두 일치할 때만 중복으로 보려면, 해당 컬럼을 모두 SELECT와 GROUP BY 양쪽에 나열하세요.

알아 둘 규칙

  • SELECT하는 모든 컬럼은 GROUP BY에 있거나 집계 함수 안에 있어야 해요. 위 쿼리에 name을 추가해도 중복된 행의 이름이 표시되지 않아요. 오류가 나는데, SQLite만 예외로 이메일당 이름 하나를 보여 줘요:
-- 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

중복된 행을 모두 보려면 “대체 문법”에 있는 쿼리 중 하나를 사용하세요.

  • NULL은 한 그룹으로 묶이고, 대소문자는 구분돼요. NULL 이메일은 모두 한 그룹에 들어가서 서로 중복처럼 보이지만, Ana@Silva.dev와 ana@silva.dev는 중복으로 잡히지 않아요:
-- 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

대체 문법

GROUP BY는 중복된 값마다 행을 하나씩 반환해요. 중복된 행 자체를 얻으려면 대신 윈도우로 개수를 세세요. COUNT(*) OVER (PARTITION BY email)은 각 이메일의 개수를 해당 이메일을 가진 모든 행에 붙여 줘요:

-- 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를 쓰면 윈도우 함수 없이 같은 결과를 얻을 수 있어요. 같은 이메일을 가진 다른 행이 존재하는 행만 남기면 돼요:

-- 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

언제 중복을 찾아야 할까요?

중복은 가져오기, 더블 클릭, 재시도, 누락된 제약 조건 때문에 생겨요. 중복과 관련해 가장 자주 하게 될 작업 4가지를 소개할게요.

컬럼이 고유한지 확인하기

UNIQUE 제약 조건을 추가하기 전이나, 로그인 또는 조회 결과가 2개씩 나오기 시작할 때는 두 번 이상 나타나는 값을 나열해 보세요. GROUP BY와 HAVING으로 바로 확인할 수 있어요.

관련된 모든 행 보기

어떤 값이 반복되는지만 알아서는 문제를 고칠 수 없어요. 행 전체를 봐야 어떻게 다른지, 어느 쪽이 맞는지 알 수 있죠. 중복된 값을 같은 테이블에 대한 쿼리에 다시 넣어 보세요.

여분의 사본 표시하기

정리하려면 각 중복 집합에서 행 하나만 남기고 나머지는 삭제하세요. ROW_NUMBER()는 각 집합 안의 행에 번호를 매기므로, 1번 이후의 행은 모두 여분의 사본이에요.

중복 제출 잡아내기

버튼을 두 번 클릭하거나 결제를 재시도하면 ID만 다르고 나머지는 모두 같은 행이 2개 남아요. 테이블을 자기 자신과 조인하면 이 행들을 짝지을 수 있고, t1.id < t2.id를 쓰면 각 쌍이 한 번씩만 나와요.

중복 찾기 실전 예제

이중 예약된 호텔 객실 찾기

객실은 하룻밤에 한 번만 예약할 수 있으니, 같은 객실과 같은 날짜의 조합이 중복이에요. 두 컬럼으로 그룹화하세요:

-- 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

같은 이메일 주소를 공유하는 계정 나열하기

같은 주소를 대소문자만 다르게 해서 두 번 가입하는 경우가 있어요. LOWER(email)로 파티션하면 두 계정을 나란히 볼 수 있어요:

-- 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

정리 전에 중복 인물 표시하기

이름과 성이 같은 사용자를 한 사람으로 보고, 가장 오래된 레코드를 남긴 뒤 나머지에 라벨을 붙여요. CTE를 쓰면 번호 매기기와 라벨 붙이기를 분리할 수 있어요:

-- 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

Duplicate로 표시된 행이 병합하거나 삭제할 대상이에요. 삭제 방법은 중복 제거 가이드에서 확인하세요.

실수로 두 번 들어간 주문 찾기

같은 고객이 같은 날 같은 상품을 두 번 주문했다면 아마 한 주문이 두 번 제출된 것일 거예요:

-- 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

중복에 관해 더 읽을거리

이 블로그의 글:

공식 문서:

Beekeeper Studio는 무료 오픈 소스 데이터베이스 GUI예요

지금까지 써 본 SQL 쿼리 & 에디터 도구 중 최고예요. 데이터베이스 관리에 필요한 모든 게 다 있어요. - ⭐⭐⭐⭐⭐ Mit

Beekeeper Studio는 빠르고 직관적이며 사용하기 쉬워요. 다양한 데이터베이스를 지원하고 Windows, Mac, Linux에서 모두 잘 작동해요.

Beekeeper의 Linux 버전은 100% 모든 기능을 갖추고 있어요. 대충 만든 부분도, 빠진 기능도 없어요.

Beekeeper Studio 사용자 후기

★★★★★
"Beekeeper Studio가 기존 SQL 작업 방식을 완전히 대체했어요. 빠르고 직관적이라 데이터베이스 작업이 다시 즐거워졌어요."
— Alex K., 데이터베이스 개발자
★★★★★
"여러 데이터베이스 GUI를 써 봤지만, Beekeeper는 기능과 단순함 사이의 균형이 완벽해요. 그냥 잘 돌아가요."
— Sarah M., 풀스택 엔지니어

SQL 작업 방식을 개선할 준비가 되셨나요?

download 무료 다운로드