Hibernateのバッチ更新効果を実測してみる
Hibernateには内部的にjava.sql.Statement.addBatch()およびexecuteBatch()を使ってバッチ更新でSQLを発行するオプションhibernate.jdbc.batch_size
がある。
デフォルトは無効となっているが、1トランザクションで大量のエンティティを登録する処理において、どれくらい効果的か実測してみる。
テスト環境
WildFlyはMac上で起動、DBはVirtualBox上のCentOSで起動しているので、取得できるデータはあくまで参考値レベル。綺麗な検証環境ではない。
テスト内容
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(); } } } }
@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 |
まとめ
一度に大量のエンティティを追加・更新することが事前にわかっている場合は、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が消えそうに見える。