Python(boto3)의 DynamoDB Scan
boto3에서 scan은 두 가지 결정입니다. 요청, 그리고 그것을 어떻게 페이지네이션하느냐. 아래 스니펫은 내장 페이지네이터를 씁니다. 누군가 여러분의 루프를 감싸 만든 래퍼가 아닙니다. botocore 설정 다섯 줄이며, 그 다섯 줄이 여러분의 Scan이 올바른지와 비용이 얼마인지를 결정합니다. (애초에 Scan을 해야 하는지는 다른 질문입니다.)
코드
import boto3
client = boto3.client("dynamodb")
paginator = client.get_paginator("scan")
items = []
for page in paginator.paginate(
TableName="Music",
FilterExpression="#filter0 >= :filterValue0",
ExpressionAttributeNames={"#filter0": "Year"},
ExpressionAttributeValues={":filterValue0": {"N": "2010"}},
):
items.extend(page["Items"])
print(f"Matched {len(items)} items")설명
- 페이지네이터는 코드가 아니라 데이터입니다. botocore는
paginators-1.json에 작업당 항목 하나를 담아 배포하며,Scan의 항목은{"input_token": "ExclusiveStartKey", "output_token": "LastEvaluatedKey", "limit_key": "Limit", "result_key": ["Items", "Count", "ScannedCount"], "non_aggregate_keys": ["ConsumedCapacity"]}입니다. 아래의 모든 것이 이 키들에서 따라 나옵니다. PaginationConfig={"PageSize": n}은Limit을 설정합니다.Limit이limit_key이기 때문입니다.Limit은 읽는 항목 수를 제한할 뿐 반환되는 항목 수는 제한하지 않으므로,FilterExpression이 있으면 페이지가 비어 있으면서도 비용은 들 수 있습니다.MaxItems는result_key항목을 셉니다. 그리고 나중 프로세스에서StartingToken으로 전달할 수 있는NextToken을 돌려줍니다. 요청이 여러분의 컷오프를 넘겨 읽는 것을 막지는 않습니다.build_full_result()는result_key필드만 집계합니다.Items,Count,ScannedCount는 합산되지만ConsumedCapacity는non_aggregate_key이므로, 병합된 결과는 한 페이지의 용량을 마치 Scan 전체의 것인 양 보고합니다. 페이지마다 직접 합산하세요. 그러지 않으면 페이지 수만큼 과소 보고하게 됩니다.FilterExpression은 읽기 후 서버 측에서 실행되므로, 요금은Count가 아니라ScannedCount기준입니다.Year가 예약어이므로#filter0으로 별칭을 붙였습니다. 별칭이 없으면 요청은 아무것도 읽기 전에 실패합니다.- 오류는 모두
botocore.exceptions.ClientError로 도착합니다.e.response["Error"]["Code"]로 분기하세요. 오류별 클래스는 클라이언트에 생성되는 속성(client.exceptions.ProvisionedThroughputExceededException)으로만 존재하며, 임포트할 수 있는 심볼로는 존재하지 않습니다. - 리소스 API는 또 다른 사용성입니다.
Table.scan은 네이티브 Python 타입을 받고, 숫자를decimal.Decimal로 반환하며, 플레이스홀더 맵 대신Attr("Year").gte(2010)으로 필터를 만듭니다.
필터가 걸린 페이지 하나의 실제 비용
각각 약 2 KB인 항목 60개, 그중 둘이 Year = 2024에 해당하고, PageSize=10으로 DynamoDB Local을 상대로 실행했습니다:
page 1: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 2: Count=1 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 3: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 4: Count=1 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 5: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 6: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 7: Count=0 ScannedCount=0 CU=0.0 LastEvaluatedKey=no
total CU across pages: 15.0실제 페이지 여섯 개 중 넷이 아무것도 반환하지 않았고, 값은 그대로 치렀습니다. 이것이 페이지네이터가 막으려고 존재하는 버그의 모습입니다. Items가 비면 끊는 직접 만든 루프는 1페이지에서 그만두고, 조건에 맞는 두 곡을 0곡으로 보고합니다.
7페이지가 나머지 절반입니다. 6페이지가 테이블의 마지막 항목에서 Limit에 걸렸기 때문에 DynamoDB는 그래도 LastEvaluatedKey를 반환했고, 페이지네이터는 남은 것이 없다는 사실을 알아내려고 왕복을 한 번 더 썼습니다. LastEvaluatedKey는 "더 있다"가 아니라 "내가 멈췄다"는 뜻입니다.
같은 Scan에서 build_full_result()를 호출하면 CapacityUnits: 2.5를 보고합니다. 여섯 페이지가 소비한 것은 15.0입니다.
루프를 쓰지 않고 페이지 넘기기
DynamoDB 쿼리 빌더는 필터, 별칭 맵, 페이지네이션 루프를 하나의 실행 가능한 프로그램으로 조립하므로, 위의 Limit 대 Count 함정이 붙여 넣기 전에 처리되어 있습니다. 스크립트가 아니라 대화형으로 실제 테이블을 페이지네이션하려면, DynoTable을 다운로드하세요.
관련 가이드
- Query vs. Scan — (드물게)
scan이 정당화되는 때. - 내 DynamoDB Scan은 왜 느리고 비쌀까요? — 비용 모델과 그것을 피하는 방법.
- 병렬 Scan —
Segment/TotalSegments로 테이블을 나누고 세그먼트마다 페이지네이터 하나. - DynamoDB ProvisionedThroughputExceededException — 전체 테이블 Scan이 프로비저닝된 테이블의 용량에 무슨 일을 하는지.
참고 자료
- Scan — Amazon DynamoDB API Reference
- scan — Boto3 DynamoDB.Client Reference
- Scan paginator — Boto3 DynamoDB Reference
- Scanning tables — Amazon DynamoDB Developer Guide
위에 링크된 공식 AWS 문서를 기준으로 2026-07-28에 마지막으로 검증했습니다.