DynamoDB의 Query 대 Scan
Query는 파티션 키로 단일 항목 컬렉션을 읽습니다(선택적으로 정렬 키로 좁힘). Scan은 테이블 전체를 읽고 그 뒤에 필터링합니다. API에서는 비슷해 보이지만 과금 — 그리고 확장 — 방식은 완전히 다릅니다.
DynamoDB에서 Query와 Scan 중 언제 무엇을 써야 할까요?
필요한 파티션의 이름을 댈 수 있을 때는 언제나 Query를 쓰세요 — 하나의 항목 컬렉션을 읽고 일치한 항목에 대해서만 과금됩니다. Scan은 일회성 내보내기나 소규모 테이블에만 쓰세요. Scan은 모든 항목을 읽고 FilterExpression이 실행되기 전에 테이블 전체 비용을 청구합니다. 실제 데이터에서는 Query가 항상 유리합니다.
- Query는 타깃형입니다: 일치한 파티션의 항목에 대해서만 비용을 냅니다.
- Scan은 망라형입니다: 모든 항목을 읽는 비용을 낸 뒤, 읽기가 계측된 다음에 실행되는
FilterExpression으로 대부분을 버립니다.
실제 규모의 테이블에서, 필터가 달린 Scan은 "왜 내 청구서는 거대하고 지연 시간은 RDS보다 나쁜가"라는 전형적인 함정입니다.
나란히 비교
| Query | Scan | |
|---|---|---|
| 읽기 | 한 파티션(PK 기준) | 테이블의 모든 항목 |
| 과금 용량 | 파티션에서 일치한 항목 | 필터링 전 테이블 전체 |
FilterExpression | 읽기 후 적용 — 그래도 읽기에 과금 | 동일 — 필터링은 비용을 줄이지 않음 |
| 지연 시간 | 테이블이 커져도 일정 | 테이블 크기에 따라 증가 |
| 페이지네이션 | 1 MB/페이지 → LastEvaluatedKey | 1 MB/페이지, 병렬화 가능 |
| 사용 용도 | 알려진 액세스 패턴 | 일회성 내보내기, 작은 설정 테이블 |
핵심 함정: FilterExpression은 두 작업 모두에서 DynamoDB가 읽기를 계측한 다음에 실행됩니다. "10행을 반환하는" Scan이 100만 행을 읽는 비용을 청구할 수 있습니다 — 필터링은 편의일 뿐, 결코 비용 제어가 아닙니다.
Query를 사용하세요
Query PK = "USER#42" AND SK begins_with "ORDER#"
흔한 액세스 패턴에 답하려고 Scan에 손이 간다면, 그것은 모델링 신호입니다: 글로벌 보조 인덱스를 추가하여 그 패턴이 Query가 되게 하세요.
결국 한 가지 질문으로 귀결됩니다 — 필요한 파티션의 이름을 댈 수 있나요?
키를 알면 Query를 쓰고, 모르면 GSI를 추가해 Query로 만들며, 맞는 키가 없을 때만 Scan으로 돌아가세요.
Scan이 괜찮은 경우
일회성 내보내기, 작은 설정 테이블, 그리고 의도적으로 테이블 전체를 페이지 넘기는 백그라운드 작업입니다. 정말로 모든 것을 읽어야 할 때는 Segment/TotalSegments로 Scan을 여러 워커에 분할하세요(병렬 스캔).
DynamoDB에 대한 반사적인 SELECT * FROM table은 PartiQL의 옷을 입은 동일한 안티패턴입니다 — Scan으로 컴파일됩니다. 항목 간 분석(GROUP BY, JOIN, 집계)이 정말로 필요할 때, DynoTable의 SQL 워크벤치는 테이블을 두드리는 대신 제한된 결과 집합 위에서 그것들을 클라이언트 측으로 실행합니다.
어느 패턴이든 들 읽기 단위는 용량 계산기로 추정하고, DynoTable을 사용해 이 쿼리들을 여러분 자신의 테이블에 대해 실행하고 확인하세요.