중급5분 분량

DynamoDB의 비정규화

SQL에서 넘어왔다면 비정규화는 죄악처럼 들립니다 — 중복된 데이터, 단일 진실 공급원의 부재. DynamoDB에서는 그것이 핵심입니다. 조인이 없으므로, 그것을 필요로 하는 아이템에 관련 데이터를 복사해 한 번에 읽어 들입니다.

DynamoDB에서 비정규화란?

DynamoDB에서 비정규화란 관련 데이터를 그것을 읽는 아이템에 복사해 두어, 단일 쿼리가 한 번에 모든 것을 반환하게 하는 것을 뜻합니다. DynamoDB에는 조인이 없으므로, 읽기 시점에 테이블을 엮는 대신 쓰기 시점에 미리 조인합니다. 그 대가는 데이터 부패입니다 — 거의 변하지 않는 값만 중복하세요.

  • 조인이 없으니 쓰기 시점에 미리 조인합니다. 관련 값을 그것을 읽는 아이템에 저장해, 쿼리가 두 번째 조회를 절대 필요로 하지 않게 합니다.
  • 두 가지 방식. 한 아이템의 복합 속성에 중첩 데이터를 임베드하거나, 여러 아이템에 값을 중복 저장합니다.
  • 지뢰는 데이터 부패입니다. 원본이 바뀌면, 펼쳐서 업데이트하기 전까지 모든 복사본이 틀립니다. 거의 변하지 않는 값만 중복하세요.
  • 그것은 쓰기가 아니라 읽기를 사는 것입니다. 더 많은(그리고 더 신중한) 쓰기를 저렴한 단일 요청 읽기와 맞바꿉니다.

의지할 조인이 없는 이유

관계형 JOIN은 읽기 시점에 정규화된 행을 재조립합니다. DynamoDB에는 조인이 없습니다 — Query는 하나의 아이템 컬렉션을 읽고 거기에 저장된 것을 정확히 돌려줍니다. 두 테이블을 대신 엮어주는 것은 아무것도 없습니다.

그래서 데이터는 이미 읽기에 맞게 형성되어 있어야 합니다. 화면이 게시물과 그 작성자의 이름을 필요로 한다면, 그 이름은 게시물 읽기가 이미 건드리는 어딘가에 존재해야 합니다. 2007년 Amazon Dynamo 논문은 이 거래를 명시적으로 했습니다. 대규모에서 예측 가능한 한 자릿수 밀리초 읽기를 얻기 위해 관계형 기능을 버리는 것이죠.

패턴 1 — 복합 속성으로 임베드하기

DynamoDB 속성은 스칼라뿐 아니라 중첩된 리스트를 담을 수 있습니다. 그래서 비정규화의 흔한 한 형태는 자식 객체에 자체 아이템을 주는 대신 부모 아이템 안에 직접 밀어 넣는 것입니다.

게시물과 그 태그, 그리고 작은 작성자 스냅샷을 모두 한 아이템에:

PKSKauthortags
POST#9f3META{id: U#12, name: "Mara Vance"}["dynamodb","aws"]

하나의 GetItem이 게시물, 태그, 작성자 블록을 함께 반환합니다. 두 번째 읽기가 없습니다. 이것은 부모에게 소유되고 크기가 한정된 데이터 — 몇 개의 태그, 하나의 작성자 스냅샷 — 에 훌륭합니다.

지켜야 할 한계: 단일 DynamoDB 아이템은 속성 이름과 값을 포함해 400 KB에서 최대치에 도달합니다 (서비스 할당량). 한정되지 않은 리스트(바이럴 게시물의 모든 댓글)를 임베드하면 그것을 넘어서게 됩니다.

패턴 2 — 값을 여러 아이템에 중복하기

블로그 사례가 교과서적입니다. 게시물을 나열하면서 각 행에 작성자의 표시 이름을 보여주고 싶지만 — 그것을 가져오려고 게시물마다 두 번째 읽기를 하고 싶지는 않습니다.

그래서 게시물이 생성될 때 작성자 이름을 각 게시물 아이템에 씁니다:

