DynamoDB 희소 인덱스
희소 인덱스(sparse index) 는 자신의 키 속성을 가진 아이템만 담는 보조 인덱스입니다 — 그래서 거대한 테이블의 작고 뜨거운 부분집합이 미리 걸러진, 바로 쿼리할 수 있는 자체 컬렉션이 됩니다.
수백만 개의 행이 있지만 하루 종일 실행하는 쿼리는 아주 얇은 조각만 건드립니다. 열린 지원 티켓, 미납 인보이스, 검토 플래그가 붙은 계정 같은 것들이죠.
그 조각을 필터링하는 건 여전히 테이블 전체를 스캔하고 모든 읽기에 과금합니다. 희소 인덱스는 인덱스 자체를 작게 만듭니다.
DynamoDB에서 희소 인덱스란 무엇인가요?
희소 인덱스는 자신의 키 속성을 가진 아이템만 담는 보조 인덱스입니다. DynamoDB는 해당 키가 없는 아이템을 건너뛰므로, 원하는 아이템만 기록하는 키를 직접 만들면 — 열린 티켓, 미납 인보이스 등 — 인덱스가 정확히 그 부분집합이 됩니다. 쿼리는 그것만 읽으므로 필터도, 낭비된 읽기 용량도 없습니다.
- 보조 인덱스는 자신의 키를 가진 아이템만 인덱싱합니다. 아이템에서 키를 빼면 그 아이템은 결코 인덱스에 들어가지 않습니다 — 플레이스홀더도, null 행도 없습니다.
- 그래서 원하는 아이템만 갖는 키를 발명합니다. 쿼리할 아이템에 그것을 쓰고, 나머지에는 제거하세요. 인덱스가 정확히 그 부분집합이 됩니다.
- 쿼리는 그 부분집합만 읽습니다, 필터 없이. 그 크기는 테이블 총량이 아니라 작고 뜨거운 집합을 따릅니다.
REMOVE가 지렛대입니다, 비우기가 아니라. 빈 문자열도 여전히 값이고 여전히 인덱싱됩니다 — 속성을 삭제해야 합니다.
문제: 필터링은 읽기를 줄여 주지 않습니다
SQL에서 왔다면 WHERE 절이 작업을 줄인다고 가정합니다. DynamoDB의
FilterExpression은 그렇지 않습니다. 아이템을 읽은 뒤 에 실행되지, 전에가
아닙니다.
AWS 개발자 안내서 에 따르면, 필터링은 "소비되는 읽기 용량을 줄이지 않습니다" — 검사된 모든 아이템에 값을 치르고, 그런 다음 일치하지 않는 것들을 버립니다.
그래서 500만 티켓 중 50개가 열려 있다면, 필터링된 Query/Scan은 그 50개를
건네주려고 수백만을 읽고 지나갑니다.
그것이 모든 "내 스캔은 왜 이렇게 비싸지" 스레드 뒤에 있는 함정입니다. query vs. scan에 비용에 대한 전체 그림이 있습니다.
희소 인덱스는 인덱스 자체를 작게 만들어 그것을 비껴갑니다.
희소성은 어떻게 작동하나
보조 인덱스는 실제로 인덱스의 키 속성을 가진 아이템만 인덱싱합니다.
글로벌 보조 인덱스에 대한 AWS 문서 가 단순명료하게 말합니다. "글로벌 보조 인덱스는 그 인덱스에 정의된 키 속성을 가진 아이템만 포함합니다."
어떤 아이템에서 GSI의 파티션 키(또는 정렬 키)를 빠뜨리면, DynamoDB는 그것을 인덱스에 쓰지 않습니다. 플레이스홀더도, null 행도 없습니다 — 그 아이템은 부재합니다.
그 "기본값이 부재"라는 것이 비결의 전부입니다. 모든 아이템이 가진 status 속성을
인덱싱하지 마세요. 쿼리하고 싶은 아이템만 아예 갖는 속성을 발명하세요.
그러면 인덱스가 정확히 그 아이템들의 깔끔한 목록이 되고, 그것에 대한 Query는 그들만
읽습니다 — 필터도, 낭비된 용량도 없습니다.
베이스 테이블이 인덱스를 먹이고, 키를 가진 아이템만 건너가는 그림을 그려 보세요:
키를 가진(열린) 아이템만 인덱스에 복제되고, 닫힌 아이템은 결코 들어가지 않습니다.
이것은 싱글 테이블 디자인과 같은 키 빚기 사고방식입니다. 키는 데이터의 충실한 거울이 아니라, 특정 액세스 패턴을 위해 만드는 도구입니다.
실전 예제: "열린 티켓만"
지원 티켓 테이블을 봅시다. 베이스 테이블은 id로 티켓을 가져오고 고객의 티켓을 나열하도록 키잉되어 있습니다:
| PK | SK | attributes |
|---|---|---|
| TICKET#a91f | DETAIL | subject, body, priority, openState |
| CUSTOMER#88 | TICKET#a91f | subject, priority, openState |
테이블의 수명 동안 대부분의 티켓은 결국 닫힙니다. 하지만 상담원이 하루 종일 때리는 대시보드 쿼리는 "모든 열린 티켓을 가장 오래된 순으로 보여줘"입니다 — 수백만 속에 숨은 수백 행이죠.
희소 인덱스 수: 파티션 키 openBucket과 정렬 키 openedAt로 GSI를 정의하고,
열린 티켓에만 openBucket을 씁니다. 티켓이 생성될 때 설정하고, 티켓이
해결되면 REMOVE합니다.
| PK | SK | openBucket | openedAt | |
|---|---|---|---|---|
| TICKET#a91f | DETAIL | OPEN | 2026-06-23T09:14:00Z | ← 열림: 인덱스에 있음 |
| TICKET#b02c | DETAIL | OPEN | 2026-06-22T16:40:00Z | ← 열림: 인덱스에 있음 |
| TICKET#77de | DETAIL | (없음) | 2026-05-30T11:02:00Z | ← 닫힘: 인덱스에 없음 |
티켓 a91f와 b02c는 openBucket을 가지므로 GSI에 삽니다. 티켓 77de는
해결되어 openBucket이 제거되었으므로 조용히 빠져나갔습니다. 이제 대시보드는 하나의
저렴한 쿼리입니다:
Query IndexName = "open-tickets-index"
KeyConditionExpression: openBucket = "OPEN"
ScanIndexForward: true # 가장 오래된 순
이것은 열린 티켓만 읽습니다. 티켓이 닫히면 인덱스는 스스로 줄어듭니다 — 그 크기는 총량이 아니라 열린 집단을 따릅니다.
하나의 정적 파티션 값("OPEN")이 여기서 괜찮은 건 바로 그 집합이 작게 유지되기
때문입니다. 거대한 열린 집합이라면 샤딩된 파티션 키가 필요하겠지만, "작은 부분집합"
인덱스는 정확히 하나의 값이 옳은 선택인 곳입니다.
이것을 작동하게 하는 전이는 단일 업데이트 표현식입니다 — 티켓이 해결될 때 속성을 제거하는 거죠.
ExpressionAttributeNames와 :val 플레이스홀더를 직접 손으로 조립하는 대신,
DynamoDB 표현식 빌더에서 그 REMOVE 절과 읽기
쪽의 타입이 지정된 키 조건을 프로토타이핑하세요.
DynoTable에서 해보기
희소 인덱스의 어려운 부분은 읽기가 아닙니다 — 어떤 아이템이 인덱스에 들어갔고 어떤 아이템이 조용히 빠졌는지 보는 것입니다.
DynoTable은 테이블 뷰를 보조 인덱스로 전환해 채워진 부분집합을 정확히 보게 해 줍니다.
그래서 해결된 티켓이 묵은 키를 단 채 남아 있지 않고 정말로 open-tickets-index를
떠났는지 확인할 수 있습니다.

