SQL에서 중복을 제거한다는 건 두 가지 중 하나를 뜻해요. DISTINCT로 쿼리 결과에서 반복되는 행을 숨기거나, 테이블에서 남는 사본을 삭제해 각각 하나씩만 남기는 거예요. 예제는 PostgreSQL, MySQL, SQLite, SQL Server에서 동작해요.
SQL에서 중복을 제거하는 방법
SELECT DISTINCT는 나열한 컬럼의 조합을 한 번씩만 반환하고 테이블은 건드리지 않아요. 테이블 자체를 정리하려면 중복 그룹마다 한 행만 남기고 나머지를 모두 삭제하세요. 보통 id가 가장 작은 행을 남겨요:
SELECT DISTINCT column_list FROM table_name;
DELETE FROM table_name
WHERE id NOT IN (
SELECT id FROM (
SELECT MIN(id) AS id FROM table_name GROUP BY column_list
) AS keepers
);
여기서는 Maria Lopez가 세 번 입력되어 있어요:
-- Input: customers
-- id | name | email | phone
-- ---+-------------+-------------------+---------
-- 1 | Maria Lopez | maria@example.com | 555-0142
-- 2 | Sam Patel | sam@example.com | 555-0178
-- 3 | Maria Lopez | maria@example.com | 555-0142
-- 4 | Lena Novak | lena@example.com | 555-0121
-- 5 | Maria Lopez | maria@example.com | 555-0142
SELECT DISTINCT name, email, phone
FROM customers
ORDER BY name;
-- Output:
-- name | email | phone
-- ------------+-------------------+---------
-- Lena Novak | lena@example.com | 555-0121
-- Maria Lopez | maria@example.com | 555-0142
-- Sam Patel | sam@example.com | 555-0178
DELETE FROM customers
WHERE id NOT IN (
SELECT id FROM (
SELECT MIN(id) AS id FROM customers GROUP BY name, email, phone
) AS keepers
);
-- Output (customers after the DELETE):
-- id | name | email | phone
-- ---+-------------+-------------------+---------
-- 1 | Maria Lopez | maria@example.com | 555-0142
-- 2 | Sam Patel | sam@example.com | 555-0178
-- 4 | Lena Novak | lena@example.com | 555-0121
알아 둘 규칙
-
두 행을 중복으로 판단할 컬럼을 나열하세요. id나
created_at타임스탬프처럼 항상 값이 다른 컬럼은 빼세요. 나열한 컬럼 중 하나라도 다른 행은 모두 남아요. -
SELECT id FROM (...) AS keepers래퍼는 MySQL용이에요. 이게 없으면 MySQL은DELETE가 서브쿼리에서 자기 테이블을 읽지 못하게 막아요: “You can’t specify target table ‘customers’ for update in FROM clause”. -
삭제하기 전에 먼저 확인하세요.
GROUP BY나ROW_NUMBER()쿼리를 먼저 실행해서 무엇이 지워질지 확인하고, 롤백할 수 있는 트랜잭션 안에서 삭제하세요. - 테이블을 정리한 뒤에는 유니크 인덱스로 새 중복을 막으세요:
CREATE UNIQUE INDEX customers_email_key ON customers (email);
INSERT INTO customers VALUES (6, 'Maria Lopez', 'maria@example.com', '555-0142');
-- ERROR: duplicate key value violates unique constraint "customers_email_key"
다른 문법
SQL Server 코드에서는 CTE에서 사본에 번호를 매기고 CTE를 통해 삭제하는 방식을 자주 볼 수 있는데, 이건 SQL Server에서만 가능해요. 원래 customers 테이블 기준:
-- SQL Server
WITH numbered AS (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY name, email, phone ORDER BY id
) AS row_num
FROM customers
)
DELETE FROM numbered WHERE row_num > 1;
-- Output (customers after the DELETE):
-- id | name | email | phone
-- ---+-------------+-------------------+---------
-- 1 | Maria Lopez | maria@example.com | 555-0142
-- 2 | Sam Patel | sam@example.com | 555-0178
-- 4 | Lena Novak | lena@example.com | 555-0121
행을 반복하지 않고 두 쿼리를 합치려면 중복을 유지하는 UNION ALL 대신 중복을 제거하는 UNION을 사용하세요:
SELECT 'apple' AS fruit UNION ALL SELECT 'apple';
-- Output:
-- fruit
-- -----
-- apple
-- apple
SELECT 'apple' AS fruit UNION SELECT 'apple';
-- Output: apple
언제 중복을 제거해야 할까요?
결과에서 반복만 숨기면 될 때도 있고, 테이블 자체를 정리해야 할 때도 있어요. 확인부터 삭제까지 일반적인 단계를 소개할게요.
각 값을 한 번만 표시하기
리포트, 드롭다운, 또는 컬럼에 무엇이 있는지 빠르게 확인할 때는 SELECT DISTINCT가 결과에서 반복을 없애고 테이블은 그대로 둬요.
삭제 전에 사본 개수 세기
중복을 정의하는 컬럼으로 그룹화하고 HAVING COUNT(*) > 1로 그룹을 걸러내세요. 아무것도 지우기 전에 무엇이 몇 번 중복됐는지 알 수 있어요. 더 많은 확인 방법은 SQL 중복 찾기 가이드에서 다뤄요.
남길 사본 고르기
ROW_NUMBER()는 중복 그룹마다 행에 번호를 매겨요. OVER (...) 안의 ORDER BY가 어떤 행이 1번을 받아 남을지 정해요. 가장 오래된 행, 가장 최신 행, 또는 가장 완전한 행을 고를 수 있어요.
남는 사본 삭제하기
남길 행을 정했다면 나머지를 삭제하세요. 가장 오래된 행 대신 최신 행을 남기려면 날짜 기준 최신순으로 번호를 매기고 1보다 큰 번호를 모두 삭제하면 돼요.
중복 제거 실전 예제
배송하는 모든 국가와 운송사
DISTINCT는 선택한 컬럼 전체 행에 적용되므로 Canada는 운송사마다 한 번씩 나타나요:
-- Input: shipments
-- order_id | country | carrier
-- ---------+---------+--------
-- 1001 | Canada | UPS
-- 1002 | Germany | DHL
-- 1003 | Canada | FedEx
-- 1004 | Canada | UPS
-- 1005 | Germany | DHL
SELECT DISTINCT country, carrier
FROM shipments
ORDER BY country, carrier;
-- Output:
-- country | carrier
-- --------+--------
-- Canada | FedEx
-- Canada | UPS
-- Germany | DHL
대소문자만 다른 이메일 주소
소문자로 바꾼 이메일로 그룹화하면 Jon@Example.com과 jon@example.com을 사본으로 셀 수 있어요. (MySQL과 SQL Server는 기본적으로 대소문자를 구분하지 않지만, LOWER()를 써도 문제없어요.)
-- Input: accounts
-- id | email
-- ---+----------------------
-- 1 | Ana.Silva@example.com
-- 2 | jon@example.com
-- 3 | ana.silva@example.com
-- 4 | Jon@Example.com
-- 5 | mei@example.com
SELECT LOWER(email) AS email, COUNT(*) AS copies
FROM accounts
GROUP BY LOWER(email)
HAVING COUNT(*) > 1
ORDER BY email;
-- Output:
-- email | copies
-- ----------------------+-------
-- ana.silva@example.com | 2
-- jon@example.com | 2
전화번호가 있는 사본 남기기
전화번호가 있는 행을 먼저, 그다음 id 순으로 정렬해서 각 그룹에서 가장 완전한 행이 1번이 되게 하세요:
-- Input: contacts
-- id | email | phone
-- ---+------------------+---------
-- 1 | ravi@example.com | NULL
-- 2 | ravi@example.com | 555-0110
-- 3 | ines@example.com | 555-0133
-- 4 | ines@example.com | NULL
SELECT
id,
email,
phone,
ROW_NUMBER() OVER (
PARTITION BY email
ORDER BY CASE WHEN phone IS NULL THEN 1 ELSE 0 END, id
) AS row_num
FROM contacts
ORDER BY email, row_num;
-- Output:
-- id | email | phone | row_num
-- ---+------------------+----------+--------
-- 3 | ines@example.com | 555-0133 | 1
-- 4 | ines@example.com | NULL | 2
-- 2 | ravi@example.com | 555-0110 | 1
-- 1 | ravi@example.com | NULL | 2
번호가 2 이상인 행(id 1과 4)을 삭제하면 이메일마다 완전한 연락처가 하나씩 남아요.
id가 없는 테이블에서 완전히 같은 사본 삭제하기
사본이 모든 컬럼에서 똑같다면 가장 작은 값을 남길 id가 없어요. 고유한 행을 새 테이블로 복사한 뒤 바꿔치기하세요. 복사본에는 인덱스와 제약 조건이 유지되지 않으니 기존 테이블에 있던 것을 다시 추가하세요:
-- Input: page_views
-- url | viewed_at
-- ---------+--------------------
-- /pricing | 2026-10-01 09:00:00
-- /pricing | 2026-10-01 09:00:00
-- /docs | 2026-10-01 09:05:00
-- /pricing | 2026-10-01 09:00:00
-- In SQL Server, use SELECT DISTINCT * INTO page_views_clean FROM page_views,
-- then EXEC sp_rename 'page_views_clean', 'page_views'
CREATE TABLE page_views_clean AS SELECT DISTINCT * FROM page_views;
DROP TABLE page_views;
ALTER TABLE page_views_clean RENAME TO page_views;
-- Output (page_views afterwards):
-- url | viewed_at
-- ---------+--------------------
-- /pricing | 2026-10-01 09:00:00
-- /docs | 2026-10-01 09:05:00
중복 제거에 대해 더 읽어 보기
이 블로그의 글:
- SQL 중복 찾기 가이드: 제거하기 전에 중복 행을 찾는 더 많은 방법
- SQL Server 테이블에서 중복 행 찾기: SQL Server 관점
- SQL COUNT DISTINCT 가이드: 반복을 제거하는 대신 고유 값 세기
- SQL DELETE 문 문법과 예제: 행 삭제의 기본
- PostgreSQL에서 유니크 제약 조건 만들기: 중복을 영구히 막는 방법
- SQL CTE와 서브쿼리 비교 가이드: “남길 행 고르기” 단계를 작성하는 두 가지 방법
공식 문서:
- PostgreSQL: DELETE 및 윈도우 함수
- MySQL: DELETE 및 ROW_NUMBER()
- SQLite: DELETE 및 윈도우 함수
- SQL Server: DELETE 및 ROW_NUMBER
Beekeeper Studio는 무료 오픈 소스 데이터베이스 GUI예요
지금까지 써 본 SQL 쿼리 & 에디터 도구 중 최고예요. 데이터베이스 관리에 필요한 모든 게 다 있어요. - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studio는 빠르고 직관적이며 사용하기 쉬워요. 다양한 데이터베이스를 지원하고 Windows, Mac, Linux에서 모두 잘 작동해요.
Beekeeper Studio 사용자 후기
"Beekeeper Studio가 예전 SQL 작업 방식을 완전히 대체했어요. 빠르고 직관적이라 데이터베이스 작업이 다시 즐거워졌어요."
"여러 데이터베이스 GUI를 써 봤지만, Beekeeper는 기능과 단순함 사이의 균형이 완벽해요. 그냥 잘 돌아가요."