DynamoDB의 싱글 테이블 디자인
SQL에서 온 본능은 엔터티당 테이블 하나입니다: customers, orders, order_items. DynamoDB에서 그 본능은 대개 틀렸습니다. 오버로딩된 키 접두사로 구분하여 모든 엔터티를 저장하는 단일 테이블은, 부모와 그 자식을 하나의 Query로 가져오게 해 줍니다 — 조인도, N+1도 없습니다.
엔터티가 아니라 액세스 패턴에서 시작하세요
싱글 테이블 디자인은 액세스 패턴 우선입니다. 단일 키를 고르기 전에, 앱이 수행하는 모든 읽기를 적어 두세요 — "고객 프로필 가져오기", "고객의 주문을 최신순으로 나열하기", "모든 열린 주문 찾기" — 키는 오직 그 목록을 위해 존재하기 때문입니다. 관계형 정규화는 저장을 최적화하지만, DynamoDB 모델링은 이미 실행할 것을 아는 쿼리를 최적화합니다. 그것들을 열거한 뒤, 각각이 단일 Query가 되도록 키를 설계하세요.
핵심 아이디어
일반적인 키 이름(PK, SK)을 고르고 엔터티 타입을 값에 인코딩하세요:
| PK | SK | attributes |
|---|---|---|
| CUSTOMER#42 | PROFILE | name, email, plan |
| CUSTOMER#42 | ORDER#2026-001 | total, status |
| CUSTOMER#42 | ORDER#2026-002 | total, status |
이제 하나의 Query PK = "CUSTOMER#42"가 단일 과금 읽기로 프로필 과 모든 주문을 반환합니다. SK begins_with "ORDER#"는 이를 주문으로만 좁힙니다.
시각적으로, 오버로딩된 항목들은 하나의 파티션 키 아래에 단일 항목 컬렉션으로 쌓입니다:
파티션을 한 번 읽으면 고객과 모든 주문을 함께 돌려받습니다.
오버로딩된 GSI
같은 기법이 인덱스에도 통합니다. 항목에 일반적인 GSI1PK/GSI1SK를 두면, 각 항목이 그 속성에 무엇을 쓰느냐에 따라 단일 GSI가 여러 액세스 패턴을 처리합니다:
| PK | SK | GSI1PK | GSI1SK |
|---|---|---|---|
| ORDER#001 | METADATA | STATUS#OPEN | 2026-01-04 |
| ORDER#002 | METADATA | STATUS#OPEN | 2026-01-05 |
이제 Query GSI1 WHERE GSI1PK = "STATUS#OPEN"은 열린 주문을 날짜순으로 나열합니다 — 기본 테이블은 답할 수 없는 패턴입니다. 다른 엔터티는 자체 의미(예: CATEGORY#books)로 GSI1을 재사용할 수 있습니다. 하나의 인덱스, 여러 쿼리.
다대다: 인접 리스트
관계(여러 팀에 속한 사용자, 여러 사용자를 가진 팀)에 대해서는, id를 바꿔 가며 엣지를 두 번 쓰세요: PK=USER#1, SK=TEAM#9와 PK=TEAM#9, SK=USER#1. 어느 쪽이든 쿼리하면 반대쪽을 나열합니다 — 조인 테이블의 DynamoDB 대체물입니다.
싱글 테이블이 아닐 때
공짜는 아닙니다. 오버로딩된 하나의 테이블은 추론하기 더 어렵고, 발전시키기 더 어렵고, 분석에 비우호적입니다. 액세스 패턴이 정말로 알려지지 않았거나 끊임없이 바뀌거나, 데이터가 대부분 분석용이라면, 별도 테이블(또는 다른 저장소)이 더 합리적인 선택일 수 있습니다. 싱글 테이블은 패턴이 알려져 있고 대량일 때 이깁니다.
잘못된 형태의 비용
별도 테이블로 모델링하면 고객을 재조립하기 위해 Scan이나 클라이언트 측 조인을 강제하게 되며, 그것이 Scan 함정입니다. 액세스 패턴을 먼저 모델링한 뒤, 각각이 Query가 되도록 키를 설계하세요.
이 항목들이 읽기당 드는 비용은 항목 크기 및 용량 계산기로 추정하고, DynoTable을 사용해 싱글 테이블 schema를 탐색하고 오버로딩된 컬렉션을 나란히 확인하세요.