한 줄 정의

클러스터링 인덱스는 프라이머리 키 값이 비슷한 레코드들을 디스크에 물리적으로 가까이 저장 하는 방식이며, 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
세컨더리 인덱스로 조회 시 동작
  1. 세컨더리 인덱스 B-Tree 탐색 → PK 값 획득
  2. 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를 지정하지 않으면 다음 순서로 자동 선택됩니다.

  1. NOT NULL인 유니크 인덱스 중 첫 번째
  2. 둘 다 없으면 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만 건 조회라도 다른 세컨더리 인덱스 조회보다 훨씬 빠릅니다. 배치 작업에서 이 특성을 활용하면 큰 효과를 볼 수 있습니다.

관련 개념

출처

  • Real MySQL 8.0 (1권), 8.8 클러스터링 인덱스