한 줄 정의
유니크 인덱스는 “중복 값을 허용하지 않는다”는 제약 조건 이며, 성능 최적화 수단이 아니라 데이터 정합성을 위한 장치 입니다.
쉽게 말하면
“유니크 인덱스는 빠른 인덱스가 아닙니다.” 이 오해를 바로잡는 것이 이 노트의 핵심입니다.
유니크 인덱스는 “이 칼럼에는 같은 값이 두 개 있을 수 없다”는 규칙 을 DB에 선언하는 것입니다. 옵티마이저가 이 정보를 이용해 약간의 최적화를 할 수는 있지만, 그 이득보다 변경 시 중복 체크 비용이 더 큽니다.
왜 중요한가?
실무에서 자주 나오는 오해 두 가지가 있습니다.
- “유니크 인덱스가 일반 인덱스보다 빠르다” → 절반만 맞음
- “조회 성능을 위해 유니크 인덱스를 걸자” → 틀림
이 오해를 그대로 두면 불필요한 유니크 제약 이 쌓여서 INSERT/UPDATE 성능을 갉아먹습니다. 유니크 인덱스의 본질을 이해하면 “이 칼럼이 정말 유니크여야 하는가?” 를 비즈니스 관점에서 묻게 됩니다.
핵심 내용
유니크 = 제약 조건 (Constraint)
유니크는 인덱스의 속성 이 아니라 제약 조건 입니다.
- 인덱스는 자료구조 (저장 방식)
- 유니크는 값의 규칙 (제약 조건)
MySQL은 이 둘을 묶어서 “유니크 인덱스”로 구현하지만, 개념적으로는 분리된 것 입니다. PostgreSQL 같은 DB에서는 UNIQUE CONSTRAINT와 INDEX가 완전히 분리된 객체로 존재하기도 합니다.
일반 인덱스 vs 유니크 인덱스: 조회 성능
조회
| 조건 | 일반 인덱스 | 유니크 인덱스 |
|---|---|---|
등가 검색 (WHERE col = ?) | 일치 항목을 끝까지 확인 | 첫 매치에서 종료 |
| 범위 검색 | 차이 없음 | 차이 없음 |
유니크 인덱스의 유일한 성능 이점은 등가 검색에서 한 건 찾으면 멈춘다 는 점입니다. 일반 인덱스는 “혹시 중복이 또 있을까?” 확인해야 하지만, 유니크 인덱스는 그럴 수 없으므로 즉시 반환할 수 있습니다.
그런데 이 이득이 크지 않은 이유
B-Tree에서 동일 키의 다음 레코드를 확인하는 비용은 거의 0 에 가깝습니다. 같은 페이지 내의 인접 레코드를 한 번 더 비교하는 수준이라, 실제로는 대부분의 워크로드에서 체감 불가능 합니다.
INSERT/UPDATE에서의 비용: 일반 인덱스가 더 빠름
유니크 인덱스는 변경 시 “이미 같은 값이 있는지 확인” 해야 합니다.
flowchart LR subgraph 일반["일반 INSERT"] G1["B-Tree 탐색"] --> G2["키 추가"] end subgraph 유니크["유니크 INSERT"] U1["B-Tree 탐색"] --> U2["이미 같은 값 존재?"] U2 --> U3["없으면 추가<br/>있으면 에러"] end
이 중복 체크 과정에서 잠금이 추가로 걸립니다. InnoDB는 유니크 제약을 위해 넥스트 키 락 을 사용하므로, 일반 인덱스보다 잠금 범위가 넓어집니다.
또한 체인지 버퍼를 사용할 수 없습니다. 일반 세컨더리 인덱스는 변경 사항을 체인지 버퍼에 모았다가 나중에 병합할 수 있지만, 유니크 인덱스는 즉시 중복 여부를 확인 해야 하므로 버퍼링이 불가능합니다.
체인지 버퍼를 쓸 수 없는 문제
InnoDB의 체인지 버퍼는 세컨더리 인덱스의 변경을 지연 병합 하는 최적화입니다 (Ch04-2 InnoDB 스토리지 엔진).
- 일반 세컨더리 인덱스 : INSERT/UPDATE 시 체인지 버퍼에 모아뒀다가 나중에 병합
- 유니크 세컨더리 인덱스 : 매번 즉시 확인 필요 → 체인지 버퍼 무용
이 차이는 대량 INSERT 워크로드에서 수 배의 성능 차이 를 만들 수 있습니다.
그럼 언제 유니크 인덱스를 쓰는가?
데이터 정합성이 필요할 때만 입니다.
| 상황 | 유니크 필요? |
|---|---|
| 이메일 중복 방지 | O |
| 주민번호 중복 방지 | O |
| 가입 코드 중복 방지 | O |
| 조회 성능 향상 | X |
| 옵티마이저 힌트 주기 | △ (대안 있음) |
PK는 자동으로 유니크
프라이머리 키는 암묵적으로 유니크입니다. 클러스터링 인덱스이자 유니크 인덱스인 셈입니다. 다만 PK는 NULL 불가 이고 테이블당 1개 라는 추가 제약이 있습니다.
유니크 인덱스와 NULL
유니크 인덱스는 NULL 값은 중복을 허용 합니다. MySQL 기준이며, 이는 ANSI SQL 표준입니다.
CREATE TABLE users (
email VARCHAR(100) UNIQUE
);
-- 둘 다 성공 (NULL은 유니크 제약에서 제외)
INSERT INTO users VALUES (NULL);
INSERT INTO users VALUES (NULL);NULL까지 유니크로 다루고 싶다면 함수 기반 인덱스 를 활용해 COALESCE(col, 'sentinel') 형태로 인덱싱할 수 있지만, 복잡도가 올라갑니다.
정리
일반 vs 유니크 인덱스 비교
| 기준 | 일반 인덱스 | 유니크 인덱스 |
|---|---|---|
| 역할 | 검색 최적화 | 제약 조건 + 검색 |
| 등가 조회 | 일치 끝까지 확인 | 첫 매치에서 종료 |
| INSERT/UPDATE | 빠름 | 느림 (중복 체크) |
| 체인지 버퍼 | 사용 가능 | 사용 불가 |
| 잠금 범위 | 일반적 | 넥스트 키 락 추가 |
| NULL | 여러 개 허용 | 여러 개 허용 (표준) |
내 생각
-
“일단 유니크 인덱스로 만들자”는 방어적 설계는 위험합니다. 데이터 정합성이 필요 없는 칼럼에 유니크를 걸면 쓰기 성능만 깎아먹습니다. 이메일처럼 논리적으로 반드시 유니크해야 하는 칼럼 에만 신중히 걸어야 합니다.
-
유니크 인덱스는 “비즈니스 규칙을 DB에 못박는 장치” 로 이해하는 것이 좋습니다. “이 값은 중복되면 안 된다”는 규칙이 변할 수 있다면, 애플리케이션 레벨의 체크로도 충분할 수 있습니다. 단, 경합 상황에서 유니크 제약은 경쟁 조건을 원천 차단 하므로, 정말 중요한 규칙이라면 DB 제약이 더 안전합니다.
-
대량 INSERT 배치에서 유니크 인덱스는 큰 병목 이 됩니다. 초기 데이터 적재 시에는 인덱스를 제거했다가 적재 후 다시 생성 하는 패턴이 유효합니다. 특히 체인지 버퍼를 쓸 수 없다는 점이 결정타가 됩니다.
관련 개념
- Ch08-8 클러스터링 인덱스 — PK와의 관계
- Ch08-3 B-Tree 인덱스 — 일반 인덱스의 동작
- Ch04-2 InnoDB 스토리지 엔진 — 체인지 버퍼
- Ch05-3 InnoDB 스토리지 엔진 잠금 — 유니크 제약과 잠금
출처
- Real MySQL 8.0 (1권), 8.9 유니크 인덱스