Python (boto3) での DynamoDB Scan
boto3 における scan は 2 つの決定です。リクエストそのものと、それをどうページングするか。以下のスニペットは組み込みのページネーターを使っています。これは誰かがあなたのループの周りに書いたラッパーではありません。botocore の設定 5 行であり、その 5 行が、スキャンが正しいかどうかと、いくらかかるかを決めます。(そもそもスキャンすべきかどうかは別の問題です。)
コード
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に操作ごとのエントリを 1 つずつ同梱しており、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なので、マージされた結果は 1 ページ分のキャパシティをスキャン全体のものであるかのように報告します。ページごとに自分で合計しないと、ページ数の分だけ過小報告することになります。FilterExpressionは読み取りの後にサーバー側で走るので、課金はCountではなくScannedCountに対して行われます。#filter0がYearをエイリアスしているのは、それが予約語だからです。エイリアスがなければ、リクエストは何も読まないうちに失敗します。- エラーはすべて
botocore.exceptions.ClientErrorとして届きます。e.response["Error"]["Code"]で分岐しましょう。エラーごとのクラスはクライアント上に生成される属性としてのみ存在し(client.exceptions.ProvisionedThroughputExceededException)、import できるシンボルとしては決して存在しません。 - リソース API はもう一方の使い勝手です。
Table.scanはネイティブな Python の型を取り、数値をdecimal.Decimalで返し、プレースホルダーのマップではなくAttr("Year").gte(2010)でフィルターを組み立てます。
フィルター付きの 1 ページが実際にいくらかかるか
それぞれ約 2 KB のアイテム 60 件、そのうち 2 件が 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実体のある 6 ページのうち 4 ページが、満額を払って何も返しませんでした。これこそ、ページネーターが防ぐために存在するバグの形です。Items が空のときに break する自作のループは 1 ページ目で終了し、合致する 2 曲を 0 件と報告します。
7 ページ目がもう半分です。6 ページ目はテーブルの最後のアイテムで Limit に達したので、DynamoDB はそれでも LastEvaluatedKey を返し、ページネーターはもう 1 往復かけて「もう何もない」ことを知りました。LastEvaluatedKey が意味するのは「まだ続きがある」ではなく「そこで止めた」です。
同じスキャンで build_full_result() を呼ぶと CapacityUnits: 2.5 と報告されます。6 ページの実消費は 15.0 でした。
ループを書かずにページングする
DynamoDB クエリビルダーは、フィルター、エイリアスのマップ、ページネーションのループを 1 つの実行可能なプログラムとして組み立てるので、上の Limit 対 Count の罠は貼り付ける前に処理済みです。スクリプトからではなく対話的に実際のテーブルをページングするには、DynoTable をダウンロードしてください。
関連ガイド
- Query と Scan の比較 — (まれに)
scanが正当化されるのはどんなときか。 - DynamoDB の Scan はなぜ遅くて高いのか? — コストモデルと、その回避方法。
- 並列スキャン —
Segment/TotalSegmentsでテーブルを分割し、セグメントごとに 1 つのページネーターを回す。 - DynamoDB ProvisionedThroughputExceededException — テーブル全体のスキャンが、プロビジョニング済みテーブルのキャパシティに何をするか。
参考資料
- Scan — Amazon DynamoDB API Reference
- scan — Boto3 DynamoDB.Client Reference
- Scan paginator — Boto3 DynamoDB Reference
- Scanning tables — Amazon DynamoDB Developer Guide
最終検証日 2026-07-28、上記にリンクした公式 AWS ドキュメントに照らして確認しました。