Vertical Partitioning(수직 파티셔닝)은 단일 테이블 설계의 핵심 원칙을 구현하는 방법론이다. Item을 더 작은 데이터 청크로 나누고, 함께 액세스해야 하는 데이터를 하나의 파티션 키 아래에 논리적으로 그룹화하여, 성능과 확장성 및 비용 효율성을 높이는 데에 중점을 둔다.
: 동일한 Partition Key 값을 공유하지만, 서로 다른 Sort Key 값을 갖는 항목들의 그룹
| Partition key: user_id | Sort key: update_timestamp | Attribute: new_state | Attribute: entity_type |
|---|---|---|---|
| aman_dhingra | 2024-01-13T14:37:07.999Z | {"First Name":{"S":"Aman"},"CityPreference":{"S":"London"}} | profile_update |
| aman_dhingra | 2024-04-15T11:35:57.557Z | {"First Name":{"S":"Aman"},"CityPreference":{"S":"Dublin"}} | profile_update |
| aman_dhingra | 2024-07-10T20:34:48.296Z | {"First Name":{"S":"Aman"},"CityPreference":{"S":"Dubai"}} | profile_update |
| lorenzo_ripani | 2023-10-29T22:35:42.427Z | {"First Name":{"S":"Lorenzo"},"CityPreference":{"S":"Milan"}} | profile_update |
위에서 user_id: aman_dhingra를 갖는 모든 항목들의 그룹이 Item Collection이다.
항목 컬렉션에 저장된 데이터를 읽을 때는 DynamoDB의 Query API를 사용할 수 있다.
begins_with): 정렬 키 값에 대한 다양한 연산자들을 지원하며, 그중 begins_with() 연산자는 항목 컬렉션 내에서 원하는 데이터 그룹을 효율적으로 검색하는 데 필수적이다.
begins_with()
ISO 8601 타임스탬프(YYYY-MM-DDThh:mm:ssZ)를 정렬 키로 사용하는 경우,sort_key begins_with('2024-01')와 같은 쿼리 조건을 사용하여 2024년 1월에 발생한 모든 변경 이력을 가져올 수 있다. 또한 엔티티 이름을 접두사로 사용할 수도 있다. (ex:o#12,,p#34)
일종의 Group By인 셈이다.
| Partition key: PK | Sort key: SK | Attr: type | Attr: title | Attr: artist | Attr: count | Attr: user |
|---|---|---|---|---|---|---|
| s#song1 | A | details | How Much Is The Fish? | Scooter | ||
| s#song1 | A#downloads | total_downloads | 100 | |||
| s#song1 | d#download-1 | download | amdhing | |||
| s#song1 | d#download-2 | download | ripani |
추가로 위 예시처럼, 항목 컬렉션은 다양한 엔티티의 데이터(1:1, 1:m, m:n 관계)를 같은 테이블 내에 함께 저장할 수 있게 해준다. song_id를 PK로 공유하는 항목 컬렉션에는 노래 세부 정보, 총 다운로드 수, 그리고 개별 다운로드 기록과 같은 여러 엔티티가 포함될 수 있다.
(다운로드 수를 같이 두지 않은 이유는 곡 정보와 같이 두게 되면 업데이트 비용이 커지기 때문)
키 네임 오버로딩:
항목 컬렉션을 사용해 여러 엔티티를 저장하려면, 파티션 키와 정렬 키에PK,SK와 같은 일반적인 이름을 사용하는 것이 일반적인 모범 사례이다. 이를 키 네임 오버로딩이라고 하며, 항목 컬렉션이 최대 두 개의 엔티티(PK, SK)에만 국한되지 않고, 정렬 키 값만 잘 설계해 한 파티션 안에 여러 타입의 엔티티 넣을 수 있게 된다.
사실 위에서 알아본 것이 데이터 분해이다.
대용량 JSON 구조를 더 작은 청크로 분해하고, 이들이 동일한 항목 컬렉션의 일부가 되도록 구성해두면, 한 번의 쿼리(Query)로 관련 데이터를 전부 효율적으로 가져올 수 있다.
단 이러한 접근 방식은 읽기 효율성을 위해 데이터 비정규화 및 중복을 수반한다. 쓰기 단계에서 여러 번의 쓰기 작업이 필요할 수도 있지만, 이는 읽기 효율성 최적화를 위한 의도적인 트레이드오프이다.
또한 DynamoDB의 400 KiB 항목 크기 제한도 극복할 수 있겠죠?
만약 하나의 아이템에 아래와 같은 정보를 다 넣는다고 생각해보자.
(위 표 참고) 노래 제목, 아티스트, 다운로드 횟수, 모든 다운로드 ID 목록
이렇게 하면, 예를 들어 다운로드 ID로 노래를 찾거나, 아티스트 또는 타이틀로 찾으려면 GSI가 여러 개 필요해진다. (예: downloadId로 인덱스 1개, artist로 인덱스 1개, title로 인덱스 1개 등)
GSI를 많이 만드는 게 성능에 크게 문제를 주진 않지만, 관리해야 할 리소스가 늘어난다.
메인 테이블과 별개로, 자체적인 throughput, capacity, autoscaling 등이 존재하기 때문에 각 인덱스마다 설정·모니터링·비용 관리가 필요하다.
그런데 GSI1PK에 아티스트나 다운로드 ID, 타이틀 등을 저장해두면, 하나의 GSI만으로도
해당 속성으로 검색이 모두 가능하다.
| PK: GSI1PK | SK: GSI1SK | PK(table) | SK(table) | type | title | artist | user |
|---|---|---|---|---|---|---|---|
| Scooter | How Much Is The Fish? | s#song1 | A | details | How Much Is The Fish? | Scooter | |
| Scooter | Ramp! (The Logical Song) | s#song2 | A | details | Ramp! (The Logical Song) | Scooter | |
| d#download-1 | s#song1 | s#song1 | d#download-1 | download | How Much Is The Fish? | Scooter | amdhing |
| d#download-2 | s#song1 | s#song1 | d#download-2 | download | How Much Is The Fish? | Scooter | ripani |
| d#download-3 | s#song2 | s#song2 | d#download-1 | download | Ramp! (The Logical Song) | Scooter | amdhing |
GSI1PK에 아티스트나 download ID가 들어가, 같은 인덱스에서 아티스트, download 검색이 가능하다.
특정 상황에서는 데이터를 너무 잘게 쪼개는 것이 오히려 비효율적일 수 있다.