중급4분 분량

DynamoDB의 0 채우기 정렬 키

DynamoDB 문자열 정렬 키는 숫자가 아니라 사전식으로 — 왼쪽에서 오른쪽으로 한 글자씩 — 정렬됩니다. 그래서 "1""2"보다 앞서기 때문에 "10""2"보다 먼저 옵니다. 고정 너비로 0을 채우는 것이 문자열 순서를 숫자 순서와 일치하게 만드는 방법입니다.

DynamoDB 정렬 키에서 "10"이 "2"보다 먼저 정렬되는 이유는 무엇인가요?

DynamoDB 문자열 정렬 키는 숫자 크기가 아닌 UTF-8 바이트 순서로 사전식 비교를 합니다. "1"의 바이트가 "2"보다 앞서기 때문에 "10""2"보다 먼저 옵니다. 모든 숫자를 앞에 0을 붙여 고정 너비로 채우면 — "2""0000000002"가 되면 — 문자열 순서와 숫자 순서가 정확히 일치합니다.

  • 함정: 문자열로 저장된 숫자는 단어처럼 정렬됩니다. DynamoDB가 주는 순서는 "100", "11", "2"입니다 — 당신이 의도한 것이 아닙니다.
  • 해법: 모든 숫자를 앞에 0을 붙여 고정 너비로 채워, "2""0000000002"가 되게 합니다. 이제 사전식 순서와 숫자 순서가 일치합니다.
  • 너비는 한 번에 고르세요: 앞으로 저장할 가장 큰 값에 맞춰 잡고, 자릿수를 몇 개 더 보태세요. 나중에 너비를 바꾸면 모든 키를 다시 써야 합니다.
  • 내림차순은 거저: 높은 값에서 낮은 값 순(리더보드 케이스)으로 정렬하려면, maxValue - value를 역시 0으로 채워 저장하세요 — DynamoDB에는 속성별 정렬 방향이 없습니다.

문자열 정렬 키가 당신을 배신하는 이유

SQL에서 넘어왔다면 정수 컬럼에 대한 ORDER BY score DESC는 "그냥 됩니다" — 엔진이 그 컬럼이 숫자임을 압니다. DynamoDB에는 Number 타입이 아닌 정렬 키에 그런 호사가 없습니다.

DynamoDB는 문자열(S) 정렬 키를 UTF-8 바이트 순서로 비교합니다 (AWS 정렬 키 문서). 크기가 아니라 바이트입니다. "9"(0x39)는 첫 바이트가 "1"(0x31)을 이기기 때문에 "10"을 능가합니다. 길이는 무관합니다 — 처음으로 다른 바이트만이 결정합니다.

그것이 지뢰입니다: 숫자가 문자열 정렬 키 안에 사는 순간, 범위를 훑는 모든 Query가 뒤죽박죽처럼 보이는 순서로 행을 반환합니다.

리더보드 정렬 키 만들기

시즌제 아케이드 리더보드를 봅시다. 시즌당 하나의 아이템 컬렉션이 모든 플레이어의 기록을 담고, 최고 점수를 먼저 보고 싶습니다.

단일 아이템 컬렉션 안의 복합 키로 모델링하세요:

  • leaderboardId (파티션 키) — 예: SEASON#2026-SPRING.
  • rankKey (정렬 키) — 0으로 채운 점수에 타이브레이커를 더한 것.

순진한 첫 시도는 가공되지 않은 점수를 문자열로 저장합니다:

leaderboardIdrankKeyplayerHandle
SEASON#2026-SPRING"9"quickdraw
SEASON#2026-SPRING"10"ace_pilot
SEASON#2026-SPRING"1500"nightowl
SEASON#2026-SPRING"240"bytecrash

SEASON#2026-SPRING에 대한 Query는 이 바이트 순서로 반환합니다: "10", "1500", "240", "9". 9점짜리 기록이 꼴찌에 앉고 1500점짜리 기록이 가운데에 묻힙니다. 리더보드로는 쓸모없습니다.

고정 너비로 채우기

앞으로 기록할 가장 큰 점수에 충분히 넓은 너비를 고른 다음, 왼쪽을 0으로 채우세요. 점수가 천만에서 상한이라 하면 — 여덟 자리이므로, 여유를 위해 자리를 쓰세요:

leaderboardIdrankKeyplayerHandle
SEASON#2026-SPRING"0000000009"quickdraw
SEASON#2026-SPRING"0000000010"ace_pilot
SEASON#2026-SPRING"0000000240"bytecrash
SEASON#2026-SPRING"0000001500"nightowl

이제 모든 키가 같은 길이라, 바이트별 비교와 숫자 비교가 동일한 순서를 만들어 냅니다. 오름차순 Query9, 10, 240, 1500을 줍니다. 마침내 숫자가 바이트와 일치합니다.

