MariaDBのJDBCドライバはuseCursorFetchができない

MariaDB 5.5
MariaDB Java Client 1.1.5

今年の夏頃からMariaDBを使っている。
今のところクライアントは主にJavaプログラムである。

この数ヶ月間、MariaDBには驚かされてばかりだ。
遭遇したすべての状況を記事にすることは現状できていないが、小出しになってしまうにせよ、そのうち時間を見つけて記載するつもりでいる。

例えば、こんなことがあった。

・テーブル名などの識別子の大文字小文字を「区別する」
・しかも、WindowsOS Xでは識別子の大文字小文字を「区別しない」
・VARCHAR型の大文字小文字をデフォルトで「区別しない」
・DATETIME型のデフォルト値にNOW()を指定できない
・TIMESTAMP型の扱いがややこしい
・行ロックの挙動が普通じゃない

などなど。

上に挙げたすべてのことは、MariaDBというよりも、MySQLの仕様であり、クセである。私はMySQLにもMariaDBにも詳しいわけではないが、今までMariaDBを使う上で不思議に感じたことは、調べてみた結果、すべてMySQLと同じことだったのだ。

しかし、最近になって、JDBCドライバに関しては、両者にかなりの違いがあることを実感している。相違というよりも、これは、「MariaDBJDBCドライバは、MySQLJDBCドライバより劣っている」と言いたくなるほどだ。

何が起きたのかといえば、事の始まりは、私が作成したJavaプログラムがOutOfMemoryで落ちたことだった。

落ちた箇所は、Statement#executeQueryの呼び出し内である。

ここでOutOfMemoryするということは・・・

このとき既に嫌な予感がしていた。

仕様書を見ることによって、嫌な予感が現実であることをようやく認識した。



http://dev.mysql.com/doc/refman/5.5/en/connector-j-reference-implementation-notes.html
http://dev.mysql.com/doc/refman/5.1/ja/connector-j-reference-implementation-notes.html
(下のリンクは5.1だが日本語訳)

・ResultSet
デフォルトにより、ResultSets は完全に摘出され、メモリに保存されます。ほとんどの場合において、これは最も効果的な操作方法であり、MySQL の設計により、ネットワーク プロトコルはより簡単に実装できます。多大な行や大きな値を持つ ResultSets を扱っていて、要求されるメモリのために JVM に十分なスペースを割り振れない場合は、ドライバに結果を一行ごとにストリームし戻すよう指示することができます。

この機能を有効にするには、次の方法で Statement インスタンスを作成する必要があります。

stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY,
              java.sql.ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);

フェッチのサイズが Integer.MIN_VALUE の、前進専用、読み取り専用のコンビネーションは、行ごとに結果セットをストリームするようドライバに指示する信号として機能します。この後、このステートメントで作成された結果セットは行ごとに摘出されます。

(以下略)

なんということだ!

確かに、JDBCの仕様では、結果セットの内容がメモリに展開されるのか、サーバー側でカーソルが保持されるのかまでは規定していない。しかし、まさか、デフォルトですべての結果セットをメモリに展開するなんてことをしているとは思ってもみなかった。仕様書には、続けて、ストリーム結果セットを使うことによって解決できるようなことが書いてある。

結論から言うと、このやり方はダメだ。

まず、

stmt.setFetchSize(Integer.MIN_VALUE);

これはJDBCの規格に違反している。JDBCでは、setFetchSizeに負の値が渡された場合は、SQLExceptionをthrowするとされている。こういうことをしてしまうと、データベースの実装に依存したJavaプログラムになってしまう。

しかし、現実問題として、OutOfMemoryを回避しなければならない。
簡単に対応できるならば、それでいいか、とも思い、一応試してみることにした。

このストリーム結果セット機能には、注意点が書いてある。

このアプローチには注意点がいくつかあります。接続にクエリを発行する前に、結果セットのすべての行を読まなければならず、さもなければ例外が投入されます。

