top / index / prev / next / target / source
日記形式でつづる いがぴょんコラム ウェブページです。
blancoDb と 生JDBC APIとの間で速度比較を行ってみました。
つい先日 β版として公開させていただいた blancoDbに早速バグが見つかりました。
このなかの「コンパイルエラーが発生」のくだりのところについて、原因は 低水準で共通化して利用しているライブラリ (blancoCommons)の名前変形ルーチンにバグが入っていたことでした。アンダーバーを含まない大文字だけで構成された列名があるばあいに、名前変形がうまく働いてくれないメソッドがありました。(さらに問題なことはそれら名前変換メソッドが2個実装されていたことです) 次回リリース (1.4.0系)に向けて 名前変形に関して大幅な整理をおこなったのですが、その過程でバグを作り込んでしまいました。加えて名前変形ルーチンの試験観点が足りないことを反省しました。試験観点を見直します。※なお、このバグは blancoDb 1.2.0には含まれません。
バグを報告していただき、どうもありがとうございました。また、お手をわずらわせてしまい、すみませんでした。
このバグを修正した版をアップロードしておきました。加えて MySQL 5.0仮対応をピンポイントで追加しました。(blancoDbプラグインにおいて JDBCドライバのクラス名がドロップダウンで選択可能になりました+α) ただし、依然として MySQLに対する包括的な試験は未実施です。
同じく、[replace-link:blancoDb: Enterprise Edition と 生JDBC APIとの間での性能の差異についてご指摘を頂いたので、私の方でも比較を行ってみました。※そもそも blancoDbは 生JDBC APIコーディングを単に肩代わりするためのツールであるはずなので、blancoDbと生JDBC APIとの間で性能差がでるのだとしたら ゆゆしき問題なので、すぐに確認しました (苦笑)
関連するリソース
プログラマとSEのあいだ http://d.hatena.ne.jp/taka_2/20051115
ターゲットは MySQL 5.0.xベースとしました。
mysql-5.0.15-win32.zip ベース http://www.mysql.com/
ひたすら Typical + Standard でインストールしました。
JDBCドライバ : MySQL Connector/J (公式JDBCドライバ) 3.1.11 http://www.mysql.com/products/connector/j/ ファイル名: mysql-connector-java-3.1.11.zip を解凍して得られる mysql-connector-java-3.1.11-bin.jar を利用
Sun JDK 5.0 + Windows XP SP2 + Pentium 4 2GHz + メモリ1GB
blancoDb Enterprise Edition 1.3.3 (Beta版)
実行結果の比較表 件数生JDBC APIblancoDbblancoDb (PreparedStatement破棄版)100件 1件あたり0.47ミリ秒 1件あたり0.31ミリ秒 1件あたり0.62ミリ秒 1件あたり0.47ミリ秒 1件あたり1.41ミリ秒 1件あたり1.25ミリ秒 1000件 1件あたり0.36ミリ秒 1件あたり0.343ミリ秒 1件あたり0.344ミリ秒 1件あたり0.328ミリ秒 1件あたり1.047ミリ秒 1件あたり0.813ミリ秒 10000件 1件あたり0.1984ミリ秒 1件あたり0.2062ミリ秒 1件あたり0.1969ミリ秒 1件あたり0.2ミリ秒 1件あたり0.7141ミリ秒 1件あたり0.6391ミリ秒
この実行結果に対する私なりの分析は 下記のような感じです。
生JDBC APIと blancoDbとの間には、件数が1000件などを越えた際には 実効性能に関しては差異がみられないように思われます。
一方で 件数が100件程度と少ない時には HotSpotが発動するまでのギャップがある (あるしきい値までインタプリタで動作するという Java実行環境の実装上の特性) ので、メソッドの積み上げ回数が少ない生JDBC APIを利用したケースのほうが いくぶん有利になったのではないかと思います。
わざとPreparedStatementを破棄して繰り返しを行うと (予想通り) JDBC APIに比べてずいぶんと低速になりました。
blancoDb は 良くも悪くも 生JDBC APIを用いたJavaソースコードをツールとして自動生成するだけのツールであるので、生JDBC APIより低速になることも、あるいは高速になることも無いと考えています。とはいえ、私が書いた検証用の Javaソースコードに私の無意識のうちにバイアスがかかっている可能性がありますね… (苦笑) 明日、いまいちど検証用コードの他人によるソースコードレビューを依頼してみます。
実行速度比較に用いた手順やソースコードを下記に示します。
前準備 作表DDL
CREATE
TABLE
customer (
ID INT NOT NULL
,NAME VARCHAR (16) NULL
,PRIMARY KEY (ID)
)
;
生JDBC API版
生JDBC API版を用いた速度計測用ソースコードはこちらです。 MyTestJdbc.java
/**
* 実測値<br>
*【100件】<br>
* 生JDBC版: 100件追加:トータル 47ミリ秒, 1件あたり0.47ミリ秒<br>
* 生JDBC版: 100件削除:トータル 31ミリ秒, 1件あたり0.31ミリ秒<br>
*
*【1000件】<br>
* 生JDBC版: 1000件追加:トータル 360ミリ秒, 1件あたり0.36ミリ秒<br>
* 生JDBC版: 1000件削除:トータル 343ミリ秒, 1件あたり0.343ミリ秒<br>
*
*【10000件】<br>
* 生JDBC版: 10000件追加:トータル 1984ミリ秒, 1件あたり0.1984ミリ秒<br>
* 生JDBC版: 10000件削除:トータル 2062ミリ秒, 1件あたり0.2062ミリ秒<br>
*/
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* PreparedStatementが有効に利用するケースのパターン
*/
public class MyTestJdbc {
/**
* 繰り返し回数
*/
private static final int COUNT = 100;
/**
* 自動コミットを行うかどうか
*/
private static final boolean AUTOCOMMIT = false;
public static void main(String[] args) {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test?useUnicode=true", "root",
"password");
if (AUTOCOMMIT == false) {
// 手動コミットに切り替えます。
conn.setAutoCommit(false);
}
if (true) {
// 表にINSERTを行います。
insertInto(conn);
}
if (true) {
// 表からDELETEを行います。
deleteFrom(conn);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
/**
* データをテーブルに挿入
*
* @param conn
* @throws SQLException
*/
private static final void insertInto(final Connection conn)
throws SQLException {
final long start = System.currentTimeMillis();
// ループの外側でPreparedStatementのインスタンスを生成します。
final PreparedStatement stmt = conn
.prepareStatement("INSERT\n INTO customer\n (ID, NAME)\nVALUES\n (?, ?)");
try {
for (int index = 0; index < COUNT; index++) {
final String name = "山田" + String.valueOf(index) + "朗";
// SQL入力パラメーターをセットします。
stmt.setInt(1, index);
stmt.setString(2, name);
if (stmt.executeUpdate() != 1) {
throw new SQLException("追加に失敗しました。");
}
}
if (AUTOCOMMIT == false) {
conn.commit();
}
} finally {
// 使い終わったら最後にcloseします。
stmt.close();
}
final long period = System.currentTimeMillis() - start;
System.out.println("生JDBC版: " + COUNT + "件追加:トータル " + period
+ "ミリ秒, 1件あたり" + ((double) period) / COUNT + "ミリ秒");
}
/**
* データをテーブルから削除
*
* @param conn
* @throws SQLException
*/
private static final void deleteFrom(final Connection conn)
throws SQLException {
final long start = System.currentTimeMillis();
// ループの外側でPreparedStatementのインスタンスを生成します。
final PreparedStatement stmt = conn
.prepareStatement("DELETE FROM customer\n WHERE ID = ?");
try {
for (int index = 0; index < COUNT; index++) {
// SQL入力パラメーターをセットします。
stmt.setInt(1, index);
if (stmt.executeUpdate() != 1) {
throw new SQLException("削除に失敗しました。");
}
}
if (AUTOCOMMIT == false) {
conn.commit();
}
} finally {
// 使い終わったら最後にcloseします。
stmt.close();
}
final long period = System.currentTimeMillis() - start;
System.out.println("生JDBC版: " + COUNT + "件削除:トータル " + period
+ "ミリ秒, 1件あたり" + ((double) period) / COUNT + "ミリ秒");
}
}
blancoDb版
blancoDbが生成したソースコードを用いた速度計測用ソースコードはこちらです。
/**
* 実測値<br>
*【100件】<br>
* Prepared版: 100件追加:トータル 62ミリ秒, 1件あたり0.62ミリ秒<br>
* Prepared版: 100件削除:トータル 47ミリ秒, 1件あたり0.47ミリ秒<br>
*
*【1000件】<br>
* Prepared版: 1000件追加:トータル 344ミリ秒, 1件あたり0.344ミリ秒<br>
* Prepared版: 1000件削除:トータル 328ミリ秒, 1件あたり0.328ミリ秒<br>
*
*【10000件】<br>
* Prepared版: 10000件追加:トータル 1969ミリ秒, 1件あたり0.1969ミリ秒<br>
* Prepared版: 10000件削除:トータル 2000ミリ秒, 1件あたり0.2ミリ秒<br>
*/
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import myapp.db.exception.IntegrityConstraintException;
import myapp.db.query.SimpleCustomerDeleteInvoker;
import myapp.db.query.SimpleCustomerInsertInvoker;
/**
* PreparedStatementが有効に利用するケースのパターン
*/
public class MyTestPrepare {
/**
* 繰り返し回数
*/
private static final int COUNT = 100;
/**
* 自動コミットを行うかどうか
*/
private static final boolean AUTOCOMMIT = false;
public static void main(String[] args) {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test?useUnicode=true", "root",
"password");
if (AUTOCOMMIT == false) {
// 手動コミットに切り替えます。
conn.setAutoCommit(false);
}
if (true) {
// 表にINSERTを行います。
insertInto(conn);
}
if (true) {
// 表からDELETEを行います。
deleteFrom(conn);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
/**
* データをテーブルに挿入
*
* @param conn
* @throws IntegrityConstraintException
* @throws SQLException
*/
private static final void insertInto(final Connection conn)
throws IntegrityConstraintException, SQLException {
final long start = System.currentTimeMillis();
// ループの外側でinvokerのインスタンスを生成します。
final SimpleCustomerInsertInvoker invoker = new SimpleCustomerInsertInvoker(
conn);
try {
for (int index = 0; index < COUNT; index++) {
final String name = "山田" + String.valueOf(index) + "朗";
// SQL入力パラメーターをセットします。
invoker.setInputParameter(index, name);
invoker.executeSingleUpdate();
}
if (AUTOCOMMIT == false) {
conn.commit();
}
} finally {
// 使い終わったら最後にcloseします。
invoker.close();
}
final long period = System.currentTimeMillis() - start;
System.out.println("Prepared版: " + COUNT + "件追加:トータル " + period
+ "ミリ秒, 1件あたり" + ((double) period) / COUNT + "ミリ秒");
}
/**
* データをテーブルから削除
*
* @param conn
* @throws IntegrityConstraintException
* @throws SQLException
*/
private static final void deleteFrom(final Connection conn)
throws IntegrityConstraintException, SQLException {
final long start = System.currentTimeMillis();
// ループの外側でinvokerのインスタンスを生成します。
final SimpleCustomerDeleteInvoker invoker = new SimpleCustomerDeleteInvoker(
conn);
try {
for (int index = 0; index < COUNT; index++) {
// SQL入力パラメーターをセットします。
invoker.setInputParameter(index);
invoker.executeSingleUpdate();
}
if (AUTOCOMMIT == false) {
conn.commit();
}
} finally {
// 使い終わったら最後にcloseします。
invoker.close();
}
final long period = System.currentTimeMillis() - start;
System.out.println("Prepared版: " + COUNT + "件削除:トータル " + period
+ "ミリ秒, 1件あたり" + ((double) period) / COUNT + "ミリ秒");
}
}
blancoDb版 (わざとPreparedStatementを破棄する版)
blancoDbが生成したソースコードを用いた速度計測用ソースコード (わざと性能劣化を発生させるようなPreparedStatementの破棄をおこなっています)
/**
* 実測値<br>
*【100件】<br>
* 非Prepared版: 100件追加:トータル 141ミリ秒, 1件あたり1.41ミリ秒<br>
* 非Prepared版: 100件削除:トータル 125ミリ秒, 1件あたり1.25ミリ秒<br>
*
*【1000件】<br>
* 非Prepared版: 1000件追加:トータル 1047ミリ秒, 1件あたり1.047ミリ秒<br>
* 非Prepared版: 1000件削除:トータル 813ミリ秒, 1件あたり0.813ミリ秒<br>
*
*【10000件】<br>
* 非Prepared版: 10000件追加:トータル 7141ミリ秒, 1件あたり0.7141ミリ秒<br>
* 非Prepared版: 10000件削除:トータル 6391ミリ秒, 1件あたり0.6391ミリ秒<br>
*/
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import myapp.db.exception.IntegrityConstraintException;
import myapp.db.query.SimpleCustomerDeleteInvoker;
import myapp.db.query.SimpleCustomerInsertInvoker;
/**
* PreparedStatementが有効に機能しないようにわざと設定したケースのパターン
*/
public class MyTestNonPrepare {
/**
* 繰り返し回数
*/
private static final int COUNT = 10000;
/**
* 自動コミットを行うかどうか
*/
private static final boolean AUTOCOMMIT = false;
public static void main(String[] args) {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test?useUnicode=true", "root",
"password");
if (AUTOCOMMIT == false) {
// 手動コミットに切り替えます。
conn.setAutoCommit(false);
}
if (true) {
// 表にINSERTを行います。
insertInto(conn);
}
if (true) {
// 表からDELETEを行います。
deleteFrom(conn);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
/**
* データをテーブルに挿入
*
* @param conn
* @throws IntegrityConstraintException
* @throws SQLException
*/
private static final void insertInto(final Connection conn)
throws IntegrityConstraintException, SQLException {
final long start = System.currentTimeMillis();
// ループの外側でinvokerのインスタンスを生成します。
final SimpleCustomerInsertInvoker invoker = new SimpleCustomerInsertInvoker(
conn);
try {
for (int index = 0; index < COUNT; index++) {
final String name = "山田" + String.valueOf(index) + "朗";
// SQL入力パラメーターをセットします。
invoker.setInputParameter(index, name);
invoker.executeSingleUpdate();
// PreparedStatementをわざと破棄して解放するためにclose()を呼び出し
invoker.close();
}
if (AUTOCOMMIT == false) {
conn.commit();
}
} finally {
// 使い終わったら最後にcloseします。
invoker.close();
}
final long period = System.currentTimeMillis() - start;
System.out.println("非Prepared版: " + COUNT + "件追加:トータル " + period
+ "ミリ秒, 1件あたり" + ((double) period) / COUNT + "ミリ秒");
}
/**
* データをテーブルから削除
*
* @param conn
* @throws IntegrityConstraintException
* @throws SQLException
*/
private static final void deleteFrom(final Connection conn)
throws IntegrityConstraintException, SQLException {
final long start = System.currentTimeMillis();
// ループの外側でinvokerのインスタンスを生成します。
final SimpleCustomerDeleteInvoker invoker = new SimpleCustomerDeleteInvoker(
conn);
try {
for (int index = 0; index < COUNT; index++) {
// SQL入力パラメーターをセットします。
invoker.setInputParameter(index);
invoker.executeSingleUpdate();
// PreparedStatementをわざと破棄して解放するためにclose()を呼び出し
invoker.close();
}
if (AUTOCOMMIT == false) {
conn.commit();
}
} finally {
// 使い終わったら最後にcloseします。
invoker.close();
}
final long period = System.currentTimeMillis() - start;
System.out.println("非Prepared版: " + COUNT + "件削除:トータル " + period
+ "ミリ秒, 1件あたり" + ((double) period) / COUNT + "ミリ秒");
}
}