입문5분 분량

DynamoDB Scan이 느리고 비싼 이유

Scan테이블의 모든 아이템을 읽고 나서야 필터링합니다. SQL 근육 기억으로 손이 가는 연산이자, 떠나온 RDS 박스보다 지연 시간을 나쁘게 만들면서 청구서를 조용히 불리는 연산입니다.

DynamoDB Scan이 느리고 비싼 이유는 무엇인가요?

ScanFilterExpression이 실행되기 전에 테이블의 모든 아이템을 읽으므로, 반환되는 행이 얼마나 적든 테이블 전체를 읽는 데 과금됩니다. 또한 테이블이 커질수록 더 느려집니다. 해결책은 거의 항상 키 기반 Query입니다 — DynamoDB가 모든 파티션이 아닌 하나의 파티션만 건드리도록 액세스 패턴을 키 중심으로 모델링하세요.

  • Scan은 매번 테이블 전체를 읽습니다. 무엇을 치르고 얼마나 걸리는지는 결과 개수가 아니라 크기가 결정합니다.
  • FilterExpression은 비용에 대한 거짓말입니다. 읽기가 계측된 에 실행되므로, 12개 아이템을 돌려주는 데 1200만 개 읽기로 과금할 수 있습니다.
  • Scan은 커질수록 느려집니다. 키 기반 Query는 평평하게 유지됩니다 — 테이블이 아무리 커져도 한 파티션만 건드립니다.
  • 해결은 거의 항상 튜닝이 아니라 모델링입니다. 일상적 질문에 답하려고 Scan한다면, 키를 놓치고 있는 것입니다.

Scan이 실제로 하는 일

SQL에서 왔다면 SELECT * FROM events WHERE type = 'checkout'이 공짜처럼 느껴집니다 — 엔진에 인덱스가 있거나 없거나, 어느 쪽이든 행을 받죠. DynamoDB에는 그걸 정해 주는 쿼리 플래너가 없습니다.

Scan은 테이블 전체를 1MB씩 순차로 훑고, 각 페이지를 여러분의 FilterExpression에 넘깁니다. 필터가 거부하는 무엇이든 여전히 읽히고, 여전히 계측되고, 여전히 청구서에 오릅니다. (AWS: 테이블 스캔)

그게 함정입니다. 필터는 WHERE 절처럼 보이지만, 결과 집합을 바꿀 뿐 비용은 결코 바꾸지 않습니다. Scan은 필터가 있든 없든 같은 읽기 용량을 소비합니다. (AWS: 테이블 스캔)

읽기 단위를 세어 보기

DynamoDB는 읽기를 읽기 용량 단위(RCU) 로 계측합니다. 1 RCU는 최대 4KB까지의 아이템 하나를 강력한 일관성으로 한 번 읽습니다. 최종적 일관성 읽기는 그 절반입니다. 더 큰 아이템은 다음 4KB로 올림됩니다. (AWS: 읽기/쓰기 용량 모드)

분석 테이블 ProductEvents를 봅시다. 각 행은 추적된 이벤트 하나입니다:

PK  = "TENANT#acme"
SK  = "TS#2026-06-23T14:08:55Z#evt_9f3a"
attrs: eventType, sessionId, userId, payloadBytes

그것이 2,000,000 개의 이벤트를, 각 ~1KB로, 모두 한 바쁜 테넌트 아래 담는다고 합시다. 오늘의 체크아웃을 원합니다. 반사적인 수:

Scan ProductEvents
FilterExpression: eventType = "checkout"

그 필터는 40행을 돌려줄 수 있습니다. 하지만 Scan은 2,000,000개 아이템을 모두 먼저 읽었습니다. 각 ~1KB로(4KB당 1 RCU, 최종적 일관성 ≈ 4KB당 0.5 RCU), 40개 아이템을 건네주려고 대략 250,000 RCU 를 계측하고 — ~500MB의 데이터를 페이지로 넘긴 것입니다.

이제 액세스 패턴을 키로 모델링하고 대신 Query하세요:

Query ProductEvents
PK = "TENANT#acme"
AND SK begins_with "TS#2026-06-23"

