DynamoDB GSI가 내부적으로 저장되는 방식
Global Secondary Index는 테이블로 되돌아가는 포인터가 아닙니다. 그것은 별개의, 내부적으로 관리되는 테이블입니다 — 자기만의 파티션, 자기만의 키 스키마, 자기만의 용량 — 으로, DynamoDB가 쓰기를 비동기로 복사해 넣어 동기화를 유지합니다.
SQL에서 왔다면 인덱스는 같은 물리 테이블에 볼트로 죈 B-트리이며, 같은 트랜잭션 안에서 업데이트됩니다. GSI는 그 두 가정을 모두 깨뜨리며, 거의 모든 GSI 의외성은 그 한 가지 사실로 거슬러 올라갑니다.
DynamoDB GSI는 어떻게 저장되나요?
DynamoDB GSI는 베이스 테이블로 향하는 포인터가 아니라, 별개의 내부 관리 테이블로 저장됩니다 — 자기만의 파티션, 키 스키마, 용량을 가집니다. DynamoDB는 각 쓰기를 인덱스에 비동기로 복사하며, GSI 키, 베이스 테이블 키, 그리고 프로젝션된 속성만 저장합니다.
- GSI는 자기만의 테이블입니다. 베이스 테이블이 아니라 GSI의 파티션 키로 키가 지정된, 완전히 독립적인 파티션 공간을 가집니다.
- 쓰기는 비동기로 복제됩니다. 여러분의 쓰기는 먼저 베이스 테이블에 커밋되고, 그런 다음 DynamoDB가 백그라운드 경로로 각 GSI에 부채꼴로 펼칩니다.
- 프로젝션된 속성만 저장됩니다. 인덱스는 GSI 키, 베이스 키, 그리고 여러분이 프로젝션한 속성을 담습니다 — 그 외에는 아무것도요.
- GSI 키가 고유할 필요는 없습니다. 여러 베이스 아이템이 하나의 GSI 파티션/정렬 키를 공유할 수 있습니다. 베이스 기본 키가 그것들을 구별되게 유지하는 동점 처리 장치입니다.
베이스 아이템 하나에서 시작하기
SaaS 감사 로그를 봅시다. 워크스페이스의 모든 권한 작업이 불변 이벤트가 됩니다. 베이스
테이블 WorkspaceEvents는 한 워크스페이스의 모든 이벤트가 하나의 아이템 컬렉션에 시간순으로
살도록 키가 지정됩니다:
| EventPK | EventSK | actorId | verb | targetRef |
|---|---|---|---|---|
| WS#orbit-9 | TS#2026-06-23T14:02:11Z | USR#kp | ROLE_GRANTED | USR#mara |
EventPK = "WS#orbit-9"는 워크스페이스로 파티셔닝하고, EventSK는 ISO 타임스탬프여서 Query가
한 워크스페이스의 이벤트를 시간순으로 반환합니다. 그것은 "이 워크스페이스의 타임라인을 보여 줘"를
완벽히 처리합니다.
그 외에는 아무것도 처리하지 못합니다. "USR#kp가 모든 워크스페이스에서 무엇을 했지?"라고 물을
수 없습니다 — actorId는 키가 아니므로, 베이스 테이블에서 그것에 답하는 유일한 방법은 전체
Scan입니다. 그것이 GSI가 추가하려고 존재하는 액세스 패턴입니다.
GSI를 추가하고 두 번째 테이블이 나타나는 것을 지켜보기
같은 이벤트를 수행한 사람으로 다시 파티셔닝하는 GSI ByActor를 정의합니다:
ByActor (GSI)
GSI1PK = actorId ("USR#kp")
GSI1SK = EventSK ("TS#2026-06-23T14:02:11Z")
이제 DynamoDB는 두 번째 물리 구조를 유지합니다. 같은 논리적 이벤트가 두 번 저장됩니다 —
한 번은 베이스 테이블의 WS#orbit-9 파티션에, 다시 한 번은 GSI의 USR#kp 파티션에:
| GSI1PK | GSI1SK | EventPK | EventSK | verb |
|---|---|---|---|---|
| USR#kp | TS#2026-06-23T14:02:11Z | WS#orbit-9 | TS#2026-06-23T14:02:11Z | ROLE_GRANTED |
무엇이 따라왔는지 주목하세요: 베이스 테이블의 키(EventPK, EventSK)가 모든 GSI 아이템에
자동으로 저장됩니다. 그게 GSI 적중이 전체 아이템으로 여러분을 되돌려 가리킬 수 있는 방법이며 —
KEYS_ONLY 인덱스조차 저장 비용이 드는 이유입니다.
GSI에 실제로 무엇이 사는가
인덱스는 전체 아이템을 복사하지 않습니다. 각 GSI 항목은 정확히 세 가지를 담으며, 여러분은 세 번째만 제어합니다:
| GSI에 저장됨 | 어디서 오는가 | 선택? |
|---|---|---|
| GSI 파티션 + 정렬 키 | GSI 키로 지정한 속성 | 아니오 |
| 베이스 테이블 키 | 모든 베이스 아이템에서 복사됨 | 아니오 |
| 프로젝션된 속성 | 여러분의 Projection 선택 | 예 |
Projection은 KEYS_ONLY, INCLUDE(명명된 목록), 또는 ALL입니다. GSI에 대한 Query는
인덱스에 있는 속성만 반환할 수 있습니다.
프로젝션되지 않은 것을 요청하면 DynamoDB는 그것을 투명하게 가져오지 않습니다 — 그 필드에 대해 아무것도 돌려받지 못합니다. (AWS GSI 문서)
그게 거꾸로 된 관계형 함정입니다: SQL이라면 빠진 컬럼을 위해 힙으로 다시 조인할 겁니다. GSI는 절대 그러지 않습니다. 프로젝션이 계약 전부입니다.
쓰기가 인덱스에 도달하는 방법
복제가 SQL 직관을 가장 세게 깨는 부분입니다. 베이스 쓰기와 그 인덱스 업데이트는 하나의 원자적 연산이 아닙니다.
PutItem할 때, DynamoDB는 베이스 테이블에 내구성 있게 커밋하고, 여러분의 쓰기를 확인 응답한
뒤, 그런 다음 각 GSI를 업데이트하는 백그라운드 경로로 변경을 전파합니다. 확인 응답은
인덱스를 기다리지 않습니다.
우리 감사 쓰기에 대한 이벤트 순서는 위에서 아래로 다음과 같습니다:
호출자는 4단계부터 6단계가 끝나기 전, 세 번째 단계에서 200 OK를 받습니다 — 그래서 그 틈에
ByActor에 대한 Query는 갓 쓰인 이벤트를 놓칠 수 있습니다.
그 비동기성은 결함이 아니라 설계입니다: 동기적 일관성보다 가용성을 택한 2007년 Amazon Dynamo 논문의 혈통입니다. 전체 결과는 GSI가 최종적으로 일관적인 이유에 살아 있습니다.
GSI 키는 고유 키가 아닙니다
SQL에서는 고유하지 않은 보조 인덱스가 기본이고 고유한 것은 옵트인하는 제약 조건입니다. GSI는 그 반대입니다: 그 어떤 고유성 보장도 없습니다, 영원히.
타임스탬프가 충돌하는 같은 행위자의 두 감사 이벤트는 같은 GSI1PK 와 GSI1SK를 공유합니다.
DynamoDB는 둘 다 저장합니다 — 항상 따라다니는 베이스 테이블의 기본 키로 내부적으로 그것들을
구별합니다.
그래서 한 행위자의 한 순간에 대한 GSI Query는 정당하게 여러 아이템을 반환할 수 있습니다.
SQL 고유 인덱스가 줄 법한 키당 한 행을 가정했다면, 그게 지뢰입니다.
인덱스를 쿼리할 때, DynamoDB 표현식 빌더는 이름과 값이
올바르게 이스케이프된 KeyConditionExpression을 작성합니다 — 예: 컷오프 이후 한 행위자를 매칭:
KeyConditionExpression: "#a = :actor AND #ts > :since"
ExpressionAttributeNames: { "#a": "actorId", "#ts": "EventSK" }
ExpressionAttributeValues: {
":actor": { "S": "USR#kp" },
":since": { "S": "TS#2026-06-01T00:00:00Z" }
}용량은 테이블이 아니라 인덱스에 산다
GSI는 자기만의 테이블이므로, 베이스 테이블과 별도로 과금되고 스로틀되는 자기만의 읽기 및
쓰기 용량을 가집니다. ByActor에서의 읽기는 테이블의 것이 아니라 GSI의 읽기 단위를 소비합니다.
무는 것은 역방향 결합입니다: 모든 베이스 테이블 쓰기는 인덱스에도 쓰며, GSI가 그것을 흡수하지 못하면 베이스 쓰기에 역압을 가합니다. 그 메커니즘은 자기만의 가이드가 있습니다 — GSI가 베이스 테이블 쓰기를 스로틀할 때.
이것은 또한 GSI의 파티션 키가 베이스 테이블의 것만큼 중요한 이유입니다. 저카디널리티 GSI 키는 베이스 쓰기가 완벽하게 퍼져 있을 때조차 쓰기를 하나의 인덱스 파티션에 뭉치게 합니다 — 다시 키를 잡아 여러분이 만들어 낸 핫 파티션입니다.
함정과 다음 단계
- 프로젝션되지 않은 속성이 돌아올 거라 기대하지 마세요. GSI
Query는 인덱스가 저장하는 것만 반환합니다. 전체 아이템이 필요하면, 그것을 프로젝션하거나 따라다니는 키로 베이스 테이블에서 가져오세요. - GSI 키를 고유한 것으로 다루지 마세요.
Query가 키당 둘 이상의 아이템을 반환하도록 계획하세요. 베이스 기본 키가 유일한 진짜 정체성입니다. - 그것을 먹인 쓰기 직후에 GSI를 읽지 마세요. 비동기 경로는 인덱스가 여러분의 쓰기를 아직 보여 주지 않을 수 있다는 뜻입니다 — 자기 쓰기 읽기가 필요할 때는 베이스 테이블을 읽으세요.
- GSI의 용량을 의도적으로 사이징하세요. 읽기에서는 독립적이고 쓰기에서는 숨은 의존성입니다.
전체 게임은 여러분의 패턴에 맞는 키 형태를 고르는 것입니다 — 단일 테이블 설계는 그것들 중 여럿에 걸쳐 하나의 GSI를 오버로드하고, GSI vs LSI는 대신 로컬 인덱스가 맞을 때를 다룹니다.
GSI KeyConditionExpression을 DynamoDB 표현식 빌더에서
만들고 미리 보세요. 그런 다음 DynoTable을 사용해 보세요 — 인덱스의 프로젝션된
속성을 점검하고 여러분 자신의 테이블에서 쓰기가 GSI로 복제되는 것을 지켜보세요.