Hibernateのバッチ更新効果を実測してみる

Hibernateには内部的にjava.sql.Statement.addBatch()およびexecuteBatch()を使ってバッチ更新でSQLを発行するオプションhibernate.jdbc.batch_sizeがある。
デフォルトは無効となっているが、1トランザクションで大量のエンティティを登録する処理において、どれくらい効果的か実測してみる。

テスト環境

WildFlyMac上で起動、DBはVirtualBox上のCentOSで起動しているので、取得できるデータはあくまで参考値レベル。綺麗な検証環境ではない。

  • Mac OS X (Core i5 1.7GHz)
  • OracleJDK 1.7.0_60
  • PostgreSQL9.3
  • WildFly8.1.0 final (Hibernate4.3.5バンドル)

テスト内容

1トランザクションでのエンティティ登録数を10、100、500、1000、3000、5000、10000エンティティと数を増やしていき、batch_size=30を設定した場合としない場合の処理時間を比較する。

テストコード

一括エンティティ登録するEJB。大量エンティティ登録時にHibernate一次キャッシュによるOutOfMemoryErrorとならないように、バッチサイズに合わせてflush&clearでDBにSQLを発行させてキャッシュをクリアした。

@Stateless
public class CustomerService {
    
    @PersistenceContext(unitName = "PostgresPU")
    private EntityManager em;
    
    public void bulkimport(int cnt) {
        for (int i = 0; i < cnt; i++) {
            Customer c = new Customer("test user" + i, "tokyo");
            em.persist(c);
            if (i % 30 == 0) {
                em.flush();
                em.clear();
            } 
        }
    }
}

シンプルにサーブレットからEJBを呼び出して時間を計測。

@WebServlet("/BulkImport")
public class BulkImportServlet extends HttpServlet {

    @Inject
    private CustomerService customerService;
    
    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        long start = System.currentTimeMillis();
        customerService.bulkimport(10);
        long finish = System.currentTimeMillis();
        
        long time = finish - start;
        System.out.println("time : " + time);
        
        response.setContentType("text/plain;charset=UTF-8");
        try (PrintWriter out = response.getWriter()) {
            out.println("import done.");
        }
    }
    // 省略 ...

persistence.xmlは以下のようにシンプルな設定。batch_sizeなしのテストをするときは、hibernate.jdbc.batch_sizeをコメントアウトした。

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
             http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
    
  <persistence-unit name="PostgresPU">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
      <jta-data-source>java:jboss/jdbc/PostgresDS</jta-data-source>
        <properties>
          <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQL9Dialect" />
          <property name="hibernate.hbm2ddl.auto" value="create-drop" />
          <property name="hibernate.jdbc.batch_size" value="30" />
        </properties>
  </persistence-unit>
</persistence>

測定結果

あくまで1つの参考値に過ぎないが、batch_size=30の設定でエンティティの一括登録が早くなることがわかる。

(処理時間の単位はミリ秒)

一括登録エンティティ数 batch_size=30 batch_sizeなし
10 321 297
100 417 504
500 1432 1878
1000 2023 2888
3000 4262 5331
5000 8999 12048
10000 13196 30902

f:id:n_agetsuma:20140702211948p:plain

まとめ

一度に大量のエンティティを追加・更新することが事前にわかっている場合は、hibernate.jdbc.batch_sizeの設定は効果がありそう。エンティティの操作数が数十〜多くても1000エンティティくらいのWebアプリケーションではそれほど意識しなくても良いと思う。

メモ

addBatch()とexecuteBatch()の仕組みを知ろうとpostgresql-jdbc-9.3-1101のソースを眺めていて気がついたが、addBatch()はスレッドセーフではないので、素でJDBCを使って1つのStatementオブジェクトにマルチスレッドでaddBatch()すると、意図しない動作になりそう。

詳細はorg.postgresql.jdbc2.AbstractJdbc2Statement.javaのaddBatch(String p_sql)を参照。まだaddBatch()が一度もされていないかのnullチェックが同期化されていないため、タイミングによっては二重初期化されて登録済みのSQLが消えそうに見える。