Python での DynamoDB バッチ書き込み(boto3 batch_writer)
batch_writer() は、Python が他の SDK より手数が少なくて済む唯一の DynamoDB 呼び出しです。put と delete をバッファリングし、25 件ずつの BatchWriteItem リクエストに切り分け、未処理アイテムを自分で再送します。やってくれないのは、たいていの一括ロードを壊す 2 つの失敗から守ることで、そのどちらも、問題のアイテムを渡した行ではなくフラッシュ時に表面化します。
コード
import boto3
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("Music")
songs = [
{"Artist": "Arturo Sandoval", "SongTitle": "Cubano Chant", "AlbumTitle": "Danzon", "Year": 1994},
{"Artist": "Arturo Sandoval", "SongTitle": "A Mis Abuelos", "AlbumTitle": "Danzon", "Year": 1994},
{"Artist": "Arturo Sandoval", "SongTitle": "Groovin' High", "AlbumTitle": "Swingin'", "Year": 1996},
]
with table.batch_writer() as batch:
for song in songs:
batch.put_item(Item=song)
# batch_writer buffers deletes too — target a key you're NOT also putting
# (two writes to the same key in one batch are rejected as a duplicate)
batch.delete_item(Key={"Artist": "Ella Fitzgerald", "SongTitle": "Misty"})
print(f"Buffered {len(songs)} puts + 1 delete; the batch flushes on exit")解説
- 遅延フラッシュ —
batch.put_item()はリストに追加するだけです。バッファが 25 件に達するかwithブロックを抜けるまで、検証もシリアライズも送信も行われません。そのため不正なアイテムのトレースバックは、それを渡したput_itemの呼び出しではなくフラッシュから来ます。イテレータからロードしているなら、何をバッファに入れたかのインデックスを自分で持っておきましょう。 - 素の Python の値 — これはリソース API なので、
{"N": "1994"}ではなく1994と書きます。小数を扱うならDecimalが必須です。floatはバッファには受け入れられ、フラッシュ時に拒否されます。 batch_writer()はTableのメソッド。読み取り側の対応物はそうではありません。batch_get_itemはServiceResourceにあり、table.batch_get_itemは存在しません。バッチ読み取りには、バッファリングもチャンク分割もリトライのヘルパーも一切ありません。- エラーではなく
UnprocessedItems— 面倒を見てくれるリトライはそれだけです。スロットリングされた書き込みは再送されますが、ValidationExceptionはそのまま伝播します。代わりにclient.batch_write_itemを使うと、ループ全体が自分の手に渡ってきます。Node.js の例のとおりです。 - サービスの制限を引き上げることはできません。1 リクエストあたり 25 件の書き込み、アイテムあたり 400 KB、リクエストあたり 16 MB、条件指定も更新も不可、そして put はすべて格納済みのアイテム全体を置き換えます。ガードが欲しい、あるいはオールオアナッシングにしたい場合は TransactWriteItems を使いましょう。
batch_writer がフラッシュ時に実際にしていること
30 件の put をバッファリングして、どんな呼び出しをするか観察してみます。table.meta.client.batch_write_item をラップしてリクエストのサイズを記録し、DynamoDB Local 3.3.0 に対して実行した結果です。
batch sizes sent: [25, 5]サービスの制限で切られた 2 リクエストで、残りは __exit__ がフラッシュしています。このフラッシュは無条件です。ブロック内で RuntimeError を送出しても、バッファ済みのアイテムは抜ける途中で書き込まれます。途中で死んだ一括ロードが残すのは、まっさらな状態ではなく中途半端なロードです。
さて、2 つの失敗です。まず、同じキーを 2 回 バッファリングする場合。元データに重複があった瞬間に起こることです。
with table.batch_writer() as batch:
batch.put_item(Item={"Artist": "Dup", "SongTitle": "Key", "Year": 1})
batch.put_item(Item={"Artist": "Dup", "SongTitle": "Key", "Year": 2})botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the
BatchWriteItem operation: Provided list of item keys contains duplicatesどちらの put_item も文句を言いませんでした。batch_writer() は、頼まない限り重複除去をしません。頼み方は table.batch_writer(overwrite_by_pkeys=["Artist", "SongTitle"]) です。同じ 2 つの put をこれに通すと、アイテムは Year: 2 として格納されます。バッファはキーごとに最後の書き込みを残すので、その 2 行が本来別のアイテムであるはずだったのにキーを取り違えていた場合、この重複除去は静かなデータ損失になります。
2 つ目は boto3 固有のもので、DynamoDB には決して届きません。
TypeError: Float types are not supported. Use Decimal types instead.4.5 という Rating は何も言われずバッファに収まり、フラッシュで吹き飛びます。Decimal("4.5") なら {"N": "4.5"} として正しく往復します。json.loads で価格や評価を JSON から読むと数値はすべて float になるので、これはたいていのインポートスクリプトにとって初回実行での失敗です。json.loads に parse_float=Decimal を渡せば、大元で解決できます。
ネイティブな Python の値とワイヤ形式のあいだを手作業で行き来しているなら、DynamoDB JSON コンバータが同じアイテムの両側を並べて表示するので、自分の Decimal が実際に何になるのかを確認できます。
CSV や JSON から、型のマッピングを自分で書かずに一括ロードするには、DynoTable をダウンロードしてください。
関連する例
- Node.js での DynamoDB BatchWriteItem — batch_writer が隠している手書きのリトライループ。
- AWS CLI での DynamoDB BatchWriteItem — 同じバッチ書き込みをシェルから。
- Python での DynamoDB PutItem — これがバッチ化している単一アイテムの書き込み。
- DynamoDB のバッチ操作 — 制限、部分的な失敗、そしてバッチ化が割に合う場面。
- 「Too many items requested for the BatchWriteItem call」 — 1 バッチに 25 件を超える put/delete リクエストを入れた場合。
- 「Provided list of item keys contains duplicates」 — 1 バッチ内で同じキーに触れる 2 つのリクエスト。
参考資料
- Amazon DynamoDB guide (batch_writer) — Boto3 documentation
- BatchWriteItem — Amazon DynamoDB API Reference
- Error handling with DynamoDB — Amazon DynamoDB Developer Guide
最終検証日 2026-07-28、上記にリンクした公式 AWS ドキュメントに照らして確認しました。