DynamoDB의 관계형 데이터 모델링에 대한 예
이 예는 Amazon DynamoDB에서 관계형 데이터를 모델링하는 방법을 설명합니다. DynamoDB 테이블 설계는 관계형 모델링에 표시된 관계형 주문 입력 스키마에 해당합니다. 이 설계는 단일 인접 목록 대신 여러 개의 특수 테이블을 사용하여 명확한 운영 경계를 제공하는 동시에 전략적 GSI를 활용하여 모든 액세스 패턴을 효율적으로 제공합니다.
설계 접근 방식은 집계 지향 원칙을 사용하여 엄격한 개체 경계가 아닌 액세스 패턴을 기반으로 데이터를 그룹화합니다. 주요 설계 결정에는 액세스 상관관계가 낮은 개체에 대해 별도의 테이블 사용, 항상 함께 액세스할 때 관련 데이터 포함, 관계를 식별하기 위한 항목 컬렉션 사용이 포함됩니다.
다음 테이블과 함께 제공되는 인덱스는 관계형 순서 항목 스키마를 지원합니다.
직원 테이블 설계
직원 테이블은 직원 정보를 항목당 단일 엔터티로 저장하고 직원 직접 조회에 최적화되어 있으며 전략적 GSI를 통해 여러 쿼리 패턴을 지원합니다. 이 표는 독립적인 운영 특성과 낮은 교차 엔터티 액세스 상관관계를 가진 엔터티에 대해 별도의 테이블을 설계하는 원칙을 보여줍니다.
테이블은 각 직원이 고유한 엔터티이므로 정렬 키 없이 단순 파티션 키(employee_id)를 사용합니다. 4개의 GSI를 사용하면 다양한 속성을 기준으로 효율적인 쿼리를 수행할 수 있습니다.
EmployeeByName GSI - 모든 직원 속성과 함께 INCLUDE 프로젝션을 사용하여 이름으로 전체 직원 세부 정보 검색을 지원하고 employee_id를 정렬 키로 사용하여 중복될 수 있는 이름을 처리합니다.
EmployeeByWarehouse GSI - 필수 속성(이름, job_title, hire_date)만 있는 INCLUDE 프로젝션을 사용하여 스토리지 비용을 최소화하는 동시에 웨어하우스 기반 쿼리를 지원합니다.
EmployeeByJobTitle GSI - 보고 및 조직 분석을 위해 INCLUDE 프로젝션을 사용하여 역할 기반 쿼리를 활성화합니다.
EmployeeByHireDate GSI - 최근 채용과 관련하여 효율적인 날짜 범위 쿼리를 활성화하기 위해 hire_date를 정렬 키로 사용하여 정적 파티션 키 값 "EMPLOYEE"를 사용합니다. 직원 추가/업데이트는 일반적으로 1,000WCU 미만이므로 단일 파티션이 핫 파티션 문제 없이 쓰기 로드를 처리할 수 있습니다.
| employee_id(PK) | 이름 | phone_number | warehouse_id | job_title | hire_date | entity_type |
|---|---|---|---|---|---|---|
| emp_001 | John Smith | ["+1-555-0101"] | wh_sea | Manager | 2024-03-15 | 피고용인 |
| emp_002 | Jane Doe | ["+1-555-0102", "+1-555-0103"] | wh_sea | 연결 | 2025-01-10 | 피고용인 |
| emp_003 | Bob Wilson | ["+1-555-0104"] | wh_pdx | 연결 | 2025-06-20 | 피고용인 |
| emp_004 | Alice Brown | ["+1-555-0105"] | wh_pdx | Supervisor | 2023-11-05 | 피고용인 |
| emp_005 | Charlie Davis | ["+1-555-0106"] | wh_sea | 연결 | 2025-12-01 | 피고용인 |
| 이름(GSI-PK) | employee_id(GSI-SK) | phone_number | warehouse_id | job_title | hire_date |
|---|---|---|---|---|---|
| Alice Brown | emp_004 | ["+1-555-0105"] | wh_pdx | Supervisor | 2023-11-05 |
| Bob Wilson | emp_003 | ["+1-555-0104"] | wh_pdx | 연결 | 2025-06-20 |
| Charlie Davis | emp_005 | ["+1-555-0106"] | wh_sea | 연결 | 2025-12-01 |
| Jane Doe | emp_002 | ["+1-555-0102", "+1-555-0103"] | wh_sea | 연결 | 2025-01-10 |
| John Smith | emp_001 | ["+1-555-0101"] | wh_sea | Manager | 2024-03-15 |
| warehouse_id(GSI-PK) | employee_id(GSI-SK) | 이름 | job_title | hire_date |
|---|---|---|---|---|
| wh_pdx | emp_003 | Bob Wilson | 연결 | 2025-06-20 |
| wh_pdx | emp_004 | Alice Brown | Supervisor | 2023-11-05 |
| wh_sea | emp_001 | John Smith | Manager | 2024-03-15 |
| wh_sea | emp_002 | Jane Doe | 연결 | 2025-01-10 |
| wh_sea | emp_005 | Charlie Davis | 연결 | 2025-12-01 |
| job_title(GSI-PK) | employee_id(GSI-SK) | 이름 | warehouse_id | hire_date |
|---|---|---|---|---|
| 연결 | emp_002 | Jane Doe | wh_sea | 2025-01-10 |
| 연결 | emp_003 | Bob Wilson | wh_pdx | 2025-06-20 |
| 연결 | emp_005 | Charlie Davis | wh_sea | 2025-12-01 |
| Manager | emp_001 | John Smith | wh_sea | 2024-03-15 |
| Supervisor | emp_004 | Alice Brown | wh_pdx | 2023-11-05 |
| entity_type(GSI-PK) | hire_date(GSI-SK) | employee_id | 이름 | warehouse_id |
|---|---|---|---|---|
| 피고용인 | 2023-11-05 | emp_004 | Alice Brown | wh_pdx |
| 피고용인 | 2024-03-15 | emp_001 | John Smith | wh_sea |
| 피고용인 | 2025-01-10 | emp_002 | Jane Doe | wh_sea |
| 피고용인 | 2025-06-20 | emp_003 | Bob Wilson | wh_pdx |
| 피고용인 | 2025-12-01 | emp_005 | Charlie Davis | wh_sea |
고객 테이블 설계
고객 테이블은 효율적인 계정 담당자 쿼리를 활성화하기 위해 account_rep_id의 전략적 비정규화로 고객 정보를 유지합니다. 이 설계 선택은 쿼리 성능을 위해 약간의 스토리지 오버헤드를 상쇄하므로 고객과 계정 담당자 데이터를 조인할 필요가 없습니다.
이 표는 DynamoDB의 스키마 유연성을 보여주는 목록 속성을 사용하여 고객당 여러 전화번호를 지원합니다. 단일 GSI는 계정 담당자 워크플로를 활성화합니다.
CustomerByAccountRep GSI - 전체 고객 레코드 검색 없이 계정 담당자 고객 관리를 지원하기 위해 이름 및 이메일 속성과 함께 INCLUDE 프로젝션 사용
| customer_id(PK) | 이름 | phone_number | 이메일 | account_rep_id |
|---|---|---|---|---|
| cust_001 | Acme Corp | ["+1-555-1001"] | contact@acme.com | rep_001 |
| cust_002 | TechStart Inc | ["+1-555-1002", "+1-555-1003"] | info@techstart.com | rep_001 |
| cust_003 | 글로벌 거래자 | ["+1-555-1004"] | sales@globaltraders.com | rep_002 |
| cust_004 | BuildRight LLC | ["+1-555-1005"] | orders@buildright.com | rep_002 |
| cust_005 | FastShip Co | ["+1-555-1006"] | support@fastship.com | rep_003 |
| account_rep_id(GSI-PK) | customer_id(GSI-SK) | 이름 | 이메일 |
|---|---|---|---|
| rep_001 | cust_001 | Acme Corp | contact@acme.com |
| rep_001 | cust_002 | TechStart Inc | info@techstart.com |
| rep_002 | cust_003 | 글로벌 거래자 | sales@globaltraders.com |
| rep_002 | cust_004 | BuildRight LLC | orders@buildright.com |
| rep_003 | cust_005 | FastShip Co | support@fastship.com |
주문 테이블 설계
주문 테이블은 주문 헤더 및 주문 항목에 대해 별도의 항목과 함께 수직 파티셔닝을 사용합니다. 이 설계를 사용하면 효율적인 액세스를 위해 모든 주문 구성 요소를 동일한 파티션 내에 유지하면서 효율적인 제품 기반 쿼리를 수행할 수 있습니다. 각 주문은 여러 항목으로 구성됩니다.
주문 헤더 - PK=order_id, SK=order_id의 주문 메타데이터 포함
주문 항목 - PK=order_id, SK=product_id가 있는 개별 품목, 직접 제품 쿼리 활성화
참고
이 수직 분할 접근 방식은 쿼리 유연성을 높이기 위해 임베디드 주문 항목의 단순성을 절충합니다. 각 주문 항목은 별도의 DynamoDB 항목이 되어, 단일 요청으로 효율적으로 검색할 수 있도록 모든 주문 데이터를 동일한 파티션 내에 유지하면서 효율적인 제품 기반 쿼리를 가능하게 합니다.
테이블에는 account_rep_id(고객 테이블에서 복제됨)의 전략적 비정규화가 포함되어 있어 고객 조회 없이 직접 계정 담당자 쿼리를 활성화할 수 있습니다. 처리량이 많은 쓰기 시나리오의 경우 OPEN 주문에는 여러 파티션에서 쓰기 샤딩을 활성화할 수 있는 상태 및 샤드 속성이 포함됩니다.
4개의 GSI가 최적화된 프로젝션으로 다양한 쿼리 패턴을 지원합니다.
OrderByCustomerDate GSI - 주문 요약 및 항목 세부 정보와 함께 INCLUDE 프로젝션을 사용하여 날짜 범위 필터링으로 고객 주문 기록을 지원합니다.
OpenOrdersByDate GSI(Sparse, Sharded) - 5개의 샤드가 있는 다중 속성 파티션 키(상태 + 샤드)를 사용하여 파티션 간에 5,000WPS(초당 쓰기 수)를 분산합니다(각 1,000WPS, DynamoDB의 파티션당 1,000WCU 제한과 일치). OPEN 주문(총 주문의 20%)만 인덱싱하므로 GSI 스토리지 비용을 줄이는 데 도움이 될 수 있습니다. 클라이언트 측 결과 병합을 사용하여 샤드 5개 모두에 병렬 쿼리 필요
OrderByAccountRep GSI - 주문 요약 속성과 함께 INCLUDE 프로젝션을 사용하여 전체 주문 세부 정보 없이 계정 담당자 워크플로 지원
ProductInOrders GSI - OrderItem 레코드(PK=order_id, SK=product_id)에서 생성된 이 GSI를 사용하면 쿼리가 특정 제품이 포함된 모든 주문을 찾을 수 있습니다. 제품 수요 분석을 위해 주문 컨텍스트(customer_id, order_date, 수량)와 함께 INCLUDE 프로젝션을 사용합니다.
| PK | SK | customer_id | order_date | status | account_rep_id | quantity | 가격 | 샤드 |
|---|---|---|---|---|---|---|---|---|
| ord_001 | ord_001 | cust_001 | 2025-11-15 | CLOSED | rep_001 | |||
| ord_001 | prod_100 | 5 | 25.00 | |||||
| ord_002 | ord_002 | cust_001 | 2025-12-20 | OPEN | rep_001 | 0 | ||
| ord_002 | prod_101 | 10 | 15.00 | |||||
| ord_003 | ord_003 | cust_002 | 2026-01-05 | OPEN | rep_001 | 2 | ||
| ord_003 | prod_100 | 3 | 25.00 |
| customer_id(GSI-PK) | order_date(GSI-SK) | order_id | status | total_amount | order_items | 샤드 |
|---|---|---|---|---|---|---|
| cust_001 | 2025-11-15 | ord_001 | CLOSED | 225.00 | [{product_id: "prod_100", qty: 5}] | |
| cust_001 | 2025-12-20 | ord_002 | OPEN | 150.00 | [{product_id: "prod_101", qty: 10}] | 0 |
| cust_002 | 2026-01-05 | ord_003 | OPEN | 175.00 | [{product_id: "prod_100", qty: 3}] | 2 |
| cust_003 | 2025-10-10 | ord_004 | CLOSED | 250.00 | [{product_id: "prod_101", qty: 5}] | |
| cust_004 | 2026-01-03 | ord_005 | OPEN | 200.00 | [{product_id: "prod_100", qty: 20}] | 1 |
| 상태(GSI-PK-1) | 샤드(GSI-PK-2) | order_date(SK) | order_id | customer_id | account_rep_id | order_items | total_amount |
|---|---|---|---|---|---|---|---|
| OPEN | 0 | 2025-12-20 | ord_002 | cust_001 | rep_001 | [{product_id: "prod_101", qty: 10}] | 150.00 |
| OPEN | 1 | 2026-01-03 | ord_005 | cust_004 | rep_002 | [{product_id: "prod_100", qty: 20}] | 200.00 |
| OPEN | 2 | 2026-01-05 | ord_003 | cust_002 | rep_001 | [{product_id: "prod_100", qty: 3}] | 175.00 |
| account_rep_id(GSI-PK) | order_date(GSI-SK) | order_id | customer_id | status | total_amount |
|---|---|---|---|---|---|
| rep_001 | 2025-11-15 | ord_001 | cust_001 | CLOSED | 225.00 |
| rep_001 | 2025-12-20 | ord_002 | cust_001 | OPEN | 150.00 |
| rep_001 | 2026-01-05 | ord_003 | cust_002 | OPEN | 175.00 |
| rep_002 | 2025-10-10 | ord_004 | cust_003 | CLOSED | 250.00 |
| rep_002 | 2026-01-03 | ord_005 | cust_004 | OPEN | 200.00 |
| product_id(GSI-PK) | order_id(GSI-SK) | customer_id | order_date | quantity |
|---|---|---|---|---|
| prod_100 | ord_001 | cust_001 | 2025-11-15 | 5 |
| prod_100 | ord_003 | cust_002 | 2026-01-05 | 3 |
| prod_101 | ord_002 | cust_001 | 2025-12-20 | 10 |
제품 테이블 설계
제품 테이블은 항목 수집 패턴을 사용하여 동일한 파티션 내에 제품 메타데이터와 인벤토리 데이터를 모두 저장합니다. 이 설계는 제품과 인벤토리 간의 식별 관계를 활용합니다. 상위 제품이 없으면 인벤토리가 존재할 수 없습니다. 제품 메타데이터에는 PK=product_id를 SK=product_id와 함께 사용하고 인벤토리 항목에는 SK=warehouse_id를 사용하면 별도의 인벤토리 테이블과 GSI가 필요하지 않으므로 비용이 약 50% 절감됩니다.
이 패턴을 사용하면 개별 웨어하우스 인벤토리(복합 키가 있는 GetItem)와 제품의 모든 웨어하우스 인벤토리(파티션 키에 대한 쿼리) 모두에 대해 효율적인 쿼리를 수행할 수 있습니다. 제품 메타데이터 항목의 total_inventory 속성은 빠른 총 인벤토리 조회를 위한 비정규화된 집계를 제공합니다.
| product_id(PK) | warehouse_id(SK) | product_name | category | unit_price | inventory_quantity | total_inventory |
|---|---|---|---|---|---|---|
| prod_100 | prod_100 | 위젯 A | 하드웨어 | 25.00 | 500 | |
| prod_100 | wh_sea | 200 | ||||
| prod_100 | wh_pdx | 150 | ||||
| prod_100 | wh_atl | 150 | ||||
| prod_101 | prod_101 | 도구 B | 전자 제품 | 50.00 | 300 | |
| prod_101 | wh_sea | 100 | ||||
| prod_101 | wh_pdx | 200 |
각 테이블은 특정 글로벌 보조 인덱스(GSI)로 설계되어 필요한 액세스 패턴을 효율적으로 지원합니다. 설계에서는 전략적 비정규화 및 희소 인덱싱과 함께 집계 지향 원칙을 사용하여 성능과 비용을 최적화합니다.
주요 설계 최적화에는 다음이 포함됩니다.
-
구문 분석 GSI - OpenOrdersByDate는 OPEN 주문(총 20%)만 인덱싱하므로 GSI 스토리지 비용을 줄이는 데 도움이 될 수 있습니다.
-
항목 수집 패턴 - 제품 테이블은 PK=product_id, SK=warehouse_id를 사용하여 인벤토리를 저장하여 별도의 인벤토리 테이블을 제거합니다.
-
Order + OrderItems 집계 - 100% 액세스 상관 관계로 인해 단일 항목으로 포함됩니다.
-
전략적 비정규화 - 효율적인 쿼리를 위해 주문 테이블에 account_rep_id가 중복됩니다.
마지막으로 앞서 정의한 액세스 패턴을 다시 살펴보겠습니다. 다음 표는 전략적 GSI와 함께 다중 테이블 설계를 사용하여 각 액세스 패턴을 효율적으로 지원하는 방법을 보여줍니다. 각 패턴은 직접 키 조회 또는 단일 GSI 쿼리를 사용하여 비용이 많이 드는 스캔을 방지하고 모든 규모에서 일관된 성능을 제공합니다.
| 일련 번호 | 액세스 패턴 | 쿼리 조건 |
|---|---|---|
|
1 |
직원 ID별로 직원 세부 정보 조회 |
직원 테이블: GetItem(employee_id="emp_001") |
|
2 |
직원 이름별 직원 세부 정보 쿼리 |
EmployeeByName GSI: Query(name="John Smith") |
|
3 |
직원의 전화번호 찾기 |
직원 테이블: GetItem(employee_id="emp_001") |
|
4 |
고객의 전화번호 찾기 |
고객 테이블: GetItem(customer_id="cust_001") |
|
5 |
날짜 범위 내에서 고객에 대한 주문 가져오기 |
OrderByCustomerDate GSI: Query(customer_id="cust_001", order_date BETWEEN "2025-01-01" AND "2025-12-31") |
|
6 |
날짜 범위 내의 모든 미결 주문 표시 |
OpenOrdersByDate GSI: 다중 속성 PK(status="OPEN" + shard=0-4), SK=order_date BETWEEN "2025-01-01" AND "2025-12-31"와 함께 5개의 샤드 쿼리, 결과 병합 |
|
7 |
최근 채용된 모든 직원 표시 |
EmployeeByHireDate GSI: Query(entity_type="EMPLOYEE", hire_date >= "2025-01-01") |
|
8 |
창고의 모든 직원 찾기 |
EmployeeByWarehouse GSI: Query(warehouse_id="wh_sea") |
|
9 |
제품 주문 시 모든 항목 가져오기 |
ProductInOrders GSI: Query(product_id="prod_100") |
|
10 |
모든 웨어하우스에서 제품에 대한 인벤토리 가져오기 |
제품 테이블: Query(product_id="prod_100") |
|
11 |
계정 담당자별로 고객 가져오기 |
CustomerByAccountRep GSI: Query(account_rep_id="rep_001") |
|
12 |
계정 담당자별로 주문 가져오기 |
OrderByAccountRep GSI: Query(account_rep_id="rep_001") |
|
13 |
직함으로 직원 가져오기 |
EmployeeByJobTitle GSI: Query(job_title="Manager") |
|
14 |
제품 및 창고별로 인벤토리 가져오기 |
제품 테이블: GetItem(product_id="prod_100", warehouse_id="wh_sea") |
|
15 |
전체 제품 인벤토리 가져오기 |
제품 테이블: GetItem(product_id="prod_100", warehouse_id="prod_100") |