이것은 한 파티션의 일치하는 슬라이스만 읽습니다. 그 40개 체크아웃 행에 그날의 다른 이벤트를 더해 ~2MB라면, 500MB가 아니라 ~2MB의 읽기에 값을 치릅니다. 같은 답을 비용의 작은 한 줌으로 — 그리고 테이블이 커져도 지연 시간은 평평하게 유지됩니다.

Scan 대 Query, 계측해서

Scan + 필터키 기반 Query
읽는 것테이블의 모든 아이템한 파티션, SK로 좁혀짐
과금 용량필터 전, 테이블 전체슬라이스의 아이템만
우리 예제~250,000 RCU (~500MB)수백 RCU (~2MB)
지연 시간테이블 크기에 따라 증가테이블이 커져도 평평
결과 개수비용에 대해 아무것도 결정 안 함치르는 값과 맞아떨어짐

표가 담은 교훈: Scan에서는 결과 개수와 청구서가 무관합니다. Query에서는 둘이 서로를 따라갑니다.

Scan하기 전에 결정하세요

대부분의 우발적 Scan은 한 질문에서 옵니다. 내가 필요한 파티션을 지목할 수 있나? 그렇다면 Query입니다. 아니라면, 해결은 더 큰 필터가 아니라 키입니다.

흐름도로 본 결정입니다.

아니오아니오아이템을 읽어야파티션 키를 아나?Query 파티션GSI로 키잉할 있나?GSI 추가, 그다음 QueryScan 최후의 수단

경로는 거의 항상 Query에서 끝납니다. 현재 있든 추가 가능하든 어떤 키도 액세스 패턴에 맞지 않을 때만 Scan으로 떨어집니다.

패턴이 실재하고 반복되지만 베이스 테이블이 키잉할 수 없다면, 그게 질문을 Query로 만들 글로벌 보조 인덱스를 추가하라는 신호입니다. 액세스 패턴을 중심으로 키를 미리 모델링하는 것이 게임 전부입니다 — 싱글 테이블 디자인을 보세요.

필터가 아니라 키 기반 쿼리를 작성하세요

키를 넘어선 조건이 정말 필요하면, 모든 걸 FilterExpression에 쏟아붓는 대신 신중히 지으세요. DynamoDB 표현식 빌더KeyConditionExpression과 속성 플레이스홀더를 대신 생성하므로, 파티션 키와 정렬 키가 좁히기를 합니다 — DynamoDB가 읽기를 계측한 후가 아니라 전에.

KeyConditionExpression: PK = :tenant AND begins_with(SK, :day)

Scan이 실제로 괜찮은 때

Scan이 금지된 건 아닙니다 — 그저 잘못된 기본값일 뿐이죠. "모든 걸 읽어라"를 진심으로 뜻할 때는 옳은 도구입니다:

  • 일회성 내보내기 나 손으로 실행하는 백필.
  • 자그마한 설정 / 조회 테이블, 테이블 전체가 몇 KB인 경우.
  • 백그라운드 작업 으로 일부러 테이블 전체를 페이지로 넘기는 것. 그런 건 Segment / TotalSegments로 워커에 나누세요 — 병렬 스캔 — 하나의 긴 순차 크롤 대신. (AWS: 테이블 스캔)

그리고 PartiQL이 구해 주지 않음에 주목하세요. 키 술어 없는 SELECT * FROM ProductEvents WHERE eventType = 'checkout'은 곧장 Scan으로 컴파일됩니다. SQL 옷을 입은 같은 함정입니다. (전체 분석은 Query vs Scan을 보세요.)

진정으로 아이템 간 분석 — DynamoDB가 표현할 수 없는 GROUP BY, JOIN, 집계 — 이 필요할 때, DynoTable의 SQL Workbench는 테이블을 전체 Scan으로 두들기는 대신 한정된 결과 집합에 대해 클라이언트 측으로 그것들을 실행합니다.

다음 단계

용량 계산기로 어느 패턴이든 비용을 추정하고, API 수준의 대비는 Query vs Scan을 읽고, DynoTable을 다운로드해 이것들을 여러분 테이블에 실행하며 소비된 용량을 직접 지켜보세요.

업데이트됨