VOICE

JavaとDoma2を使った大量データ一括処理のパフォーマンス比較と改善策【ZYYX Tech Blog】

こんにちは。第二事業部でエンジニアをしている盛です。普段はサーバサイドエンジニアとして、WEBアプリケーションの開発に携わっています。
業務の中で大量データを扱う際に直面したパフォーマンスの問題について、私が取り組んだ解決策と、検証結果についてお話したいと思います。

はじめに

概要

Doma2を使用して、大量データをできるだけ高速に一括処理する方法について、複数の方法を比較しながらご紹介します。

この記事の対象読者

  • Javaのデータアクセスに、ORMを使った開発を始めた方
  • Doma2の初心者の方
  • 一括登録・一括編集の方法に興味がある方
  • パフォーマンスを意識したコードを書きたい方

バージョン

Java:openjdk 17.0.1 2021-10-19
Spring Boot : 3.0.0
MySQL:8.0.18
Doma2:2.54.1
Doma Spring Boot : 1.7.0

背景・課題

数万、数十万といった大量のデータをデータベースに保存した時、適切な最適化をせずに処理を行った結果、膨大な時間がかかってしまいました。
データベースへのリクエスト回数を減らし、保存時間を効率的に短縮するにはどうするべきか?
新しい解決策を比較しながら適切な実装を行い、パフォーマンス低下と時間の無駄を減らします。

大量データ処理のパターン

私が考えた大量データの一括処理のパターンを3つ挙げ、それぞれのパフォーマンスの違いを検証します。

1)for文を使用してデータ登録を要素の数だけ行う
この低下が予想されますが、比較のために検証を行います。

この方法は、データの数だけデータベースへリクエストを送信するため、パフォーマンスが低下することが予想されます。ただし、比較のために検証を行います。

2)Doma2の機能を使用してバッチ処理を実行
Doma2ではデータを登録する際、1件ずつの登録には @Insert アノテーションを使用します。しかし、複数データを効率的に登録する方法として @BatchInsert が存在することがわかったため、試すことにしました。

3)SQLでVALUES句を”,”で区切って処理
後述しますが、上記2つがうまく機能せず、SQLを作成して一括処理を行いました。
Doma2では、SQL内にfor文やif文を記述することができるので、繰り返し処理や条件分岐を自由に組み込めます。

前提条件

今回の検証はDoma2を使用します。
テスト用に作成したクラスやSQLファイルは以下のとおりです。

Entity
Dao
SQL

一括登録の検証

テストデータ10000件を登録する際にかかる時間を計測し、比較します。
※HogeDaoを使用するため、Springやlombokを導入しています

一括登録の検証

for文による一括処理

実際のログは以下です。

for文による一括処理

1件ごとにENTERとEXITを繰り返し、複数回メソッドが呼び出されていることがわかります。都度メソッドの呼び出しやSQLの処理が走るため、時間がかかります。

Doma2のBatchInsertを利用した一括処理

続いて、Doma2の機能にあるbatchInsertを用いて一括登録を行います。

Doma2のBatchInsertを利用した一括処理

メソッドの呼び出し自体は1回です。しかし、ログを見ると1件ずつSQLの処理が呼び出され、時間がかかる処理になっているようです。

自作のSQLによる一括処理

最後に、自分で書いたSQLでの一括登録です。

自作のSQLによる一括処理

メソッドの呼び出しもSQLの処理も一回で済み、DBへのリクエスト回数を減らすことができています。
※実際はSQLの部分に改行が挟まりますが、削除しています

結果

コンソールに出力した結果です。

結果

結果を見ると、自作SQLの処理が圧倒的に速いことがわかります。

パフォーマンス観点から、一括登録はSQLを作成したメソッドが非常に優れていることがわかりました。

【注意】SQL文の長さには限界があり、限界を超えるとエラーが発生します。 処理が速くても、中断しては意味がありません。データが多い時は、ある程度のサイズで区切って実行することが重要です。

一括編集

一括編集も、一括登録同様にSQLでまとめて処理を行えば、速度が向上する可能性があります。
問題が発生するのはUPDATE文で、単純に","で区切った連結はできません。
編集にはデータを指定する必要があり、登録データとの関連性や、PRIMARY KEYを用いて特定します。
そのため、一括編集ではELT()とFIELD()を使用してSQLを組み立てます。

ELT()

MySQLのELT()関数は、指定したインデックスの位置に対応する値を返す関数です。複数の文字列値からなるリストや配列のようなデータ構造を持ち、指定した位置の値を取得する時に使用できます。

ELT()

具体例として以下のような記述ができます。

ELT()_2

このSQLで取得できる値は、2番目の Bravo です。
ELT()関数を使用することで、編集する値をリストから取得出来そうです。

FIELD()

MySQLのFIELD()関数は、指定した値がリスト内のどの位置にあるかを検索し、その位置を返す関数です。指定した値がリスト内に存在するか、またその位置が何番目かを調べる時に使用されます。

field

具体例として以下のような記述ができます。

field_2

組み合わせ

ELT()関数とFIELD()関数を組み合わせて、一括登録したデータの一部を書き換えてみます。

組み合わせ

管理IDが、1と2のデータを上のSQL実行後に取得してみると

実行結果

このように変更できました。

一括編集用のSQL

HogeDaoに新たにメソッドを追加します。

一括編集用のSQL

続いてSQLも作成します。
先程までのSQLを、Doma2のif文やfor文を駆使してSQLを記述します。

一括編集用のSQL_2

内容は

  • nameのELT()の中のFIELD()のid以降に、メソッドで与えたHogeリストの管理IDを設定
  • ELTのFIELD()以降に、Hogeリストのnameを設定
  • WHERE句のIN句で、Hogeリストの管理IDを設定

実行すると

一括編集用のSQL_3

1回のメソッド呼び出しで動いていますね。

さいごに

  • リクエスト回数はできるだけ少ない方が良い
  • Doma2の@BatchInsert, @BatchUpdateはメソッド的には1回だが、リクエストは複数
  • SQLは記述量の上限があるため、一定のデータ量で区切って処理することも忘れない

※SQLにまとめすぎると Packet for query is too large というエラーが出ます

Doma2を使用してSQLを自作することで、大幅な時間の短縮となり、パフォーマンスアップにつながりました。
今回の検証では一万件のデータを扱いましたが、実際のプロジェクトでは数十万件のデータを処理する必要があったため、恩恵も大きなものでした。

また、一括登録方法は知っていましたが、チューニングで更新処理も一括で出来ることを知り、思わぬ副産物を得ることができました。