大量データ処理でパフォーマンスが低下する原因

Django でデータベースに複数のレコードを作成する場合、ループ内で save() を呼び出す書き方をよく見かけます。

for item in items:
    MyModel.objects.create(name=item["name"], value=item["value"])

この書き方は直感的でわかりやすいのですが、レコードの数だけ INSERT 文が発行されるため、データ件数が増えるとパフォーマンスが大幅に低下します。たとえば 1,000 件のレコードを作成する場合、データベースへのクエリが 1,000 回実行されることになります。
本記事では、この問題を解決する bulk_create と bulk_update の使い方を解説します。

bulk_create で一括作成する

bulk_create は、複数のモデルインスタンスを 1 回の SQL クエリでまとめてデータベースに挿入するメソッドです。

instances = [MyModel(name=item["name"], value=item["value"]) for item in items]
MyModel.objects.bulk_create(instances)

この方法なら、1,000 件のレコードであっても発行される SQL は 1 回だけです。データ件数が多い場合は batch_size パラメーターを指定することで、一定件数ずつ分割して挿入できます。

MyModel.objects.bulk_create(instances, batch_size=500)

batch_size を指定すると、上記の例では 500 件ずつ 2 回に分けて INSERT が実行されます。メモリ使用量が気になる場合やデータベース側の制約がある場合に有効です。

bulk_update で一括更新する

既存のレコードを大量に更新する場合も、ループ内で save() を呼び出すと同様のパフォーマンス問題が発生します。bulk_update を使えば、複数のレコードを 1 回のクエリで更新できます。

products = Product.objects.filter(category="electronics")
for product in products:
    product.price = int(product.price * 0.9)

Product.objects.bulk_update(products, fields=["price"])

bulk_update の第 2 引数 fields には、更新対象のフィールド名をリストで指定します。ここで指定したフィールドだけが UPDATE 文に含まれるため、意図しないフィールドが上書きされる心配がありません。
bulk_create と同様に batch_size パラメーターも利用可能です。

使用時の注意点

bulk_create と bulk_update は高速ですが、以下の点に注意が必要です。

  1. モデルの save() メソッドが呼ばれない
    bulk_create / bulk_update はデータベースに直接 SQL を発行するため、モデルに定義した save() メソッドのオーバーライドは実行されません。save() 内でバリデーションや加工処理を行っている場合は、事前に手動で実行する必要があります。
  2. シグナル (pre_save, post_save) が発火しない
    Django のシグナル機構を利用している場合、bulk 操作ではシグナルが送信されません。シグナルに依存した処理がある場合は、別途対応が必要です。
  3. auto_now フィールドが更新されない
    bulk_update では auto_now=True が設定された updated_at のようなフィールドは自動更新されません。更新が必要な場合は fields に明示的に含め、値を手動で設定してください。

まとめ

大量のレコードを処理する場合は、ループ内での save() の代わりに bulk_create / bulk_update を使うことで、データベースへのクエリ数を大幅に削減できます。ただし、save() メソッドやシグナルが実行されない点には注意が必要です。
パフォーマンスが問題になる場面では、まず Django Debug Toolbar などでクエリ数を確認し、bulk 操作への置き換えを検討してみてください。