SQLite에서 UUID 기본 키의 위험성

1 hour ago 1
  • 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 저장소에 공개
Read Entire Article