0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Javaでバルクインサートする方法 ― addBatch()とexecuteBatch()を理解する

0
Posted at

大量のデータを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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?