« システムコールの最適化 | メイン | キャッシュシステムの Thundering Herd 問題 »

2007年09月20日

MySQL の高速化プチBK

 鴨志田さんに教えていただいたのですが、MySQL のクエリは数値をクォートしない方が高速になるらしいです。たとえば以下の例では、160万件の整数から4の倍数を数えていますが、数値をクォートしないほうが約50%も高速になっています。

mysql> show create table numbers;
+---------+----------------------------------------------------------------------------------------+
| Table   | Create Table                                                                           |
+---------+----------------------------------------------------------------------------------------+
| numbers | CREATE TABLE `numbers` (
  `n` int(11) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1 | 
+---------+----------------------------------------------------------------------------------------+
1 row in set (0.05 sec)

mysql> select count(*) from numbers where n%'4'='0';
+----------+
| count(*) |
+----------+
|   400000 | 
+----------+
1 row in set (0.36 sec)

mysql> select count(*) from numbers where n%4=0;
+----------+
| count(*) |
+----------+
|   400000 | 
+----------+
1 row in set (0.24 sec)

 Perl の場合、DBI::execute を呼んでいると、プリペアードステートメントのパラメータは必ずクォートされてしまいます。集約処理等、重たいSQLを実行するにあたっては、DBI::bind_param で数値型であることを明示すべき、と言えるでしょう。

# 変更前 (DBI::execute を使用)
use DBI;
...
$sth->execute(@params)
    or die $dbh->errstr;

# 変更後 (DBI::bind_param を使用)
use DBI qw(:sql_types);
...
for (my $i = 0; $i < @params; $i++) {
    if ($params[$i] =~ /^-?[0-9]+$/) {
        $sth->bind_param($i + 1, $params[$i], SQL_INTEGER);
    } else {
        $sth->bind_param($i, $params[$i]);
    }
}

ところで、実数の場合はどうすればいいんでしょうね。

2007年21日追記: 2点追記します。

 まず、上の例についてですが、たとえばクエリのパラメータとして文字列 "0" を渡した場合にも、数値 0 への変換が行われます。ゼロは空文字列と等価ですので、文字列 "0" を検索したかった場合には不具合が発生することになります。言うまでもないことかもしれませんが、値をみてエスケープの有無を判断するのではなく、パラメータの文脈 (どのように使用したいのか、という点) からエスケープする/しないを判断するのが王道です。

 次に、実数のバインドについて。気になってコードを追っていたのですが、現時点で最新のドライバである DBD::mysql-4.005 には浮動小数点型として bind_param された変数の扱いにバグがあるようです。具体的には、指数表現が無視されます (parse_number 関数が e 以降を読み捨てているっぽい)。よって、現時点では、実数をバインドする際には、型を指定しないか (あるいは SQL_VARCHAR を指定する) すべきだと思います。以下が、確認用のコードです。

my $dbh = DBI->connect(...);

my $sth = $dbh->prepare('select ?') or die $dbh->errstr;
$sth->bind_param(1, 1e16, SQL_FLOAT) or die $dbh->errstr;
$sth->execute() or die $dbh->errstr;

print $sth->fetchrow_arrayref()->[0], "\n";  # should print 1e16, but shows "1"

投稿者 kazuho : 2007年09月20日 12:06 このエントリーを含むはてなブックマーク このエントリーを含むはてなブックマーク

トラックバック

このエントリーのトラックバックURL:
http://labs.cybozu.co.jp/cgi-bin/mt-admin/mt-tbp.cgi/1535

このリストは、次のエントリーを参照しています: MySQL の高速化プチBK:

» perl - $scalarの中身が数値か否かを判定する from 404 Blog Not Found
詳しくはCookbookの2.1を参照してもらうとして、これは以外とよく出てくる設問なので。 Perl Cookbook (Englis... [続きを読む]

トラックバック時刻: 2007年09月21日 00:51

» 数値項目に対するSQLインジェクション対策のまとめ from 徳丸浩の日記
数値項目に対するSQLインジェクション対策について検討しました。数値をクォートしてエスケープする方法は副作用が多く、SQL組み立て時に数値チェックを行う方... [続きを読む]

トラックバック時刻: 2007年09月24日 11:13

コメント

暗黙の型変換をしているか否かの差ですね。

投稿者 wasabi : 2008年02月22日 09:12