2. 핵심 원리
2.1 "같다" 를 먼저 정한다
중복 여부는 키에 달려 있습니다. 전화번호만 같아도 중복인지, 이름·전화·이메일이 모두 같아야 중복인지는 업무가 정합니다. 키가 좁을수록 지워지는 행이 많아서 위험합니다.
| 종류 | 예 | 찾는 방법 |
|---|---|---|
| 완전 중복 | 모든 값이 같음 | 키 컬럼 그대로 GROUP BY |
| 사실상 중복 | 공백·대소문자·하이픈만 다름 | 정규화 키로 GROUP BY |
| 키만 중복 | 이메일만 같고 이름은 다름 | 업무 판단, 사람이 확인 |
사실상 중복은 값을 같은 모양으로 맞춘 정규화 키로 찾습니다. 이메일은 LOWER(TRIM(email)), 전화번호는 REPLACE(phone, '-', '') 입니다.
2.2 탐지 쿼리의 두 가지 모양
SELECT 키, COUNT(*) FROM member GROUP BY 키 HAVING COUNT(*) > 1;GROUP BY 는 중복 키와 건수를 한 줄씩 보여 주지만 어느 행들인지는 알려 주지 않습니다. 행 전체를 보려면 COUNT(*) OVER (PARTITION BY 키) 를 붙이고 바깥에서 cnt > 1 로 거릅니다. 윈도 함수는 WHERE 에 직접 쓸 수 없어서 인라인 뷰로 감쌉니다(고급 01 순위 함수).
2.3 NULL 은 GROUP BY 에서는 같고 = 에서는 다르다
GROUP BY email 은 NULL 인 행을 모두 한 그룹으로 묶습니다. 반면 a.email = b.email 은 양쪽이 NULL 이면 알 수 없음이라 짝이 되지 않습니다. 같은 "중복" 인데 방법에 따라 NULL 행이 잡히기도 하고 빠지기도 합니다.
이메일이 NULL 인 회원 5명은 서로 다른 사람일 가능성이 높습니다. 이메일 하나만 키로 삼으면 NULL 그룹 5건이 "중복" 으로 나오고, 그대로 지우면 4명이 사라집니다. 키에 NULL 이 있을 수 있는 컬럼은 다른 컬럼과 함께 키를 만들거나 WHERE email IS NOT NULL 로 제외합니다.
핵심중복 키를 정한 뒤 NULL 이 있는 컬럼은 따로 판단합니다. GROUP BY 는 NULL 을 한 그룹으로 묶고, = 조인은 NULL 을 짝짓지 않습니다.
2.4 한 건만 남기는 삭제 패턴
| 방식 | 남기는 행 | 특징 |
|---|---|---|
NOT IN (SELECT MIN(id) ... GROUP BY 키) |
id 가 가장 작은 행 | 표준(MySQL 은 파생 테이블로 감싸야 함) |
ROW_NUMBER() ... ORDER BY 기준 후 rn > 1 삭제 |
기준으로 고른 행 | 최신 가입 같은 조건 가능 |
| DB별 관용구 | 위와 같음 | ROWID, CTE 삭제, 자기 조인 DELETE |
MIN(id) 는 그룹마다 NULL 이 아닌 값을 하나 돌려주므로 NOT IN 이 안전합니다. 하지만 서브쿼리가 NULL 을 돌려줄 수 있는 컬럼을 고르면 결과가 0건이 됩니다. 이 함정은 중급 02 에서 다룬 것과 같습니다.
2.5 삭제 전 준비와 재발 방지
삭제 전에 두 가지를 합니다. 백업 테이블을 만들고, 같은 WHERE 조건으로 SELECT 를 먼저 돌려 건수와 행을 눈으로 확인합니다. 삭제 후에는 백업과 건수를 비교합니다.
재발 방지는 UNIQUE 제약입니다(중급 08 제약 조건). 중복이 남은 채로 제약을 걸면 실패하므로 정리가 먼저입니다. 정규화 키로 막으려면 함수 기반 유니크 인덱스나 계산 열을 씁니다. 자세한 문법은 4절에서 봅니다.