DynamoDB 필터링 전략
DynamoDB에서 "필터링"은 같은 단어를 쓰는 네 가지 서로 다른 것을 의미합니다. 셋은 읽고
과금되기 전에 데이터를 좁히고, 하나 — Filter라는 이름을 가진 그것 — 은 후에
좁힙니다. 어느 것이 어느 것인지 아는 것이 이 기술의 대부분입니다.
DynamoDB에서 필터링은 어떻게 동작하나요?
DynamoDB에는 필터링하는 네 가지 방법이 있고, 그중 단 하나만 과금된 후에 실행됩니다. 파티션 키는 파티션을 고르고, 정렬 키는 슬라이스를 좁히며, 희소 인덱스는 속성의 존재로 필터링합니다 — 이 셋 모두 계량 전에 읽기 비용을 줄입니다. FilterExpression은 읽기 후에 실행되므로 응답은 줄이지만 청구서는 절대 줄이지 않습니다.
- 파티션 키가 가장 저렴한 필터입니다: 파티션을 고르므로 테이블의 나머지는 절대 건드리지 않습니다.
- 정렬 키는 파티션 안에서
begins_with,between,<,>로 필터링합니다 — 여전히 과금 전이고, 여전히 저렴합니다. - 희소 인덱스는 부재로 필터링합니다: 아이템은 인덱싱된 속성을 가질 때만 인덱스에 나타나므로, 인덱스가 곧 필터링된 집합입니다.
- **
FilterExpression**이 함정입니다: DynamoDB가 읽기를 계량한 후 실행되므로 응답 크기는 줄이지만 비용은 절대 줄이지 않습니다.
예제 설정
제품 카탈로그입니다. 하나의 테이블, 파티션 키 PK, 정렬 키 SK:
PK = "DEPT#kitchen" SK = "PROD#00194"
모든 제품은 price, inStock(불리언), clearanceAt(unix 타임스탬프, 클리어런스로
표시된 아이템에만 존재)도 함께 가집니다. 한 부서의 아이템은 파티션을 공유하며 제품 id로
정렬됩니다.
네 가지 액세스 패턴을 원합니다. 각각이 다른 필터링 전략에 매핑되며 — 어느 하나에서든
잘못된 선택은 영원히 비용을 낼 Scan입니다.
파티션 키로 필터링하기
"kitchen의 모든 제품을 줘." 파티션 키가 이를 직접 처리합니다:
Query PK = "DEPT#kitchen"
DynamoDB는 정확히 하나의 파티션을 읽습니다. 테이블의 다른 어떤 것도 건드려지거나
과금되지 않습니다. 이것이 중요한 의미에서 무료인 유일한 필터입니다 —
Query와 Scan의 차이입니다.
SQL에서 넘어왔다면 이것은 거꾸로처럼 느껴집니다: 인덱스를 스캔하는
WHERE department = 'kitchen'이 없고, 그냥 파티션을 이름으로 부릅니다. 이름을 부를 수
없다면, 그것은 쿼리 문제가 아니라 모델링 문제입니다.
정렬 키로 필터링하기
"PROD#00100부터의 kitchen 제품을 줘." 정렬 키는 파티션 안에서 좁히며, 읽기가 계량되기
전에 그렇게 합니다:
Query PK = "DEPT#kitchen" AND SK between "PROD#00100" AND "PROD#00200"
정렬 키 조건은 의도적으로 제한됩니다: =, <, <=, >, >=, between,
begins_with. OR도, 임의의 술어도 없습니다.
그 제약이 읽기를 겨냥된 상태로 유지하는 것입니다 — DynamoDB는 파티션 전체가 아니라 연속된 슬라이스를 훑습니다.
여기서의 지렛대는 정렬 키를 어떻게 인코딩하느냐입니다. 패턴이 "가격대별"이라면
PROD#<id> 정렬 키는 도움이 안 됩니다 — 가격을 키에 구워 넣어야 합니다.
그것은 쿼리 시점이 아니라 설계 시점에 내리는 정렬 키 전략 결정입니다.
희소 인덱스로 필터링하기
"지금 클리어런스 중인 모든 것을 줘." 대부분의 제품은 그렇지 않으므로, 몇 안 되는 것을 찾으려고 카탈로그를 읽고 싶지 않습니다.
희소 인덱스는 이를 부재로 해결합니다. 글로벌 보조 인덱스는 아이템이 인덱스의 키 속성을 둘 다 가질 때만 그 아이템을 포함합니다.
GSI 파티션 키를 clearanceAt으로 — 클리어런스 아이템에만 존재 — 설정하면, 인덱스는 그
외에 아무것도 담지 않습니다.
AWS는 이를 명시합니다: GSI는 "인덱싱된 속성을 가진 아이템만 포함"하므로, 키 속성이 없는 아이템은 그냥 전파되지 않습니다 (AWS — 희소 인덱스 활용하기).
이제 쿼리는 클리어런스 아이템만 읽고, 그것에 대해서만 과금됩니다:
Query ON ClearanceIndex GSI_PK = "CLEARANCE" (sorted by clearanceAt)
필터는 당신이 데이터를 쓸 때 일어났습니다 — clearanceAt을 설정할지 말지 선택하면서.
인덱스가 곧 필터링된 집합입니다. 어느 인덱스 유형이 맞는지는
GSI vs LSI를 참고하세요.
FilterExpression으로 필터링하기
"재고가 있는 kitchen 제품을 줘." inStock은 키 속성이 아니므로 FilterExpression에
손을 뻗습니다:
Query PK = "DEPT#kitchen"
Filter inStock = true
여기 함정이 있습니다. DynamoDB는 kitchen 파티션의 모든 아이템을 읽고, 그 모두에 대해
용량을 계량하고, 그 다음 재고 없는 것들을 떨굽니다.
공식 규칙: 필터 표현식은 "Query가 끝난 후, 결과가 반환되기 전에 적용"되며 "어떤 추가
읽기 용량 단위도 소비하지 않습니다" — 전체 읽기에 대해 이미 비용을 냈으니까요
(AWS — Query의 필터 표현식).
그래서 kitchen에 10,000개 제품이 있고 12개가 재고에 있다면, 10,000개를 읽는 비용을
냅니다. 응답은 작지만, 청구서는 작지 않습니다. FilterExpression은 회선을 건너는
페이로드를 줄일 뿐, 읽기는 절대 줄이지 않습니다.
두 번째, 더 날카로운 모서리가 있습니다: 페이지네이션은 필터링 전에 계량됩니다. 한 페이지는 일치 항목 1 MB가 아니라 읽은 아이템 1 MB입니다.
필터는 LastEvaluatedKey가 설정된 빈 페이지를 반환할 수 있습니다 — DynamoDB가 1
메가바이트를 통째로 읽고, 아무것도 일치시키지 못하고, 빈 배열을 건넵니다. 계속 페이지를
넘기고, 모든 빈 페이지에 비용을 냈습니다.
표현식 — 이름, 값, 올바른 예약어 이스케이프 — 을
DynamoDB Expression Builder로 만들어
#inStock/:val 자리표시자가 첫 시도에 올바르게 되도록 하세요.
넷을 비교하기
| 언제 필터링하나 | 읽기 비용 줄임? | 술어 능력 | 설정 비용 | |
|---|---|---|---|---|
| 파티션 키 | 읽기 전 | 예 — 한 파티션 | 동등성만 | 무료(키 그 자체) |
| 정렬 키 | 읽기 전 | 예 — 슬라이스 | 범위 / begins_with | 정렬 키 설계 |
| 희소 인덱스 | 읽기 전 | 예 — 인덱스만 | 속성의 존재 | 추가 GSI + 쓰기 비용 |
| **FilterExpression | 읽기 후 | 아니오** | 거의 모든 조건 | 없음 |
표를 위에서 아래로 읽으세요: 술어 능력은 올라가고, 비용 통제는 내려갑니다.
FilterExpression은 이미 읽은 아이템에서 실행되기 때문에 무엇이든 정확히 표현할 수
있습니다 — 그것이 바로 비용을 절약할 수 없는 같은 이유입니다.
DynoTable에서 보기
필터를 걸어 Query를 실행할 때, 읽은 아이템과 반환된 아이템 사이의 간극이 핵심
전부입니다. DynoTable은 소비된 용량을 결과 수 옆에 드러냅니다 — 그래서 파티션 전체를
조용히 읽는 필터가 월별 청구서에 숨지 않고 보입니다.
필터가 답할 수 없는 진짜 아이템 간 질문 — "부서별 평균 가격", "리뷰와 조인된 재고 제품" —
에 대해, DynoTable의 SQL Workbench는 테이블 전체 Scan으로 컴파일되는 대신 한정된
결과 집합에 대해 GROUP BY, JOIN, 집계를 클라이언트 쪽에서 실행합니다.
함정과 다음 단계
FilterExpression을 주요 액세스 경로로 쓰지 마세요. 패턴이 흔하다면 키나 희소 인덱스로 모델링하세요. 필터는 마지막 자투리 좁히기를 위한 것이지, 그 대부분을 위한 것이 아닙니다.- 빈 페이지를 주의하세요. 필터를 건 쿼리는 아무것도 반환하지 않으면서 오래 페이지를
넘길 수 있습니다.
LastEvaluatedKey를 존중하세요. 빈 페이지가 "끝"을 의미한다고 가정하지 마세요. - 희소 인덱스도 공짜가 아닙니다. 거기에 떨어지는 모든 아이템에 대해 쓰기 용량과 스토리지가 듭니다 — 속성이 드물 때는 저렴하고, 그렇지 않을 때는 덜 저렴합니다.
필터를 건 읽기가 실제로 얼마나 들지를 용량 계산기로 추정하고, DynoTable을 사용해 자신의 테이블에서 소비된 용량을 반환된 행과 대비해 지켜보세요.