# SQLite에서 UUID 기본 키의 위험성

> Clean Markdown view of GeekNews topic #30254. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=30254](https://news.hada.io/topic?id=30254)
- GeekNews Markdown: [https://news.hada.io/topic/30254.md](https://news.hada.io/topic/30254.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-06-07T20:01:53+09:00
- Updated: 2026-06-07T20:01:53+09:00
- Original source: [andersmurphy.com](https://andersmurphy.com/2026/06/05/the-perils-of-uuid-primary-keys-in-sqlite.html)
- Points: 1
- Comments: 1

## Topic Body

- SQLite의 기본 키 구현은 일반 rowid 테이블과 WITHOUT ROWID 테이블에서 물리 저장 순서를 달리 만들며, **랜덤 UUID4**를 클러스터드 인덱스로 쓰면 B-tree 재균형과 추가 페이징 비용 발생
- 정수 rowid 기준선은 100만 행 단위 삽입에서 대략 초당 100만 건 수준이며, **UUID4 WITHOUT ROWID**는 14~16배 느린 삽입 시간 기록
- UUID4의 무순서 특성은 행을 B-tree에 무작위로 삽입하게 만들고, 프로파일 결과에서 트리 균형 조정과 읽기·쓰기에 더 많은 시간 사용
- **UUID7 WITHOUT ROWID**는 시간 순서 UUID로 UUID4의 정렬 문제를 줄여 더 합리적인 삽입 시간을 보였지만, 16바이트 BLOB 키라 8바이트 정수 키보다 여전히 느림
- UUID4 WITH ROWID는 숨은 rowid의 순차성을 얻지만 두 인덱스로 인한 **쓰기 증폭**과 랜덤 인덱스 삽입 비용이 남아 UUID7 WITHOUT ROWID보다 낮은 성능

---

### 클러스터드 인덱스란?
- 클러스터드 인덱스는 테이블 행의 물리적 저장 순서를 결정하는 인덱스
- 행은 물리적으로 한 가지 방식으로만 정렬될 수 있어 테이블마다 클러스터드 인덱스는 하나만 존재
- 클러스터드 인덱스는 테이블 자체이며, 비클러스터드 인덱스는 인덱싱된 컬럼과 실제 행 데이터가 있는 위치를 가리키는 포인터만 저장

### Rowid
- 모든 일반 SQLite 테이블은 `rowid`라는 암묵적 64비트 정수 기본 키 보유
- 테이블 데이터는 각 행마다 하나의 엔트리를 가진 B-tree 구조에 저장되며, `rowid` 값을 키로 사용
- `rowid`는 사실상 SQLite의 클러스터드 인덱스이며, 행의 물리적 저장 순서는 `rowid` 순서

### WITHOUT ROWID
- SQLite는 `WITHOUT ROWID` 테이블을 지원하며, 이 테이블에는 암묵적 `rowid` 부재
- `WITHOUT ROWID` 테이블에서는 선언한 기본 키가 클러스터드 인덱스 역할
- SQLite `rowid` 테이블은 모든 콘텐츠가 리프에 저장되는 B*-Tree로 구현되고, `WITHOUT ROWID` 테이블은 리프와 중간 노드 모두에 콘텐츠를 저장하는 일반 B-Tree 사용

### 기준선: rowid 정수 기본 키
- 기준선은 `id INTEGER PRIMARY KEY, data BLOB` 구조의 일반 rowid 테이블에서 100만 행 단위 삽입 시간 측정
- 결과 표의 총 행 수는 1천만 행부터 1억 행까지이며, 측정 시간은 692ms에서 838ms 범위
- 기준선 성능은 대략 초당 100만 건 삽입 수준

### UUID4 WITHOUT ROWID
- UUID4 테스트는 `id BLOB PRIMARY KEY, data BLOB` 구조의 `WITHOUT ROWID` 테이블에서 `random-uuid4-bytes` 값을 기본 키로 삽입
- 결과 표의 측정 시간은 1천만 행에서 2649ms, 1억 행에서 12586ms
- 삽입 성능은 정수 rowid 기준선보다 14~16배 느린 수준

### 프로파일
- 정규화 diffgraph는 INTEGER와 UUID4 프로파일링 스냅샷을 비교하고, flamegraph 구조로 차이를 표시
- 정규화 뷰는 두 프로파일의 전체 샘플 수를 같게 조정해 상대적 차이를 백분율로 확인하게 하는 방식
- 파란 프레임은 두 번째 프로파일인 UUID4에서 해당 함수 시간이 INTEGER보다 줄어든 경우, 빨간 프레임은 UUID4에서 더 늘어난 경우
- 색 강도는 해당 프레임 자체의 샘플 수 변화, 즉 self time delta의 상대적 변화
- diffgraph에서는 트리 균형 조정과 읽기·쓰기에 더 많은 시간 사용
- UUID4의 무순서 특성 때문에 키가 무작위 순서로 정렬되고, SQLite가 B-tree를 계속 재균형화하는 구조

### UUID7 WITHOUT ROWID
- UUID7은 시간 순서 UUID이며, UUID4의 정렬 문제를 제거할 수 있는 방식
- UUID7 테스트도 `id BLOB PRIMARY KEY, data BLOB` 구조의 `WITHOUT ROWID` 테이블에서 실행
- 결과 표의 측정 시간은 1천만 행에서 1372ms, 1억 행에서 1258ms
- UUID7 WITHOUT ROWID는 UUID4 WITHOUT ROWID보다 합리적인 수치로 돌아왔지만, 기준선보다는 여전히 느린 성능
- UUID BLOB 기본 키는 16바이트이고 정수 기본 키는 8바이트라는 차이

### UUID4 WITH ROWID
- UUID4 WITH ROWID 테스트는 `WITHOUT ROWID` 없이 `id BLOB PRIMARY KEY, data BLOB` 테이블을 사용
- 이 구성에서는 숨은 `rowid`가 클러스터드 인덱스이며, `rowid`의 장점은 순차성
- 단점은 테이블에 인덱스가 두 개 생기며, 그로 인한 쓰기 증폭
- 결과 표의 측정 시간은 1천만 행에서 2003ms, 1억 행에서 7119ms
- UUID4 WITH ROWID는 UUID7 WITHOUT ROWID만큼 성능이 좋지 않으며, 클러스터드 인덱스가 아니더라도 랜덤 삽입으로 인덱스를 계속 구축해야 하는 구조

### 결론
- SQLite에서 UUID 기본 키는 클러스터드 인덱스와 키 정렬성에 따라 삽입 성능이 크게 달라질 수 있는 선택지
- 랜덤 UUID 문제는 SQLite에만 한정되지 않고, 클러스터드 인덱스를 사용하는 다른 데이터베이스에도 확장되는 문제
- 전체 벤치마크 코드는 [GitHub 저장소](https://github.com/andersmurphy/clj-cookbook/tree/master/sqlite-perils-of-uuid)에 공개

## Comments



### Comment 59078

- Author: neo
- Created: 2026-06-07T20:01:54+09:00
- Points: 1

###### [Lobste.rs 의견들](https://lobste.rs/s/76plqm/perils_uuid_primary_keys_sqlite) 
- 좋음. **rowid 테이블**에서 정수 키가 단조 증가가 아니라 무작위일 때의 수치도 보면 흥미로울 듯함  
  글에서 빠진 중요한 점은 rowid 테이블의 기본 인덱스는 B+트리이고, `without rowid` 테이블은 B트리라는 점임  
  그래서 일반적으로 평균 레코드 크기가 어느 임계값을 넘으면 후자가 이상적이지 않음. 인덱스 내부 노드가 전체 레코드를 저장하기 때문이고, 기억하기로 SQLite 매뉴얼은 페이지 크기의 **1/20**을 경험칙으로 제시함

- UUID 성능을 측정하는 데 그렇게 공을 들였는데 **자연 키**는 고려하지 않았음  
  정수든 UUID든 다른 형태든 **대리 키**는 복잡성을 더하고, 정보를 추가하지 않으며, 정규화 오류를 가림  
  글쓴이는 UUID를 쓰는 이유로 “중복 방지”를 들지만, 그건 이유가 아님. 모든 데이터베이스의 모든 테이블에서 모든 행은 그 행을 유일하게 식별하는 열 묶음인 키를 최소 하나 가져야 함  
  그런 키가 없는 데이터베이스는 중복 정보를 품고 논리적 불일치에 취약함. 그런 키가 이미 있는 데이터베이스에는 대리 키가 필요 없음. 자연 키가 이미 존재하고 강제되고 있다면 “성능 때문에 필요하다”는 주장은 추가 비용과 불필요한 복잡성을 뜻하므로 증명이 필요함
  - 상상력이 부족한 걸 수도 있지만, 현실 세계에서 온 데이터를 다룰 때는 진짜 **자연 키**를 찾기가 대체로 어렵다 봄  
    겉보기엔 유일해 보이는 값이 기대만큼 유일하지 않거나, 불변이라고 예상한 값이 결국 바뀌어야 하는 일이 생김  
    반면 대리 키를 쓰면 다른 사람이 식별성을 어떻게 정의했는지, 혹은 보통 제대로 정의하지 않았는지에 기대지 않고 내 시스템 안에서 정체성을 정의할 수 있음  
    예외는 있음. 전체 모델을 내가 정의하고, 데이터가 이른바 현실 세계에서 오지 않는다면 자연 키가 더 말이 됨. 하지만 완전히 정규화된 적이 없을 법한 현실 데이터를 담는 스키마를 설계할 때는 대리 키가 유용한 경우가 많음
  - 단조 증가하는 정수 **대리 키**에는 즉각적인 실용성이 꽤 있음  
    어떤 행이든 정수로 참조할 수 있어 접근이 크게 단순해지고, 사람이 기억하고 쿼리에 쓰기에도 쉬움  
    단조 증가한다면 그 자체로 정보도 담고 있음. 중복 정보이긴 하지만 사실임  
    조회 속도도 최적화됨. B트리, 비트맵 등으로 인덱싱하기에 최적에 가까운 경우임  
    사람들이 UUID를 주로 쓰는 건 대체로 혼란 때문이라고 봄. 보통 키를 난독화하고 추측 불가능하게 만들자는 논리인데, 왜 그 목적을 위한 별도 식별자가 아니라 **기본 키**에까지 강요해야 한다고 생각하는지는 이해를 포기했음
  - “중복 방지”라는 표현은 블로그 글에 나오지 않음. 사실 글에서는 UUID를 왜 쓰는지 자체를 전혀 말하지 않았음

- **UUID 버전 7**은 앞부분에 48비트 타임스탬프가 있어서 이런 식으로 무작위 분포가 되지 않음. 그래서 과도한 페이징과 재균형도 줄어들 것임
  - 맞음. 글에서 다루는 게 UUID7임

- 사람들이 정말 UUID를 **기본 키**로 쓰고 있나? UUID가 필요할 때 보조 키로 두는 대신 그렇게 하는 이점이 뭔지 궁금함
  - 규모 확장 때문임. 많은 컴퓨터와 전 세계 여러 데이터센터에서 높은 속도로, 예를 들어 **S3 업로드**처럼 유일 ID를 분산 생성하려면 단일 증가 정수에 잠금을 걸고 싶지 않음. 잠금은 전 세계적으로 동기화하기 느림  
    GUID와 UUID는 구조적으로 이 문제를 해결함  
    v1과 v6은 기계 ID와 타임스탬프를 인코딩하므로, 각 기계별 이름공간을 가진 자동 증가 정수에 가까움  
    많은 사람이 UUID가 무작위라고 가정하는 데서 혼란이 생김. 그건 v4에만 해당하고, v4 선택에는 불행히도 비용이 있음  
    데이터를 정리해 주거나 성능·충돌 보장을 위해 v3, v5, v7처럼 어느 정도의 **결정성**이 필요한 때가 많음
  - 무작위 UUID나 균등 분포의 임의 값을 보조 키에 두더라도 삽입은 여전히 느려짐. 클러스터드 인덱스는 아니어도 그 인덱스의 **B트리**에는 여전히 무작위 삽입이 일어나기 때문임