너비는 일방통행 문입니다. 열 자리로 채웠는데 나중에 점수가 그것을 넘으면, 11자리 값이 10자리 값보다 먼저 정렬되어 — 모든 것을 다시 깨뜨리고 — 고치려면 기존의 모든 rankKey를 다시 써야 합니다. 너비를 넉넉히 잡으세요. 비용은 몇 바이트입니다.

내림차순 정렬: 차이를 저장하라

리더보드는 가장 높은 점수를 먼저 원합니다. DynamoDB는 ScanIndexForward: false로 정렬 키를 앞으로든 뒤로든 읽을 수 있으므로, 내림차순은 보통 읽기 시점 플래그입니다 — 먼저 그것에 손을 뻗으세요.

하지만 하나의 아이템 컬렉션이 혼합된 정렬 방향을 처리해야 하거나, 읽기 플래그와 무관하게 최고 점수가 물리적으로 먼저 오기를 원한다면, 숫자 자체를 뒤집으세요. maxValue - score를 같은 너비로 0을 채워 저장합니다:

score   inverted (9999999999 - score)   rankKey
1500    9999998499                       "9999998499"
240     9999999759                       "9999999759"
10      9999999989                       "9999999989"
9       9999999990                       "9999999990"

뒤집힌 값에 대한 오름차순 바이트 순서는 이제 원래 점수를 높은 순에서 낮은 순으로 산출합니다: 1500, 240, 10, 9. 이 트릭은 2007년 Amazon Dynamo 논문의 정신에 닿아 있습니다 — 키는 불투명한 바이트이므로, 의도를 바이트 안에 인코딩합니다.

타이브레이커 추가하기

두 플레이어가 동점일 수 있습니다. 0으로만 채운 점수는 정렬 키에서 충돌하고, 두 번째 쓰기가 첫 번째를 덮어씁니다(같은 PK + SK). 각 기록이 구별되는 아이템이 되고 동점이 결정적으로 해소되도록 고유한 접미사를 붙이세요:

rankKey = "<paddedScore>#<paddedTimestamp>#<playerId>"

예를 들어 "0000001500#0000001719100800#p_8842". 같은 점수에서 더 이른 타임스탬프가 더 높은 자리를 차지합니다 — 타임스탬프도 채우세요, 그러지 않으면 방금 고친 바로 그 버그를 다시 들여옵니다.

DynoTable에서 0으로 채운 rankKey로 정렬된 시즌 리더보드를 탐색하며 채워진 값들이 행을 올바르게 정렬하는 것을 확인할 수 있습니다 — 배포 전 너비가 올바르다는 증거입니다.

복합 키를 손으로 조립할 때는 너비를 잘못 입력하기 쉽습니다. "시즌 최상위" Query를 위한 KeyConditionExpressionexpression builder에서 생성하면 너비를 실험하는 동안 begins_with / between 구문을 정직하게 유지해 줍니다.

DynoTable에서 0으로 채운 rankKey로 정렬된 시즌 리더보드를 둘러보는 모습.
DynoTable에서 0으로 채운 rankKey로 정렬된 시즌 리더보드를 둘러보는 모습.

피해야 할 함정

  • 너무 좁게 채우기. 이 방식 전체는 값이 너비를 넘치는 첫 순간에 무너집니다. 최악의 경우에 맞춰 잡고, 자릿수를 더하세요.
  • 읽기 플래그 잊기. 항상 내림차순으로만 읽는다면 ScanIndexForward: false만으로 충분할 수 있습니다 — 플래그가 해주는 일을 두고 뒤집힌 키에 손을 뻗지 마세요.
  • 한 컬렉션에 혼합된 너비. 정렬 범위를 공유하는 모든 키는 같은 너비를 써야 합니다. 새 행은 채우고 옛 행은 채우지 않은 마이그레이션은 그것들을 잘못 끼워 넣습니다.
  • 잘못된 세그먼트 채우기. 복합 키에서는 정렬에 참여하는 모든 숫자 세그먼트를 채우세요 — 점수만이 아니라 점수와 타임스탬프 둘 다.

다음 단계

0 채우기는 더 넓은 정렬 키 설계 도구 상자 안의 한 도구입니다. 키를 오버로딩해 여러 패턴을 처리할 때는 아이템 컬렉션과 짝지으세요. 그리고 순서가 올바르게 잡히면 Scan 대신 정밀한 Query에 의지하세요.

DynoTable을 사용해 실제 테이블을 둘러보고, 스키마를 출시하기 전에 0으로 채운 정렬 키가 숫자 순서로 떨어지는 모습을 지켜보세요.

업데이트됨