함정과 다음 단계
주의할 점 몇 가지:
- 키를 비우지 말고 제거하세요. 빈 문자열도 여전히 값이고, DynamoDB는
openBucket이""인 아이템을 인덱싱합니다. 인덱스에서 아이템을 빼려면 속성을REMOVE해야 합니다 — falsy 값으로 설정하면 안에 남습니다. - 인덱스는 최종적 일관성입니다. GSI는 비동기로 갱신되므로, 방금 해결된 티켓이 잠시 여전히 보일 수 있습니다 — GSI 읽기는 최종적 일관성만 지원 합니다. "이 티켓이 지금 열려 있나"에는 믿지 마세요.
- 프로젝션된 속성을 유념하세요. 인덱스에 대한
Query는 그 안에 프로젝션된 속성만 돌려줍니다. 대시보드가 제목과 우선순위를 필요로 하면 그것을 프로젝션하세요 — 아니면 전체 베이스 아이템에 추가GetItem비용을 치르세요. - 이건 GSI의 강점이지 LSI의 강점이 아닙니다. 로컬 보조 인덱스는 베이스 테이블의 파티션 키를 공유하므로 이런 식으로 아이템을 선택적으로 뺄 수 없습니다. GSI vs. LSI가 그 절충을 분석합니다.
희소 인덱스는 이 모델에서 가장 오래된 아이디어 중 하나입니다. 원조 2007년 Amazon Dynamo 논문 은 알려진 고볼륨 액세스 패턴을 저렴하게 처리하도록 저장소를 지었습니다.
희소 인덱스가 바로 그것입니다. 흔한 쿼리가 필요 없는 것을 전혀 읽지 않도록 키를 빚는 거죠.
실제로 하나를 만들고 살펴보려면 DynoTable을 다운로드해 테이블을 가리키고, 데이터 뷰를 희소 GSI로 넘기세요 — 아이템이 인덱스 키를 얻고 잃을 때 부분집합이 갱신되는 걸 지켜보세요.


