大量のデータをDBに登録する処理を見ていると、「バルクインサート」という言葉が出てきます。
最初は「普通のINSERTと何が違うんだろう?」と思ったのですが、調べていくと、単にINSERTの書き方だけではなく、JavaがどうやってDBとやり取りしているのかにもつながってきました。
この記事では、JDBCのaddBatch()とexecuteBatch()を中心に、
- バルクインサートとは何か
- JDBCとは何か
- JavaからDBへどうやってSQLを送っているのか
- なぜバッチ処理で効率化できるのか
を整理します。
まず、バルクインサートとは?
バルクインサートは、複数件のデータをまとめてDBへ登録する考え方です。
例えば、ユーザーを1000件登録するとします。
単純に1件ずつ処理するなら、
for (User user : users) {
ps.setInt(1, user.getId());
ps.setString(2, user.getName());
ps.executeUpdate();
}
のように書けます。
この場合、ループするたびにexecuteUpdate()を実行します。
件数が少なければ問題にならないこともあります。
しかし、大量データではDBとのやり取りを何度も繰り返すことになります。
そこで出てくるのがバッチ処理です。
JDBCのaddBatch()とexecuteBatch()
JDBCでは、複数の更新処理をバッチとして実行できます。
String sql = "INSERT INTO users (id, name) VALUES (?, ?)";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
for (User user : users) {
ps.setInt(1, user.getId());
ps.setString(2, user.getName());
ps.addBatch();
}
ps.executeBatch();
}
ポイントは、
ps.addBatch();
で実行する内容をバッチへ追加し、
ps.executeBatch();
でまとめて実行しているところです。
自分の中では、
setXXX()
↓
値を設定
addBatch()
↓
実行対象として追加
executeBatch()
↓
バッチを実行
と考えると理解しやすかったです。
そもそもJDBCって何?
ここまで調べていて、
そもそもJDBCって何だろう?
という疑問が出ました。
JDBCはJava Database Connectivityの略で、Javaからリレーショナルデータベースへアクセスするための標準APIです。
例えば、
Connection
PreparedStatement
ResultSet
などがJDBCでよく登場します。
JavaからSQLを実行する場合、
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setInt(1, 10);
ResultSet rs = ps.executeQuery();
のように書けます。
つまりJDBCは、
JavaとDBの間でSQLを実行したり、結果を受け取ったりするための仕組み
と考えると分かりやすいです。
JDBCとJDBC Driverは別物
ここも最初は同じものだと思っていました。
実際には、
JDBC
と
JDBC Driver
は別です。
JDBCはJava側の標準API・仕様です。
一方、PostgreSQLなどのDBには、それぞれJDBC Driverがあります。
Java側では共通のJDBC APIを使い、その先で各DB用のJDBC Driverが実際のDBとの通信を担当します。
この構造を知ると、
「なぜPostgreSQL JDBC Driverというものを別途使うのか」
も理解しやすくなりました。
Spring Bootを使っていたらJDBCは使わない?
もう一つ疑問だったのがこれです。
Spring Bootでは、
jdbcTemplate.query(...);
のようなコードを書くことがあります。
では、JdbcTemplateを使っているならJDBCは使っていないのでしょうか。
そうではありません。
JdbcTemplateは、JDBCをSpringから扱いやすくするための仕組みです。
JDBCを直接使うと、
Connection
PreparedStatement
ResultSet
などを自分で扱う場面が増えます。
Spring JDBCでは、その面倒な部分をJdbcTemplateがかなり肩代わりしてくれます。
そのため、
ps.addBatch();
ps.executeBatch();
はJDBCを直接利用したバッチ処理。
Spring JDBCなら、
jdbcTemplate.batchUpdate(...);
という書き方ができます。
JavaからDBを扱う方法はJDBCだけ?
さらに調べると、JavaからDBを扱う方法にはいくつか選択肢がありました。
代表的なものとして、
| 方法 | 特徴 |
|---|---|
| JDBC | Java標準のDBアクセスAPI |
| JdbcTemplate | SpringからJDBCを扱いやすくする |
| MyBatis | SQLを自分で制御しやすい |
| JPA / Hibernate | Javaオブジェクトを中心にDBを扱える |
| jOOQ | SQLをJavaから型安全に組み立てやすい |
があります。
例えばJPAを使うと、
User user = new User();
user.setName("田中");
userRepository.save(user);
のように書けます。
コード上では、
INSERT INTO users ...
を書いていません。
JPAの実装としてHibernateなどを利用すると、必要なSQLが生成されます。
じゃあJDBCを勉強する必要はない?
JPAやMyBatis、JdbcTemplateがあるなら、
JDBCを直接使わないなら、JDBCを知らなくてもいいのでは?
とも思いました。
でも、むしろ逆でした。
これらの仕組みの下では、一般的なJavaのRDBアクセスではJDBCが重要な役割を持っています。
大まかな構造を見ると、
となります。
もちろん、それぞれのフレームワーク内部の構造には違いがあります。
ただ、
JavaアプリからRDBへアクセスする基礎としてJDBCが存在する
と理解しておくと、上のライブラリやフレームワークを使ったときにも処理を追いやすくなります。
今回のaddBatch()も、まさにそのJDBCが提供している機能の一つでした。
なぜバッチ処理にすると効率が良くなる?
OracleのJDBCドキュメントでは、複数のINSERT、UPDATE、DELETEをバッチ化することで、DBへのラウンドトリップを減らし、パフォーマンスを改善できると説明されています。
1件ずつ処理する場合、
Java → DB
Java → DB
Java → DB
Java → DB
と繰り返します。
一方、バッチを利用すると、
複数件をBatchへ追加
↓
executeBatch()
↓
DB
という形で処理できます。
大量データを扱うときには、
SQLそのものだけではなく、JavaとDBの間を何回行き来しているのか
という視点も重要だと分かりました。
全件をaddBatch()すればいい?
例えば10万件登録するなら、
10万件全部addBatch()して、最後にexecuteBatch()すればいいのでは?
とも思いました。
しかし、バッチサイズを無制限に大きくすればいいわけではありません。
大量のデータを一度に扱えば、メモリやDB、ドライバなどへの負荷も考える必要があります。
そのため、一定件数で区切る方法があります。
例えば1000件ずつなら、
for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
ps.setInt(1, user.getId());
ps.setString(2, user.getName());
ps.addBatch();
if ((i + 1) % 1000 == 0) {
ps.executeBatch();
}
}
if (users.size() % 1000 != 0) {
ps.executeBatch();
}
2500件なら、
となります。
ただし、1000件という数字が常に正解というわけではありません。
データサイズ、DB、JDBC Driver、ネットワークなどによって適切な値は変わります。
バルクインサート = JDBC Batchではない
ここも今回調べて理解が変わったところです。
最初は、
バルクインサート
=
addBatch() + executeBatch()
だと思っていました。
しかし、厳密には少し違います。
addBatch()とexecuteBatch()はJDBCが提供するバッチ更新の仕組みです。
大量データを効率よくINSERTする方法には、それ以外にもあります。
例えばPostgreSQLなら、
INSERT INTO users (id, name)
VALUES
(1, 'Alice'),
(2, 'Bob'),
(3, 'Carol');
という複数行INSERTがあります。
さらに大量データを読み込むためのCOPYもあります。
つまり、
バルクインサートは目的・考え方で、JDBC Batchはそれを実現する方法の一つ
くらいに整理すると分かりやすそうです。
PostgreSQL JDBC Driverにも最適化がある
PostgreSQL JDBC Driverには、
reWriteBatchedInserts=true
という設定があります。
これを有効にすると、互換性のあるバッチINSERTについて、複数VALUESを使ったINSERTへ書き換えることで性能を改善できる場合があります。
つまり今回調べた範囲だけでも、
というように、JavaコードだけでなくJDBC Driver側にも性能改善の仕組みが存在します。
今回の学び
最初は、
バルクインサートって何?
という疑問から始まりました。
そこから調べていくと、
と、かなり話がつながりました。
特に今回大きかったのは、
JDBCは単なるINSERTの書き方ではなく、JavaからRDBへアクセスするための基礎となるAPI
だと分かったことです。
そして、
JdbcTemplate
MyBatis
JPA / Hibernate
などを使っていても、その下にあるDBアクセスの仕組みを知っていると、処理を追いやすくなります。
バルクインサートについても、
「addBatch()を使えば速い」
だけで終わらせず、
なぜ速くなるのか
JavaとDBの間では何が起きているのか
どのレイヤーの機能なのか
まで考えると、かなり理解しやすくなりました。
参考資料
- Oracle Java Tutorials - Using Statement Objects for Batch Updates
- Oracle JDBC Developer's Guide - Update Batching
- Spring Framework Documentation - JDBC Batch Operations
- PostgreSQL JDBC Driver Documentation -
reWriteBatchedInserts - PostgreSQL Documentation - COPY