PKSKauthorIdauthorNametitle
POST#9f3METAU#12"Mara Vance""Modeling 1:N"
POST#a71METAU#12"Mara Vance""Sparse GSIs"
POST#b04METAU#88"Lio Tan""Query vs Scan"

이제 Query PK begins_with "POST#"(또는 게시물에 대한 GSI)가 제목과 작성자가 담긴 목록 전체를 행별 조회 없이 렌더링합니다. 작성자 이름은 비정규화되었습니다 — 정본 복사본은 USER#12에 살고, 모든 게시물이 자기 복사본을 가집니다.

거래가 바로 거기에 있습니다. N+1 읽기를 하나의 읽기로 바꾸되, "Mara Vance"를 N+1 곳에 보관하는 비용을 치른 것입니다.

임베드 vs. 중복 — 어느 쪽인가

임베드 (복합 속성)중복 (아이템 전반에 복사)
형태부모 안에 자식이 중첩됨여러 아이템에 같은 값
적합한정되고 부모가 소유한 데이터여러 아이템이 표시하는 공유 값
읽기GetItem 하나Query 하나
업데이트 비용부모 아이템 하나만 다시 씀모든 복사본으로 펼쳐서 씀
크기 위험400 KB 아이템 상한아이템당 없음

자식이 오직 부모와 함께만 등장한다면 임베드에 손을 뻗으세요. 여러 독립적 아이템이 같은 공유 값을 보여줘야 한다면 중복에 손을 뻗으세요.

지뢰: 낡은 복사본

여기가 당신을 무는 부분입니다. Mara가 자신을 "Mara V."로 개명합니다. USER#12를 업데이트합니다. 당신이 가서 고치기 전까지 모든 게시물 아이템은 여전히 "Mara Vance"라고 말합니다.

그래서 중복된 값을 업데이트하는 것은 한 줄짜리가 아니라 펼쳐 쓰기입니다. 영향받는 모든 아이템을 조회한 다음 각각을 다시 씁니다 — 이상적으로는 여전히 옛 값을 가진 행만 건드리도록 가드를 걸어서:

UPDATE POST#9f3
SET authorName = "Mara V."
WHERE authorName = "Mara Vance"

그 조건부 SETauthorName에 대해 Expression Builder에서 구성하고, 생성된 UpdateExpressionConditionExpression을 코드에 바로 복사할 수 있습니다.

펼쳐 쓰기 자체는 아이템당 하나의 쓰기입니다. 작성자의 게시물을 조회한 다음 업데이트를 발행합니다. 순서는:

"DynamoDB"App"DynamoDB"App"USER"작성자의 게시물 조회""POST"각 authorName 업데이트"

데이터 중복의 비용: 원본의 모든 변경은 하나의 쿼리에 더해 복사본당 하나의 쓰기입니다.

이것이 규칙이 거의 변하지 않는 값만 중복하라인 이유입니다. 표시 이름, 요금제 등급, 카테고리 라벨 — 괜찮습니다. 실시간 카운터나 자주 편집되는 필드 — 안 됩니다. 펼쳐 쓰기가 당신을 산 채로 잡아먹을 것입니다.

정규화가 여전히 이기는 때

값이 자주 변하거나, 한 아이템이 정말로 예측 불가능한 패턴으로 읽힌다면, 정규화를 유지하고 추가 읽기를 받아들이세요. 비정규화는 알려진 읽기 중심 액세스 패턴에 대한 최적화이지, 어디에나 적용하는 기본값이 아닙니다. 실제로 실행하는 읽기를 미리 조인하고, 나머지는 그대로 두세요.

이 중복된 속성이 어디에 살지 결정하려면 액세스 패턴부터 모델링하세요 — 단일 테이블 설계와, 거래의 읽기 측면에 대해서는 Query vs Scan을 참고하세요.

DynoTable을 다운로드하여 비정규화된 테이블을 들여다보고, 어떤 복사본이 어긋났는지 찾아내고, 자신의 데이터에 펼쳐 쓰기를 실행해 보세요.

업데이트됨