UUID vs bigint 기본 키: 성능 차이의 실체와 uuidv7

4 분 소요

단일 DB에서 시작하는 보통의 서비스라면 기본 키는 bigint GENERATED ALWAYS AS IDENTITY가 기본값이고, UUID가 필요한 요건(분산 생성·외부 노출)이 있을 때만 uuidv7을 씁니다. 기초 2편에서 표 한 장으로 정리했던 결론인데, 이 글은 그 근거, 즉 “UUID가 느리다"는 말의 실체가 무엇이고 uuidv7이 무엇을 바꿨는지를 파는 심화 편입니다.

bigint가 기본값인 이유 #

  • 크기: bigint 8바이트 vs UUID 16바이트. PK 자체보다 중요한 것은 파급입니다. PK는 모든 보조 인덱스에 포함되고, FK 컬럼으로 자식 테이블마다 복제됩니다. PK가 2배면 인덱스 전반이 함께 커지고, 캐시에 들어가는 인덱스 양이 그만큼 줄어듭니다.
  • 삽입 지역성: IDENTITY 채번은 단조 증가라서 새 행이 항상 B-tree의 오른쪽 끝에 붙습니다. 쓰기가 닿는 페이지가 소수로 유지되어 캐시 적중이 높고 WAL도 적게 나옵니다.
  • 가독성: 로그와 장애 대응에서 id = 4213은 다루기 쉽습니다. 사소해 보여도 운영에서 매일 체감하는 차이입니다.

“UUID가 느리다"의 실체: v4의 무작위성 #

UUID 자체가 느린 것이 아니라 UUID v4의 무작위성이 느립니다. v4는 완전 무작위라 새 행이 B-tree의 아무 데나 꽂힙니다. 그 결과가 세 가지입니다. 삽입마다 인덱스의 서로 다른 페이지를 건드리므로 캐시 적중이 무너지고(인덱스가 메모리보다 커지는 순간 삽입마다 디스크 읽기가 섞입니다), 페이지 분할이 잦아져 인덱스가 부풀고, 건드린 페이지마다 기록이 필요해 WAL 양이 늘어납니다. 수백만 행 규모부터 삽입 처리량 차이가 실측으로 드러나는 이유입니다. 범위 조회(“최근 가입자순”)를 PK로 할 수 없다는 것도 덤으로 따라오는 손실입니다.

uuidv7: 무작위성 문제를 시간순으로 해소 #

uuidv7은 앞 48비트에 밀리초 타임스탬프를 넣고 나머지를 난수로 채웁니다. 생성 시각순으로 정렬되는 UUID라서, 새 행이 bigint처럼 인덱스 오른쪽 끝 근처에 모여 붙습니다. v4의 캐시·페이지 분할·WAL 문제가 대부분 사라지고, PostgreSQL 18부터는 내장 함수로 씁니다.

orders 테이블 생성
CREATE TABLE orders (
    id         uuid PRIMARY KEY DEFAULT uuidv7(),
    user_id    bigint NOT NULL REFERENCES users (id),
    created_at timestamptz NOT NULL DEFAULT now()
);

남는 트레이드오프도 정확히 알아 둘 필요가 있습니다. 16바이트라는 크기는 그대로이므로 인덱스·FK 파급은 여전하고, 생성 시각이 ID에 노출됩니다(v4를 고르는 이유가 “추측 불가"였다면, v7은 시각 부분이 추측 가능해지는 절충입니다. 대부분의 서비스에서 문제가 아니지만, 생성 시각 자체가 민감한 도메인이면 고려 대상입니다). 순번 노출로 개수를 유추당하는 bigint의 문제는 없습니다.

판단 기준 #

조건권장
단일 DB, 내부 서비스, 보통의 규모bigint IDENTITY
클라이언트·여러 서비스에서 ID를 미리 생성uuidv7
URL 등 외부에 노출되는 ID의 추측 방지uuidv7 (또는 아래 병행)
순번도 시각도 노출 불가UUID v4 + 성능 비용 수용

병행 설계도 실무의 유력한 답입니다. 내부 PK·FK·조인은 bigint로 두고, 외부 노출용 public_id uuid DEFAULT uuidv7() UNIQUE 컬럼을 따로 두는 형태입니다. 조인 성능과 노출 안전을 둘 다 가져가는 대신 컬럼과 인덱스가 하나씩 늘어나는 비용을 치릅니다. API 응답과 URL에는 public_id만 내보내면 됩니다.

마지막으로 흔한 오해 하나. “나중에 샤딩할지 모르니 처음부터 UUID"는 대부분 과잉 설계입니다. 그 시점이 오면 파티셔닝이나 병행 설계로의 이행 경로가 있고, 오지 않을 미래를 위해 현재의 모든 조인과 인덱스에 세금을 내는 것은 순서가 뒤집힌 결정입니다.

정리 #

  • 기본값은 bigint IDENTITY입니다. 크기의 파급(모든 인덱스·FK)과 삽입 지역성이 근거입니다.
  • “UUID가 느리다"의 실체는 v4의 무작위 삽입입니다. 캐시 붕괴·페이지 분할·WAL 증가가 실측되는 메커니즘입니다.
  • uuidv7은 시간순 정렬로 그 문제를 대부분 해소했고 PostgreSQL 18에 내장됐습니다. 크기 16바이트와 생성 시각 노출은 남는 절충입니다.
  • 분산 생성·외부 노출 요건이 있으면 uuidv7, 둘 다 잡고 싶으면 내부 bigint + 외부 uuidv7 병행 설계입니다.
  • 미래의 샤딩을 위한 선제 UUID는 대부분 과잉입니다. 현재의 워크로드로 결정하세요.
X