この注意の意味はつまり、StatementとResultSetの対をネストさせることができない、という意味である。1コネクション内で開けるストリームの数は1つであり、ストリームが閉じられていない状態で別のクエリを発行することはできない。

Statement stmt1 = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt1.setFetchSize(Integer.MIN_VALUE);
ResultSet rset1 = stmt1.executeQuery("select * from some_table");

while (rset1.next()) {
	String sql = "select * from other_table where id=1";
	Statement stmt2 = conn.createStatement();
	ResultSet rset2 = stmt2.executeQuery(sql);	// SQLException
}

それどころか、select for updateで複数行を取得して、while内で1行ずつupdateした後に、まとめてcommitする、なんてこともできなくなる。

これは大変困った問題だ。

例えば、あるプログラム内の複数のクラスが、1つのConnectionを共有して、階層的に呼び出されている処理のある1点において、

上位「今ストリームフェッチ中なんだけど、これから呼び出すメソッドは、同じコネクションでSQLを発行するのだろうか。」
下位「はて、このConnectionは、自分の上位でストリームフェッチをしているだろうか。」

なんてことを、気にするべきではないし、そんなものはオブジェクト指向ではない。ましてや、クラスごとにConnectionを分離して、各々に取得させる、なんてことは論外だ。そんなことをしたら、トランザクションを有効に使えなくなる上、コネクション数の見積もりすらできなくなるだろう。

よって、つまり、公式ドキュメントに書かれている、ストリーム結果セットなるものは、使い物にならない。

もっとまともな解決方法はないかと探したところ、useCursorFetchなるオプションが存在した。

http://dev.mysql.com/doc/refman/5.5/en/connector-j-reference-configuration-properties.html

この機能を使うには、JDBCの接続URLに、useCursorFetch=trueを含めれば良いようだ。そして、サーバーからクライアントの結果セットに1度に持ってくるデフォルト行数(defaultFetchSize)を指定できる。この値は、setFetchSizeに渡すべき、本来の1以上の値を指定することによって、実行時に変更することも可能だ。

jdbc:mysql://localhost:3306/dbname?useCursorFetch=true&defaultFetchSize=100

実際の挙動としては、

・DBサーバーは、結果セットを、可能ならばMEMORYテーブルに展開し、カーソルを保持する
・メモリが足りなければ、MyISAMテーブルに展開し、カーソルを保持する
・クライアントからの要求に応じて、1度に、FetchSizeで指定された分の行数を返送する
・クライアントは取得したFetchSize分の結果セットをメモリに保持する

となるそうだ。

これは先述の setFetchSize(Integer.MIN_VALUE) よりは、かなりマトモな解決策に思えた。



ようやく決着できると意気込んで試してみるものの、どうも、私の環境では、useCursorFetchが効いていないように見えた。JDBCのオプションを色々試した挙げ句、MariaDBJDBCドライバのソースを落としてきて解読してみた。結果、useCursorFetchなどというオプションは、MariaDBJDBCドライバでは、取得すらされていなかった。

そりゃそうだ。

useCursorFetchオプションは確かに、その存在がMySQLのドキュメントには記載されているが、MariaDBJDBCパラメータ一覧には記載されていない。

https://mariadb.com/kb/en/about-the-mariadb-java-client/

結果的に私が施した対処は、

・一度のクエリで取得する行数がOutOfMemoryにならないように、少しずつデータを取得するというプログラム的対処と、
・リクエスト処理時に大量レコードの検索が必要にならないように、バッチプログラムとの連携を改善するという設計的対処

であった。


一応、現状は、MariaDBサーバーに対して、MySQLJDBCドライバを使えるようだが、今後どうなることやら。パフォーマンス面でも、MariaDBJDBCドライバは、MySQLJDBCドライバよりも遅いようだ。その辺も含めて、MariaDBJDBCドライバには、改善を期待したい。