JDBCでResultSet.nextを使って逐次処理しているからといって安心しない。setFetchSizeを忘れずに!

JDBCを直接使っている人向けの話。いねえよとか言わない!おれがいる!

try (ResultSet result = statement.executeQuery();) {// このクエリは数百万件とかすごい量のデータを返すとする

とかしたとして、ブロックの中で

while (result.next()) {
    // ここで順次的に処理をする
}

とか書いて置いて、ほらちゃんとresult.nextで順次的に処理してるし、try with resourceでcloseもバッチリ!といっても、俺偉いとか思わない。executeQueryを実行した直後のヒープダンプがコレ。

ResultSetのインスタンスが60%以上取っている

まだresult.next()していない。そんな!ResultSetはデータそのものじゃなくて、接続の管理クラスみたいなやつだから、実際はnextして順次的にDBから値をもらうんだって教わった。なのにどうもメモリを食ってる。

ヒープダンプの中身を解析してみる。

rowsというVectorに全部のデータが格納されている

rowsというVectorがデカイ。

_人人人人人_
> Vector <
 ̄Y^Y^Y^Y ̄

WeakReferenceでもSoftReferenceでもないタダのVectorなので、メモリにガッツリ確保しております。そう、ResultSetはパフォーマンスのために、結果のうち数行をキャッシュするのだ!

そのキャッシュのサイズを制御するのがこのメソッド

https://docs.oracle.com/javase/6/docs/api/java/sql/Statement.html#setFetchSize(int)

setFetchSize

void setFetchSize(int rows)
                  throws SQLException
Gives the JDBC driver a hint as to the number of rows that should be fetched from the database when more rows are needed for ResultSet objects genrated by this Statement. If the value specified is zero, then the hint is ignored. The default value is zero.

なるほど。フェッチする行サイズを(あくまでヒントとして)指定できるらしい。デフォルトでは0、すなわち無指定状態のようだ。

じゃあ、ヒント無しの場合はどうなるのだろう。

http://grepcode.com/file/repo1.maven.org/maven2/postgresql/postgresql/8.4-702.jdbc3/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java#AbstractJdbc2ResultSet.next%28%29

row_offset += rows.size(); // We are discarding some data.
//
// 省略
//
int fetchRows = fetchSize;
if (maxRows != 0)
{
    if (fetchRows == 0 || row_offset + fetchRows > maxRows) // Fetch would exceed maxRows, limit it.
        fetchRows = maxRows - row_offset;
}

rowsが件のVectorであるので、つまり全部のデータをfetchRowsしようと試みてるってこと??

なので、大きめのクエリを流そうとしている場合は、setFetchSizeはほぼ確実に呼び出すべき。ただ、大きめにするとメモリを食うし、小さめだとパフォーマンスが不利になるのでその辺りは統計を取って最適値を模索するしかないかと。

あと、あくまで行のキャッシュであるので、一行にtext型を格納したドデカイカラムがあったりするとやばそうです。


コメントを残す

メールアドレスが公開されることはありません。