한 줄 정의
클러스터링 인덱스는 프라이머리 키 값이 비슷한 레코드들을 디스크에 물리적으로 가까이 저장 하는 방식이며, InnoDB에서 PK는 단순한 인덱스가 아니라 테이블 그 자체의 저장 구조 입니다.
쉽게 말하면
일반 인덱스가 “책 뒷면의 색인” 이라면, 클러스터링 인덱스는 “책 자체를 알파벳순으로 쓴 것” 입니다.
- 일반 인덱스 : 색인에 “단어 → 페이지 번호”를 적어두고, 본문은 별도로 존재
- 클러스터링 인덱스 : 책의 본문 자체가 이미 정렬되어 있음. 따로 색인을 만들 필요조차 없음
InnoDB에서 PK는 본문(데이터) 자체의 정렬 기준 이 됩니다. 그래서 PK로 검색하는 것은 색인을 거치지 않고 바로 본문에 도달 하는 것과 같습니다.
왜 중요한가?
“InnoDB의 PK는 클러스터링 인덱스다”는 단순한 구현 세부가 아니라, MySQL 성능 설계의 중심 개념 입니다.
- PK 설계가 전체 테이블 저장 구조를 결정 합니다
- 세컨더리 인덱스의 리프 = PK 값 이므로, PK가 크면 모든 세컨더리 인덱스가 커집니다
- PK가 랜덤하면 INSERT 성능이 무너집니다
- 오라클이나 SQL Server와 다르게, MySQL InnoDB는 모든 테이블이 자동으로 클러스터링 됩니다
이 원리를 모르면 “왜 UUID를 PK로 쓰면 느려질까?” 같은 질문에 답할 수 없습니다.
핵심 내용
일반 테이블 저장 vs 클러스터링
flowchart TB subgraph MyISAM["MyISAM (Non-clustered)"] direction LR PK_IDX["PK 인덱스<br/>(B-Tree)"] DATA_FILE["데이터 파일<br/>(삽입 순서대로)"] PK_IDX -->|ROWID로 참조| DATA_FILE end subgraph InnoDB["InnoDB (Clustered)"] direction LR PK_TREE["PK B-Tree<br/>= 테이블 그 자체<br/>(리프에 실제 데이터 저장)"] end
- MyISAM : 데이터 파일과 인덱스 파일이 분리. PK 인덱스는 ROWID(물리 주소) 를 가리킴
- InnoDB : PK B-Tree의 리프 페이지가 곧 테이블 데이터. 별도 데이터 파일 없음
세컨더리 인덱스의 특수성
InnoDB에서 세컨더리 인덱스의 리프에는 ROWID가 아닌 PK 값 이 저장됩니다.
flowchart LR subgraph Sec["세컨더리 인덱스 (first_name)"] S1["'Georgi' → PK=10001"] S2["'Georgi' → PK=10002"] S3["'Bezalel' → PK=10003"] end subgraph Clus["클러스터링 인덱스 (PK)"] C1["PK=10001 → 실제 레코드"] C2["PK=10002 → 실제 레코드"] C3["PK=10003 → 실제 레코드"] end Sec -->|PK로 다시 탐색| Clus
세컨더리 인덱스로 조회 시 동작
- 세컨더리 인덱스 B-Tree 탐색 → PK 값 획득
- PK 값으로 클러스터링 인덱스 B-Tree를 다시 탐색 → 실제 데이터 획득
이것이 인덱스 왕복(back-ref) 입니다. 클러스터링 인덱스 덕분에 페이지 분리가 일어나도 세컨더리 인덱스를 갱신할 필요가 없다 는 이점이 있지만, 조회 시 왕복 비용이 발생합니다.
클러스터링 인덱스의 장단점
| 구분 | 장점 | 단점 |
|---|---|---|
| 조회 | PK 검색이 매우 빠름 | 세컨더리 인덱스는 왕복 필요 |
| 범위 검색 | PK 범위 스캔이 물리적 순차 I/O | — |
| INSERT | 순차 PK면 매우 빠름 | 랜덤 PK면 페이지 분리 폭증 |
| 공간 | 별도 데이터 파일 없음 | 세컨더리 인덱스 크기 증가 (PK 포함) |
PK 설계 원칙 — InnoDB에서 가장 중요한 원칙
클러스터링 인덱스의 특성으로 인해, PK 설계는 다음 원칙을 따라야 합니다.
1. 단조 증가 (Monotonic)
PK는 AUTO_INCREMENT나 시간 기반 ID 처럼 단조 증가해야 합니다.
flowchart LR subgraph 단조증가["단조 증가 PK (AUTO_INCREMENT)"] direction TB A1["1,2,3... 순차"] A2["항상 마지막 페이지에 추가"] A3["페이지 분리 거의 없음"] A1 --> A2 --> A3 end subgraph 랜덤["랜덤 PK (UUID)"] direction TB B1["임의 값"] B2["중간 페이지에 계속 삽입"] B3["페이지 분리 폭증<br/>→ 버퍼 풀 낭비<br/>→ 디스크 I/O 증가"] B1 --> B2 --> B3 end
UUID v4를 PK로 쓰면 INSERT 성능이 심각하게 저하됩니다. 꼭 UUID를 써야 한다면 정렬 가능한 UUID v7 이나 ULID 를 고려해야 합니다.
2. 작게 (Small)
PK 크기는 모든 세컨더리 인덱스의 크기 에 영향을 줍니다.
세컨더리 인덱스 1개의 크기 ≈ (세컨더리 칼럼 + PK) × 레코드 수
PK가 8바이트 BIGINT vs 36바이트 UUID 문자열
→ 세컨더리 인덱스가 5배 더 커짐
→ 버퍼 풀 효율 저하, 캐시 히트율 하락
3. 불변 (Immutable)
PK 값은 변경하지 않는 것이 원칙입니다. PK가 바뀌면 모든 세컨더리 인덱스의 해당 엔트리 가 갱신되어야 합니다.
4. 비즈니스 의미 없음 (Surrogate Key)
주민번호, 이메일 같은 비즈니스 키 는 PK로 부적합합니다. 변경 가능성이 있고, 크기가 크고, 노출될 수 있기 때문입니다. 대신 BIGINT AUTO_INCREMENT 같은 대리 키(surrogate key) 를 PK로 쓰고, 비즈니스 키는 별도 유니크 인덱스로 관리합니다.
PK를 지정하지 않으면?
InnoDB는 반드시 클러스터링 인덱스가 필요 합니다. 개발자가 PK를 지정하지 않으면 다음 순서로 자동 선택됩니다.
NOT NULL인 유니크 인덱스 중 첫 번째- 둘 다 없으면 InnoDB가 내부적으로 6바이트 숨은 클러스터링 키 를 자동 생성
숨은 키는 개발자가 제어할 수 없고, 단조 증가조차 보장되지 않을 수 있습니다. 반드시 명시적으로 PK를 지정 해야 합니다.
클러스터링 vs 논-클러스터링 — 세컨더리 인덱스에서 발생하는 차이
-- 인덱스: (first_name)
SELECT first_name, last_name FROM employees WHERE first_name = 'Georgi';| 엔진 | 동작 |
|---|---|
| MyISAM | 세컨더리 인덱스에서 ROWID → 데이터 파일 직접 접근 |
| InnoDB | 세컨더리 인덱스에서 PK → PK B-Tree → 데이터 (왕복) |
InnoDB의 왕복 비용을 줄이려면 커버링 인덱스 를 활용해야 합니다. 세컨더리 인덱스 리프에 이미 PK가 저장되어 있으므로, PK + 인덱스 칼럼 은 사실상 공짜로 커버링됩니다.
정리
InnoDB PK 설계 체크리스트
| 원칙 | 실천 방안 |
|---|---|
| 단조 증가 | AUTO_INCREMENT 또는 정렬 가능한 UUID |
| 작게 | BIGINT 이하 권장, UUID 문자열 지양 |
| 불변 | 비즈니스 값을 PK로 쓰지 않기 |
| 의미 없음 | 대리 키 사용, 비즈니스 키는 유니크 인덱스로 |
PK vs 세컨더리 인덱스 조회 비용
| 조회 방식 | 경로 | 비용 |
|---|---|---|
| PK 조회 | PK B-Tree만 | 낮음 |
| 세컨더리 조회 (비커버링) | 세컨더리 → PK → 데이터 | 중간 |
| 세컨더리 조회 (커버링) | 세컨더리만 | 매우 낮음 |
내 생각
-
UUID를 PK로 쓰지 말라 는 조언의 진짜 이유는 클러스터링 인덱스 때문입니다. “성능이 나쁘다”는 추상적인 설명이 아니라, “페이지 분리가 폭증해서 버퍼 풀을 낭비하고 I/O가 증가한다” 는 구체적 이유를 알고 있어야 설계 결정을 설득할 수 있습니다.
-
세컨더리 인덱스의 리프가 PK라는 사실은 “세컨더리 인덱스 하나당 PK를 한 번 더 저장한다” 는 의미입니다. 세컨더리 인덱스가 5개 있는 테이블에서 PK가 36바이트라면, PK 값만 180바이트가 추가로 중복 저장됩니다. 이것이 PK 크기가 중요한 수학적 근거입니다.
-
클러스터링 구조 덕분에 “PK 범위 조회는 거의 무조건 빠릅니다.”
WHERE id BETWEEN 1 AND 1000같은 쿼리는 디스크 상에서 물리적으로 연속된 페이지만 읽으므로, 같은 100만 건 조회라도 다른 세컨더리 인덱스 조회보다 훨씬 빠릅니다. 배치 작업에서 이 특성을 활용하면 큰 효과를 볼 수 있습니다.
관련 개념
- Ch04-2 InnoDB 스토리지 엔진 — PK 클러스터링 전반 개요
- Ch08-3 B-Tree 인덱스 — 리프 노드 구조
- Ch08-9 유니크 인덱스 — PK와 유니크 인덱스의 관계
- Ch05-3 InnoDB 스토리지 엔진 잠금 — 인덱스와 잠금
출처
- Real MySQL 8.0 (1권), 8.8 클러스터링 인덱스