Golangで実現するMongoDB大量Upsert戦略 ~設計・実装・パフォーマンス考察~

はじめに

本記事では、Go言語を用いてMongoDBに対して大量のUpsert処理を実行する際の設計と実装、そしてパフォーマンスの観点から考察を行います。なお、ここで取り上げる実行結果は実際のベンチマークに基づいており、各手法の特徴や適用シーンを明確にするためのものです。この記事はあくまで一つの見解ですので、利用される環境や要件に応じた最適な選択は各自で判断してください。

開発環境

  • Go: バージョン 1.24
  • MongoDB: バージョン 8.0
  • mongo-driver: go.mongodb.org/mongo-driver/v2 (v2.1.0)

MongoDBドライバーの主要メソッド

大量のUpsert処理を実現するために、以下の3つのメソッドの理解が必要です。

UpdateOne

概要: 単一ドキュメントの更新を行います。upsertオプションをtrueにすることで、該当ドキュメントが存在しない場合に新規作成(Insert)も実施します。

特徴: 単体テストが容易で、エラー発生時に個別対応が可能なため、初心者にも扱いやすいです。

UpdateMany

概要: 複数ドキュメントを一度に更新可能です。ただし、主に1つのフィルタ条件に対して複数の更新対象がある場合に利用します。

注意点: 複数の顧客IDにまたがる更新など、複雑なシナリオには適しません。

BulkWrite

概要: 複数の更新処理をバッチとして一括実行します。MongoDBサーバとの通信回数を最小限に抑え、ネットワークラウンドトリップのオーバーヘッドを低減します。

オプション:

Ordered BulkWrite: 指定した順序でバッチを実行。エラー発生時に中断されるため、詳細なエラーハンドリングが難しくなる場合があります。

Unordered BulkWrite: 順序に依存せず平行実行されるため、高速な処理が期待できますが、エラーハンドリングはさらに複雑です。

ベンチマークテストの概要

本記事では、以下の3つの手法でUpsert処理のパフォーマンスを比較しました。

  • ForループによるUpdateOneの連続実行
  • Ordered BulkWriteによる一括Upsert
  • Unordered BulkWriteによる一括Upsert

テストケースとして、全体件数をn件とし、その半数(n/2件)に対してInsert、残り半数に対してUpdateが実行されるシナリオを採用しました。

GitHub上でベンチマーク用のコードを公開しており、実際の動作確認にはDocker環境が必要です。(エントリーポイントは command/benchmark/main.go となっています。)

測定結果

下記は各手法で実施したベンチマークの実行結果(ns/op)を対数グラフとして示した表です。

※「Ordered BulkWrite」と「Unordered BulkWrite」はほぼ同等のパフォーマンスを示しています。

件数Upsert [ns/op]OrderedBulkWrite [ns/op]UnorderedBulkWrite [ns/op]
2115,841112,584111,761
10561,951135,917138,510
50028,710,0983,098,2853,138,651
1,00057,223,4085,553,6365,836,793
5,000284,290,82316,828,07817,224,555
10,000565,461,89633,233,71733,868,402
50,0002,849,452,458215,622,847179,378,590
100,0005,650,468,333358,285,472367,965,097

※上記の数値は実際のベンチマーク結果に基づくものであり、実行環境により変動する可能性があります。

上記を対数グラフにしたものは以下です。
BulkWriteをしたときの順序の有無による大きなパフォーマンスの違いがないことがわかります。

考察と適用シナリオ

UpdateOneをforループで実行

メリット:

• 単体テストがシンプルで、エラー発生時の個別対応が可能。

• 初心者や保守性重視の開発に適している。

デメリット:

• 件数が3000件を超えると1秒以上の遅延が発生するため、パフォーマンス面で劣る。

推奨シナリオ:

• 更新件数が数百件以下の場合。

Ordered BulkWrite

メリット:

• 複数件のアップサートを一括処理するため、ネットワークラウンドトリップが削減され高速に処理可能。

デメリット:

• エラーハンドリングが一括実行のため、トラブルシュートが複雑になる可能性がある。

推奨シナリオ:

• 数千件以上のアップサートが必要な場合で、処理順序が重要なケース。

Unordered BulkWrite

メリット:

• 並列実行が可能なため、Ordered BulkWriteとほぼ同等の速度を発揮。

• 大量件数の更新処理において、さらに高速なパフォーマンスが期待できる。

デメリット:

• エラーハンドリングがさらに複雑になり、原因特定や対応が難しくなる。

推奨シナリオ:

• 数千件以上のアップサートで、処理順序に依存しない場合。

InsertOneとUpdateOneの組み合わせ

概要:

• Upsert機能を使用せず、あらかじめ存在確認を行い、存在しなければInsertOne、存在すればUpdateOneを実行する戦略も有効です。

メリット:

• 単体テストが容易で、エラー処理が明確に行えるため、初心者にとって理解しやすい実装パターン。

留意点:

• データの整合性や処理速度については、ケースバイケースで評価する必要があります。

まとめ

少量(数百件以下)の更新:

保守性やデバッグの容易さを重視する場合は、UpdateOneをforループで実行する戦略が有効です。

大量(数千件以上)の更新:

パフォーマンスを最重視する場合は、BulkWriteの利用が効果的です。

• 処理順序が必要な場合はOrdered BulkWriteを、順序に依存しない場合はUnordered BulkWriteを選択すると良いでしょう。

その他の実装手法:

Upsert機能を使わず、InsertOneとUpdateOneを組み合わせる方法も検討の価値があります。

BulkWriteはMongoDBの基本的な高速化手法の一つですので、公式ドキュメント「db.collection.bulkWrite()」も併せて確認することをお勧めします。


以上、各手法のメリット・デメリットと実際のベンチマーク結果に基づく考察を通して、用途に応じた最適な戦略選択の参考になれば幸いです。

GitHub リポジトリ

このベンチマークの詳細なコードは、以下の GitHub リポジトリで公開しています。

GitHub – taako-502/go-mongodb-bulk-vs-…

GitHub – taako-502/go-mongodb-bulk-vs-…

BulkWriteを使う時とそうでない時のUpsert処理の速度比較. Contribute to taako-502/go-mongodb-bulk-vs-single-upsert development by creating an …

BulkWriteを使う時とそうでない時のUpsert処理の速度比較. Contribute to taako-502/go-mongodb-bulk-vs-single-upsert development by creating an …