UUID v1은 정렬되지 않는다
시간으로 만든 식별자인데 문자열로 정렬하면 순서가 뒤섞입니다. v1·v6·v7의 차이를 실행 결과로 정리했습니다.
UUID v1은 시각에서 만듭니다. 그러니 정렬하면 시간순일 것 같습니다.
100초 간격으로 여섯 개를 만들어 봤습니다.
12:00:00 53866000-a46a-11f1-8a1b-0242ac120002
12:01:40 8f212a00-a46a-11f1-8a1b-0242ac120002
12:03:20 cabbf400-a46a-11f1-8a1b-0242ac120002
12:05:00 0656be00-a46b-11f1-8a1b-0242ac120002
12:06:40 41f18800-a46b-11f1-8a1b-0242ac120002
12:08:20 7d8c5200-a46b-11f1-8a1b-0242ac120002
문자열로 정렬하면 이렇게 됩니다.
12:05:00 0656be00-a46b-…
12:06:40 41f18800-a46b-…
12:00:00 53866000-a46a-…
12:08:20 7d8c5200-a46b-…
12:01:40 8f212a00-a46a-…
12:03:20 cabbf400-a46a-…
순서가 완전히 뒤섞였습니다.
필드 순서가 뒤집혀 있다
v1의 앞 세 필드는 이렇습니다.
53866000-a46a-11f1-8a1b-0242ac120002
^^^^^^^^ ^^^^ ^^^^
time_low mid ver+hi
타임스탬프의 낮은 자리가 맨 앞에 옵니다. 문자열 정렬은 앞에서부터 비교하므로, 가장 빨리 변하는 부분을 먼저 봅니다.
time_low는 32비트이고 단위가 100나노초입니다.
2^32 / 10^7 = 429.5초 = 7.2분
7분 남짓마다 한 바퀴 돌아 0으로 돌아옵니다. 그 순간 정렬 순서가 끊깁니다. 위 예에서 cabbf400(12:03) 다음이 0656be00(12:05)이 된 자리가 그것입니다.
v6은 같은 값을 뒤집는다
12:00:00 1f1a46a5-3866-6000-8a1b-0242ac120002
12:01:40 1f1a46a8-f212-6a00-8a1b-0242ac120002
12:03:20 1f1a46ac-abbf-6400-8a1b-0242ac120002
12:05:00 1f1a46b0-656b-6e00-8a1b-0242ac120002
12:06:40 1f1a46b4-1f18-6800-8a1b-0242ac120002
12:08:20 1f1a46b7-d8c5-6200-8a1b-0242ac120002
정렬 순서가 생성 순서와 같습니다.
v6은 새 알고리즘이 아닙니다. v1과 같은 타임스탬프를 높은 자리부터 배치한 것뿐입니다. time_hi | time_mid | time_low 순서입니다. 그래서 v1 값을 v6으로 재배열할 수 있고, 반대도 됩니다.
v7은 여기서 더 나아가 그레고리력 기준 100나노초 대신 유닉스 밀리초를 씁니다. 앞 48비트가 밀리초, 나머지가 무작위입니다. 계산이 단순하고 다른 시스템의 타임스탬프와 바로 비교됩니다.
정렬이 왜 중요한가
B-트리 인덱스는 새 값이 끝에 붙을 때 가장 쌉니다. 무작위 값이 들어오면 페이지 곳곳이 갈라지고, 캐시에 없는 페이지를 계속 읽어야 합니다.
MySQL InnoDB처럼 기본 키가 클러스터드 인덱스인 경우 특히 그렇습니다 — 행 데이터 자체가 기본 키 순서로 저장되므로, 무작위 기본 키는 삽입마다 다른 페이지를 건드립니다.
v4(완전 무작위)와 v1(뒤섞인 순서)이 여기서 같은 문제를 냅니다. v6·v7은 끝에 붙습니다.
v1이 노출하는 것
v1에서 시각을 되꺼낼 수 있습니다.
53866000-a46a-11f1-… → 2026-08-30T12:00:00.000Z
time_hi + time_mid + time_low를 이어 붙이면 1582년 10월 15일부터 100나노초 단위로 센 값이고, 여기서 유닉스 시각으로 옮기면 됩니다.
마지막 필드인 노드는 규격상 MAC 주소를 쓰게 되어 있었습니다. 그러면 하나의 UUID에서 두 가지가 나옵니다 — 어느 기계에서, 언제 만들었는지.
2001년 Melissa 웜 작성자를 특정한 단서가 Word 문서에 남은 UUID의 MAC 주소였습니다.
요즘 구현은 대체로 무작위 노드 ID를 씁니다. 규격도 그것을 허용합니다. 다만 옛 데이터에 남은 v1은 여전히 읽힙니다.
노드가 0242ac...로 시작하면 도커 컨테이너 대역입니다 — 기계를 특정하지는 못하지만 컨테이너에서 생성됐다는 것은 드러납니다.
무엇을 고르나
| 상황 | 선택 |
|---|---|
| 새 기본 키 | v7 |
| 정렬이 필요 없고 시각도 감추고 싶다 | v4 |
| 이름에서 결정론적으로 | v5 |
| v1 데이터와 호환이 필요 | v6 |
| 새로 v1 | 이유가 없다 |
확인 순서
- 기본 키가 v4나 v1이면 삽입 성능을 재 봅니다. 테이블이 커진 뒤에 드러납니다
- v1을 생성하고 있다면 노드 ID가 MAC인지 무작위인지 확인합니다
- 외부에 나가는 식별자에 v1을 쓰면 생성 시각이 함께 나갑니다. 감춰야 할 정보인지 봅니다
- 정렬 가능한 식별자가 필요한데 v7을 못 쓰면 ULID도 같은 자리입니다
- 로그에 남은 v1에서 시각을 뽑으면 사고 시각 추정에 쓸 수 있습니다
tools.onuel.dev의 UUID v1 생성기는 무작위 노드 ID로 만들어 MAC 주소가 새지 않게 합니다. UUID v6에서 같은 값이 시간순으로 재배열되는 것을 볼 수 있고, UUID v7과 ULID가 지금 고를 자리입니다. 무작위가 필요하면 UUID v4, 이름 기반이면 UUID v5입니다.
- #UUID
- #식별자
- #데이터베이스