2014年02月18日

DBD::mysql において、例えばテーブルの中に大量のレコードが入っているテーブルにクエリを投げる場合は execute で凄いブロックされた経験は誰にでも有るはず。

my $sth = $dbh->prepare('SELECT * FROM okkiinari');
$sth->execute; # ここで okkiinari テーブルのデータを全部読み込んでる

だからみんなは LIMIT OFFSET とか頑張ったりするんですが、これを回避するための mysql_use_result っていうオプションがある。

connect する時に

my $dbh= DBI->connect('DBI:mysql:test;mysql_use_result=1', 'root', '');
と指定したり、途中で
$dbh->{mysql_use_result}=1; 
(これは prepare 前にやる)したり
$sth->{mysql_use_result} = 1
したり
$sth->prepare($sql, { mysql_use_result => 1 })
したときに有効になる。

このオプションを利用すると、通常は mysql_store_result を利用して mysqld からの結果を受け取っていたものを mysql_use_resultするので、 okkiinari テーブルの全データを読み込めるだけのバッファメモリを確保しなくても省メモリで全データを舐める事が可能なのです。

mysql_use_result を使うと $sth->rows で結果行を取ろうとしても0が帰ってきたり、全レコード取得する or $sth->finish するまで、該当レコードが READ LOCK かかるみたいな雰囲気でしたが、 fetch row を1秒おきにするスクリプトを走らせながら裏でレコード更新してもロックされない(当然 fetch 中のスクリプトの内容はクエリ発行前の値が出る)とかなのでよくわかりませんでした。

ちなみに DBD::mysql は async API を使うと mysql_use_result の併用が出来ないので悲しい。
mysql_db_async_result の中で rows を取ってきて、結果があったかどうかで分岐してるんだけど、ざっとコード見た感じ使えない理由がないからよくわからない。

async API を使えても使えなくても mysql_fetch_row するとこの前で select して監視すると、 mysqld のクエリ処理が終わった後のデータ転送を似非非同期的に取り扱う事は可能ではある。
その場合は烏賊みたいなめんどっちい感じ(ちゃんと使うにはもっとだるいことになる)をすると良い。

$sth-->execute;
while (1) {
while (1) {
my $ret = IO::Select-->new($dbh-->mysql_fd)-->can_read(0);
last if $ret;
...; # *1 read 出来るまでの間にやっときたいこと
}
my $row = $sth-->fetchrow_hashref;
last unless $row;
...
}

なんで似非非同期的かというと、mysql_fetch_row の中で read_one_row するために read_one_row 経由で cli_safe_read する時に全部のパケットを読み込んでるので(参考)パケットのキリが良い時は延々と cli_safe_read の中でブロックするし MAX_PACKET_LENGTH がでっかかったら、そもそもずっと読んでるし運が良くない限り *1 とかに処理はいらないっていうオチです。そもそも普通の web アプリに於いては1個のパケットの中で結果が戻ってくるケースばっかじゃないでしょうか。

あと mysql_use_result して全件取ってきてもクライアントには優しいですが、サーバ側でどんなことになるかはよく知らないのできっと偉い人が解説してくれて、ブクマコメントに url 入れてくれるとおもいます。

Posted by Yappo at 2014年02月18日 18:59 | TrackBack | Perl
Comments
Post a comment









Remember personal info?






コメントを投稿する前に↓の場所にnospamと入力してください。