SQLite에서 UUID 기본 키의 위험성
WITHOUT ROWID로 구성된 SQLite 테이블에서 무작위 UUID(특히 UUIDv4)를 기본 키로 사용하면 심각한 성능 저하가 발생할 수 있으며, 정수 기본 키와 비교했을 때 삽입 속도가 최대 14-16배까지 떨어질 수 있습니다. 이는 무작위 UUID가 데이터베이스로 하여금 클러스터형 인덱스 B-tree를 지속적으로 재균형(re-balance)하도록 강제하여, 과도한 페이징과 디스크 I/O를 유발하기 때문입니다.
클러스터형 인덱스가 성능에 미치는 영향
클러스터형 인덱스에서 행의 물리적 저장 순서는 인덱스 키에 의해 결정됩니다. 행은 물리적으로 단 한 가지 방식으로만 정렬될 수 있으므로, 테이블은 단 하나의 클러스터형 인덱스만 가질 수 있습니다. 즉, 클러스터형 인덱스가 곧 테이블입니다.
SQLite에서 기본 동작은 rowid라고 불리는 암시적 64비트 정수 기본 키를 사용하는 것입니다. 테이블의 데이터는 이 rowid를 키로 사용하여 B-tree에 저장되므로, 이것이 클러스터형 인덱스가 됩니다. WITHOUT ROWID 최적화를 사용하여 테이블을 생성하면, 선언된 기본 키가 암시적 rowid 대신 클러스터형 인덱스가 됩니다.
벤치마크 결과: 정수 vs. UUIDv4 vs. UUIDv7
100만 개씩 배치로 1,000만 개의 행을 삽입하는 벤치마크는 SQLite에서 다양한 기본 키 전략의 성능 비용을 보여줍니다:
1. 기준점: 정수 기본 키 (표준 rowid)
삽입은 매우 효율적이며, 초당 약 100만 건의 삽입에 도달합니다. 테이블이 커짐에 따라 백만 행당 소요되는 시간은 일정하게 유지됩니다.
2. UUIDv4 WITHOUT ROWID
WITHOUT ROWID 테이블에서 무작위 UUIDv4를 기본 키로 사용하면 성능이 크게 떨어집니다. 테이블 크기가 커짐에 따라 이후 100만 행 배치 삽입에 걸리는 시간이 증가합니다 (예: 첫 100만 건은 2,649ms, 100번째 100만 건은 12,586ms).
이러한 성능 저하는 UUIDv4의 무작위성 때문에 발생하며, 이로 인해 SQLite가 B-tree에 행을 무작위로 삽입하도록 강제하여 지속적인 재균형과 페이징 증가를 유발합니다.
3. UUIDv7 WITHOUT ROWID
시간 순서로 정렬되는 UUIDv7을 사용하면 성능 문제를 대부분 해결할 수 있습니다. 삽입 시간은 합리적인 수준으로 돌아오지만 (백만 행당 평균 약 1,250ms), 정수 기준점보다는 약간 느립니다. 이는 정수 기본 키(8바이트)에 비해 UUID blob(16바이트)의 크기가 더 크기 때문입니다.
4. UUIDv4 WITH ROWID
UUIDv4를 기본 키로 사용하되 암시적 rowid(기본 테이블 유형)를 유지하면 WITHOUT ROWID보다 나은 성능을 보여줍니다. 클러스터형 인덱스가 순차적(rowid)이기 때문입니다. 하지만, 데이터베이스가 UUID 기본 키를 위한 비클러스터형 인덱스를 여전히 유지해야 하므로, 무작위 삽입 오버헤드가 발생하여 UUIDv7보다는 여전히 느립니다.
기본 키 전략 요약
| 전략 | 클러스터형 인덱스 | 성능 | 트레이드오프 |
|---|---|---|---|
| Integer PK | 순차적 rowid |
가장 빠름 | 행 개수/이웃 ID 노출 |
| UUIDv7 (WITHOUT ROWID) | 시간 순서 UUID | 빠름 | 생성 타임스탬프 노출 |
| UUIDv4 (WITH ROWID) | 순차적 rowid |
보통 | 쓰기 증폭 (인덱스 2개) |
| UUIDv4 (WITHOUT ROWID) | 무작위 UUID | 가장 느림 | 심각한 B-tree 재균형 오버헤드 |
커뮤니티 인사이트 및 대안
이 벤치마크에 관한 기술적 논의에서는 UUID를 기본 키로 사용하는 것에 대한 몇 가지 아키텍처적 대안을 강조합니다:
- 하이브리드 접근 방식: 많은 개발자들은 조인(join)과 조회를 위한 내부 기본 키로는 순차적 정수를 사용하고, 공개용 식별자로 별도의 UUID 컬럼을 것을 권장합니다. 이는 무작위 키의 성능 문제를 피하면서 API를 위한 불투명한 ID를 제공합니다.
- 분산 시스템 요구사항: 정수는 단일 인스턴스 데이터베이스에 선호되지만, UUIDv7은 여러 애플리케이션 인스턴스 간에 데이터를 병합하거나 충돌 없이 장치 간에 동기화해야 하는 시스템에서 필수적인 도구로 간주됩니다.
- 보안 고려사항: 삽입 성능이 주요 제약 사항이 아닌 경우, 생성 시간이나 행 개수를 엄저히 숨겨야 한다면 UUIDv4가 여전히 유용할 수 있다고 주장하는 이들도 있습니다.
"무작위 ID 기본 키는 UU 종류든 SQ 종류든, 혹은 다른 어떤 종류든 간에 그냥 나쁜 아이디어입니다. 제 DB 지식에 따르면, 이 부류의 ID는 모든 트리 알고리즘을 파괴합니다..."
"UUIDv7과 순차적 정수는 매우 유사합니다. 순차적 정수는 개수와 이웃 ID를 노출하는 반면, UUIDv7은 타임스탬프를 노출합니다."
결론
SQLite 사용자라면, UUID를 구현하는 가장 효율적인적인 방법은 WITHOUT ROWID 테이블에서 시간 순서로 정렬되는 UUIDv7을 사용하는 것입니다. 만약 절대적으로 가장 높은 성능이 필요하다면 표준 정수 rowid를 사용하십시오. 무작위 UUIDv4를 클러스터형 인덱스로 사용하는 것은 피하십시오. 데이터셋의 크기가 커짐에 따라 성능 페널티가 선환적으로 증가하기 때문입니다.