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_itemServiceResource にあり、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.loadsparse_float=Decimal を渡せば、大元で解決できます。

ネイティブな Python の値とワイヤ形式のあいだを手作業で行き来しているなら、DynamoDB JSON コンバータが同じアイテムの両側を並べて表示するので、自分の Decimal が実際に何になるのかを確認できます。

CSV や JSON から、型のマッピングを自分で書かずに一括ロードするには、DynoTable をダウンロードしてください。

関連する例

参考資料

最終検証日 2026-07-28、上記にリンクした公式 AWS ドキュメントに照らして確認しました。

Console なしで DynamoDB を扱う

DynamoDB では実行できない本物の SQL(JOINs、GROUP BY、集計)を実行する高速な DynamoDB デスクトップクライアント。ビジュアル編集と、あなた自身の Bedrock キーで動く AI エージェントを備えています。

30日間無料トライアル、クレジットカード不要 — その後は期限のない Free プラン。