데이터베이스 용어 사전
인덱스page split · 단편화

페이지 분할

인덱스 페이지가 가득 찬 상태에서 중간에 값이 삽입될 때 페이지를 쪼개는 동작. 랜덤 PK가 이를 유발한다.

B+Tree의 한 페이지가 가득 찬 상태에서 그 사이에 새 키가 들어올 때, 페이지를 둘로 쪼개는 동작.

왜 비싼가

페이지가 꽉 찼다:  [10 | 20 | 30 | 40]
키 25 를 삽입해야 한다
     ↓
① 새 페이지를 할당한다
② 기존 키를 절반씩 나눠 옮긴다   [10 | 20 | 25]   [30 | 40]
③ 부모 노드에 새 페이지를 등록한다 (부모도 꽉 찼으면 부모까지 분할)

한 번의 INSERT가 여러 페이지 쓰기 + 트리 상위 갱신으로 번진다. 그리고 쪼개진 두 페이지가 각각 절반만 차 있으므로 인덱스가 커지고 캐시 효율이 떨어진다(단편화).

단조 증가 키는 왜 유리한가

  • AUTO_INCREMENT · UUIDv7 · ULID — (계속 커지는 값)
    • 항상 트리의 '맨 끝' 페이지에만 추가된다
    • 중간을 쪼갤 일이 없다. 페이지가 꽉 차면 새 페이지를 뒤에 붙일 뿐

랜덤 UUIDv4

  • 삽입 위치가 트리 전체에 흩어진다
  • 곳곳에서 페이지 분할 → 쓰기 증폭 · 단편화

InnoDB에서 특히 아프다

InnoDB 는 테이블 자체가 PK 기준 B+Tree(클러스터형)다

  • PK 인덱스의 페이지 분할 = 행 데이터의 재배치
  • PostgreSQL(힙 + 별도 인덱스)보다 타격이 크다

"PK를 UUID로 바꿨더니 느려졌다"는 사례가 InnoDB에서 두드러지는 이유다.

대안

-- UUIDv7: 앞부분이 타임스탬프라 시간순으로 정렬된다
-- ULID:   같은 성질의 26자 문자열
-- 또는 내부 PK 는 BIGINT AUTO_INCREMENT, 외부 노출용 ID 만 UUID 로 둔다
CREATE TABLE member (
  id         BIGINT AUTO_INCREMENT PRIMARY KEY,   -- 내부용 · 작고 단조 증가
  public_id  CHAR(36) NOT NULL UNIQUE             -- 외부 노출용
);

마지막 방식이 실무에서 가장 흔한 절충이다 — 클러스터형 인덱스의 이점을 지키면서 ID 추측을 막는다.

면접 함정

  • "UUID PK는 어디서나 나쁘다" → PostgreSQL은 힙이라 영향이 훨씬 작다. 문제는 InnoDB + 랜덤 조합이다.
  • "페이지 분할은 조회를 느리게 한다" → 직접적으로는 쓰기 비용이고, 단편화를 통해 간접적으로 조회에 영향을 준다.

얼마나 비어 있는지 측정하기

-- PostgreSQL: 인덱스 내부 단편화
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstatindex('idx_member_pk');
-- avg_leaf_density: 90 이면 리프가 90% 차 있다 (건강)
-- avg_leaf_density: 55 이면 절반 가까이 비었다 (분할이 잦았다)

-- MySQL: 테이블·인덱스에 남은 여유 공간
SELECT table_name, data_free FROM information_schema.tables WHERE table_name='member';

avg_leaf_density가 낮으면 같은 데이터를 읽는 데 더 많은 페이지를 읽어야 하므로 조회까지 느려진다.

되돌리는 법

-- PostgreSQL: 인덱스만 온라인으로 다시 만든다 (락 최소)
REINDEX INDEX CONCURRENTLY idx_member_pk;

-- MySQL: 테이블을 다시 쓴다
OPTIMIZE TABLE member;      -- InnoDB 에서는 사실상 ALTER TABLE ... FORCE

둘 다 무거운 작업이라 트래픽이 적은 시간에 돌린다. 근본 해법은 애초에 단조 증가 키를 쓰는 것이다.

fillfactor로 미리 여유를 둔다

-- 갱신이 잦은 테이블은 페이지를 꽉 채우지 않고 여유를 남긴다
ALTER TABLE member SET (fillfactor = 80);   -- PostgreSQL

빈 공간을 남겨 두면 같은 페이지 안에서 갱신이 처리돼(HOT update) 분할과 인덱스 갱신을 피할 수 있다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 인덱스 — B+Tree·클러스터형·커버링(PostgreSQL·MySQL)