2009年06月30日

MyISAM/InnoDB で primary key/index/uniqu な複合カラムなインデックス貼って SELECT COUNT(*) FROM table WHERE column = x したベンチマーク

とってみた物の、どれも速度変わらんっぽいなぁ。計りかたわるいかなぁ。

結果は下記の通り
先頭の文字が l = InnoDB, h = MyISAM, 二番目の数字や、最後の文字の意味はベンチスクリプト見てくださいなり。

一応適すとでもこぴえp

[~/perl]$ perl ./sql-count-benchmark.pl
^[[C        Rate l4_i l3_i h2_n l3_n l2_i l4_n h3_n h4_n h1_i l1_n h3_i l1_i h4_i h1_n l2_n h2_i
l4_i 10638/s   --  -3%  -5%  -6%  -7% -10% -10% -11% -12% -13% -14% -14% -16% -17% -18% -18%
l3_i 10989/s   3%   --  -2%  -3%  -4%  -7%  -7%  -8%  -9% -10% -11% -11% -13% -14% -15% -15%
h2_n 11236/s   6%   2%   --  -1%  -2%  -4%  -4%  -6%  -7%  -8%  -9%  -9% -11% -12% -13% -13%
l3_n 11364/s   7%   3%   1%   --  -1%  -3%  -3%  -5%  -6%  -7%  -8%  -8% -10% -11% -12% -12%
l2_i 11494/s   8%   5%   2%   1%   --  -2%  -2%  -3%  -5%  -6%  -7%  -7%  -9% -10% -11% -11%
l4_n 11765/s  11%   7%   5%   4%   2%   --  -0%  -1%  -2%  -4%  -5%  -5%  -7%  -8%  -9%  -9%
h3_n 11765/s  11%   7%   5%   4%   2%   0%   --  -1%  -2%  -4%  -5%  -5%  -7%  -8%  -9%  -9%
h4_n 11905/s  12%   8%   6%   5%   4%   1%   1%   --  -1%  -2%  -4%  -4%  -6%  -7%  -8%  -8%
h1_i 12048/s  13%  10%   7%   6%   5%   2%   2%   1%   --  -1%  -2%  -2%  -5%  -6%  -7%  -7%
l1_n 12195/s  15%  11%   9%   7%   6%   4%   4%   2%   1%   --  -1%  -1%  -4%  -5%  -6%  -6%
h3_i 12346/s  16%  12%  10%   9%   7%   5%   5%   4%   2%   1%   --   0%  -2%  -4%  -5%  -5%
l1_i 12346/s  16%  12%  10%   9%   7%   5%   5%   4%   2%   1%   0%   --  -2%  -4%  -5%  -5%
h4_i 12658/s  19%  15%  13%  11%  10%   8%   8%   6%   5%   4%   3%   3%   --  -1%  -3%  -3%
h1_n 12821/s  21%  17%  14%  13%  12%   9%   9%   8%   6%   5%   4%   4%   1%   --  -1%  -1%
l2_n 12987/s  22%  18%  16%  14%  13%  10%  10%   9%   8%   6%   5%   5%   3%   1%   --  -0%
h2_i 12987/s  22%  18%  16%  14%  13%  10%  10%   9%   8%   6%   5%   5%   3%   1%   0%   --
[~/perl]$ perl ./sql-count-benchmark.pl
        Rate l3_i l4_n l3_n h4_n l4_i h4_i l2_n l2_i h2_n h2_i h1_i h3_i h3_n l1_i h1_n l1_n
l3_i 10753/s   --  -1%  -3%  -3%  -6%  -9% -10% -11% -11% -11% -11% -13% -14% -15% -16% -18%
l4_n 10870/s   1%   --  -2%  -2%  -5%  -8%  -9% -10% -10% -10% -10% -12% -13% -14% -15% -17%
l3_n 11111/s   3%   2%   --  -0%  -3%  -6%  -7%  -8%  -8%  -8%  -8% -10% -11% -12% -13% -16%
h4_n 11111/s   3%   2%   0%   --  -3%  -6%  -7%  -8%  -8%  -8%  -8% -10% -11% -12% -13% -16%
l4_i 11494/s   7%   6%   3%   3%   --  -2%  -3%  -5%  -5%  -5%  -5%  -7%  -8%  -9% -10% -13%
h4_i 11765/s   9%   8%   6%   6%   2%   --  -1%  -2%  -2%  -2%  -2%  -5%  -6%  -7%  -8% -11%
l2_n 11905/s  11%  10%   7%   7%   4%   1%   --  -1%  -1%  -1%  -1%  -4%  -5%  -6%  -7% -10%
l2_i 12048/s  12%  11%   8%   8%   5%   2%   1%   --   0%  -0%  -0%  -2%  -4%  -5%  -6%  -8%
h2_n 12048/s  12%  11%   8%   8%   5%   2%   1%   0%   --  -0%  -0%  -2%  -4%  -5%  -6%  -8%
h2_i 12048/s  12%  11%   8%   8%   5%   2%   1%   0%   0%   --   0%  -2%  -4%  -5%  -6%  -8%
h1_i 12048/s  12%  11%   8%   8%   5%   2%   1%   0%   0%   0%   --  -2%  -4%  -5%  -6%  -8%
h3_i 12346/s  15%  14%  11%  11%   7%   5%   4%   2%   2%   2%   2%   --  -1%  -2%  -4%  -6%
h3_n 12500/s  16%  15%  13%  13%   9%   6%   5%   4%   4%   4%   4%   1%   --  -1%  -2%  -5%
l1_i 12658/s  18%  16%  14%  14%  10%   8%   6%   5%   5%   5%   5%   3%   1%   --  -1%  -4%
h1_n 12821/s  19%  18%  15%  15%  12%   9%   8%   6%   6%   6%   6%   4%   3%   1%   --  -3%
l1_n 13158/s  22%  21%  18%  18%  14%  12%  11%   9%   9%   9%   9%   7%   5%   4%   3%   --
<

Server version: 5.1.33 Source distribution
use strict;
use warnings;
use DBI;

use Benchmark ':all';

my $dbh = DBI->connect('DBI:mysql:test');
sub setup {
    for (1..4) {
        $dbh->do("DROP TABLE IF EXISTS hatena$_");
        $dbh->do("DROP TABLE IF EXISTS labs$_");
    }

    $dbh->do(q{
CREATE TABLE hatena1 (
    id    int unsigned,
    name  char(10),
    primary key(id, name),
    index(name, id)
) TYPE=MyISAM
});
    $dbh->do(q{
CREATE TABLE hatena2 (
    id    char(10),
    name  char(10),
    primary key(id, name),
    index(name, id)
) TYPE=MyISAM
});
    $dbh->do(q{
CREATE TABLE hatena3 (
    id    char(10),
    name  char(10),
    unique(id, name),
    index(name, id)
) TYPE=MyISAM
});
    $dbh->do(q{
CREATE TABLE hatena4 (
    id    char(10),
    name  char(10),
    index(id, name),
    index(name, id)
) TYPE=MyISAM
});

    $dbh->do(q{
CREATE TABLE labs1 (
    id    int unsigned,
    name  char(10),
    primary key(id, name),
    index(name, id)
) TYPE=InnoDB
});
    $dbh->do(q{
CREATE TABLE labs2 (
    id    char(10),
    name  char(10),
    primary key(id, name),
    index(name, id)
) TYPE=InnoDB
});
    $dbh->do(q{
CREATE TABLE labs3 (
    id    char(10),
    name  char(10),
    unique(id, name),
    index(name, id)
) TYPE=InnoDB
});
    $dbh->do(q{
CREATE TABLE labs4 (
    id    char(10),
    name  char(10),
    index(id, name),
    index(name, id)
) TYPE=InnoDB
});
}
setup if $ARGV[0]||'' eq 'setup';

sub apply_insert {
    my @buf = @_;
    for my $num (1..4) {
        for my $name (qw/ hatena labs /) {
            $dbh->do(sprintf "INSERT INTO $name$num (id, name) VALUES%s;", join(', ', @buf));
        }
    }
}

if ($ARGV[0]||'' eq 'setup') {
    my @buf;
    for my $id (1..1000) {
        for my $name (1..1000) {
            push @buf, "($id, $name)";
            if (@buf == 1000) {
                apply_insert(@buf);
                @buf = ();
            }
        }
    }
    if (@buf) {
        apply_insert(@buf);
    }
}

my $coderefs = {};
my $countcache = {};
for my $name (qw/ hatena labs /) {
    for my $num (1..4) {
        my $table = "$name$num";
        for my $column (qw/ id name /) {
            my $testname = join '', substr($name, 0, 1), $num, '_', substr($column, 0, 1);
            $coderefs->{$testname} = sub {
                my $sth = $dbh->prepare("SELECT COUNT(*) FROM $table WHERE $column = ?");
                $countcache->{$testname}++;
                $sth->execute($countcache->{$testname} % 1000);
            };
        }
    }
}

cmpthese( 10000 => $coderefs);

という生魚的エントリ。

Posted by Yappo at 15:48 | Comments (0) | TrackBack

2009年06月27日

YAPC::Asia 2009

そういえば、今年から SHIBUYA PERL MONGERS から JPA 名義のイベントになるんだなーと去年のパンフみて思い出した。

Posted by Yappo at 18:03 | Comments (0) | TrackBack

2009年06月17日

Imager::ExifOrientation - Exifの情報を元にして画像を回転するよ

全国的にみんな真面目だな〜。勉強会の目的なんてないよ。楽しいからやっている。それで何が悪いのかな?の実況中継、その勉強会への 参加そのものについてちょっと考えなおした方がいいかもしれない。

Imager::ExifOrientationをCPANにうpりました。
Exif の Orientation というパラメータを元にして回転済みのImagerオブジェクトを返します。
一緒に Imager::Filter::ExifOrientation もバンドルしてるので、filterとしても利用出来ます。
Orientationは何かと言うと、カメラに縦方向センサーが入ってる機種で、画像に対してカメラの上方向はどちらかというような情報が入っている所です。
わかり易い説明は500で見れないのでgoogleのキャッシュを見てみてください。

使い方は簡単で、exif入りの画像のパスをrotateメソッドに渡すとexif情報に基づいて回転したimager objectが帰ってきます。

my $image = Imager::ExifOrientation->rotate(
      file => 'foo.jpg'
  );
もしくは、適当に読み込んだ画像データを渡す事も出来ます。
my $data = do {
      open my $fh, '<', 'foo.jpg';
      local $/;
      <$fh>;
  };
  my $image = Imager::ExifOrientation->rotate(
      data => $data
  );

カメラで撮ったオリジナルのjpegファイル自体を回転させるのもいいですが、Imagerのフィルターとしても使えるようにしました。

 use Imager;
  use Imager::ExifOrientation;

  my $img = Imager->new;
  $img->filter(
      type => 'exif_orientation',
      path => 'foo.jpg',
  );
こんな感じで、 foo.jpg に入ってるexif情報を元に$imgの画像を回転させます。
他にもImage::ExifToolで抜き出したHASHを元にしたり、Orientationの数値を直接指定する方法も使えます。Orientationを指定する形で使えばlibjpegとか入ってなくても使えます(libjpeg入ってないとテストが全部skipされてたのを0.02で直してshipitた)。詳しくはPODを見てください。

試しにfilterを使ってacotieを回転させてみましょう。
以下ソース

use strict;
use warnings;
use Imager;
use Imager::Filter::ExifOrientation;

for my $i (1..8) {
    my $img = Imager->new;
    $img->read( file => 'acotie.png' );
    $img->filter( type => 'exif_orientation', orientation => $i );
    $img->write( file => "おうっふ_$i.jpg" );
    `open おうっふ$i.jpg`;
}
acotie.png は
これです。
実行結果は以下の通り。

Orientation の 3 は、単純に180度回転なのですが、Imagerのrotateを使うと汚くなるのでclip( dir => 'hv' )して、左右上下反転しました。
rotateは計算して頑張って画像を回転させるというのと、flipは単純にピクセルを並び替えるという差が奇麗さの差になってると思います。rotateはデータをコピーして計算するし。
90度やら270度回転は、流石にrotate使うしか無いけど。

追記:
そもそも90度づつの回転の場合は $img->rotate( right => 90 * $x );使うべきなので、書き換えて0.03をshipiしました。 90度やら270度の回転した画像も奇麗になた!

Posted by Yappo at 11:18 | Comments (1) | TrackBack

2009年06月11日

ひろせさんふるはしさん優勝おめでとう!

Interop Tokyo 2009のクラウドコンにkumofsで出場します - (ひ)メモ

いんたろっぷで優勝すげーすげー

kumofsすげーすげー

Posted by Yappo at 16:23 | Comments (0) | TrackBack

2009年06月10日

Data::Model っていう ORM みたいの CPAN にあげたよ

あざーす。循環参照しすぎるとバターになる。。なんでそんなに人の目を気にするのだろうと、マジレス。

早速ですが Data::Model っていう O/Rマッパー 的な物を CPAN にあげました。
Data::Model
http://github.com/yappo/p5-Data-Model/tree/master
元来は MVC モデルで言う所の Model を一括でまかなえるつもりで実装していますが、ロジック処理は普通の Perl のクラスで書いちゃった方が潰しが聞くため、主にストレージを Perl のオブジェクトにマッピングする ORM 的な使い方が主流となっています。
そして、 Data::Model の多くの実装や設計などは Data::ObjectDriver を参考にして開発しました。
他にも後述してる ORM の実装を参考にしています。
あ、あとは tokuhirom 先生による日本語チュートリアルがあります。

現在の所の情報源は CPAN に上がってるドキュメントの他に http://d.hatena.ne.jp/tokuhirom/searchdiary?word=Data%3a%3aModelとかhttp://d.hatena.ne.jp/yappo/searchdiary?word=Data%3a%3aModelぐらいですね。
あと、Hatetterのコードも結構参考になると思います。

Perl の ORM といえば、 Class::DBI をはじめに DBIx::Class, Data::ObjectDriver, Fey::ORM, Rose::DB, Jifty::DBI, DBIx::MoCo など( DBIx::Skinny も github にあるよ )があり、再実装する必要が無いようにも思います。
しかしながら既存の物は、 Inflate/Deflate まわりが貧弱で ForuceUTF8 的な事をやるのもちょっと大変だったり不安定だったり、 cache させるのが面倒だったり、 Moose 使ってたり、 社内の最新リポジトリと CPAN のバージョンが乖離していたりと「こいつと一緒にやりたい!」的な物が見つかりませんでした。確かに DBIC なんかは良いものだとは思いますが、複雑な事が出来てしまうがためにgdgdしてしまう事もあったりしたのです。
Data::ObjectDriver がだいぶ希望に近かったのですが、ドキュメントや利用事例が少なくてユーザになるのに二の足を踏みました。

という現状の ORM に持っていた不満点もあったのですが、それ以外の要因としては RBDMS を kvs 的なインターフェィスで使いたいな。そもそも Web アプリケーションだったら RDBMS の R の要素って使わなくても、やってけてるんじゃないか? だったら kvs 的に使えるように下ほうがさっくり DB の処理書けるんじゃないかな? という観点で作り始めました。
そういったスタンスなので Data::Model::Driver::Memcached という memcached protocol を喋るストレージサーバをバックエンドストレージとして使えるようになってます。

あまり長文はアレなので軽く特徴を

column sugar

user テーブルの id というカラムと同じ役割のカラムを別のテーブルに持つ。それも沢山のテーブルで user id なんかを持ってたいと言うケースは多々あると思います。
そんな時は column sugar を使うと、カラムの詳細定義は一度だけ書いて、後は column sugar を呼ぶだけですみます。

# 定義する
column_sugar 'user.id'
    => int => {
        required => 1,
        unsigned => 1,
    };

# user テーブルでの定義
column 'user.id' => { # ここではカラム名が id になる
    auto_increment => 1, # auto_increment 属性だけ追加する
};

# bookmark テーブルでの定義
column 'user.id'; # ここではカラム名が user_id になる
user id だと当てはまらないですが、 複数のテーブルで定義するカラムで char がた見たいな仕様が代わり安いカラムだと、文字長の仕様変更が入っても一ヶ所だけ size を書き直せば良いので、楽でミスも減ります。

CREATE TABLE 構文を出力する

他の ORM でもよくあるですが、 Data::Model 使って定義したスキーマを CREATE TABLE の SQL に出力します。
そして column sugar が強力にいかせるのは、スキーマ定義に変更が入ったら as_sqls してしまって、そのまま RDBMS 側の DDL も一緒に変えると言う事です。
DB 側のスキーマ情報を自動的に読み込んで ORM のスキーマとしてやるのもありますが、それだとスキーマを二ヶ所で変えなきゃいけなくて面倒なので ORM 側のスキーマのみ変更すれば良いように考えています。
rails のようはマイグレーションも自動的にやりたい所だがまだ未実装です。DB と ORM 側の差分を自動的に反映したいな。

DBI だけでなく kvs にも保存できる

Tokyo Tyrant や groonga や kai などの memcached protocol を使える kvs 等を DBI の代わりに利用する事が出来ます。決定的な制限として primary key でしかデータが引けません。がこれは別の driver と組み合わせる事で対応可能にする方向性です。

透過的なキャッシュ

Data::ObjectDriver インスパイアですが、 cache driver の failback driver を設定した driver object を使う時は、 cache にデータが無ければ、自動的に fallback driver にリクエストする用になります。

cache は Data::Model の Row object その物を渡す感じですが、 DBIC とは違って必要最低限の情報しか入ってないので、あまり問題にならない予定です。

また failback driver には Driver::Memcached を使う事も出来ますが、いわゆる memcached protocol しゃべる kvs は、それ単体での高速性を売りにしているためやる意味が分からんすね。

index に対する検索を簡素化に

Data::Model の get method では index を特別に扱います。
例えば index post (user_id, post_at) の用な index を張っていたら

my $iterator = $model->get( tweet => { index => { post => [ 1, 1281729102 ] } } );
といった形でクエリを引けます。

Q4M 対応

Data::Model では標準で Q4M が扱えます。
以下の例は SELECT queue_wait('smtp', 'pop', 10); を発行して、 queue が帰ってきたらそれぞれのクロージャを呼び出します。
第一引数には dequeue された queue の row object が渡されますので、改めて query を発行せずに、 queue の処理が行えます。

my $retval = $model->queue_running(
    smtp => sub {
        my $row = shift;
        is($row->id, 'foo');
        is($row->data, 1);
    },
    pop => sub {
        my $row = shift;
    },
    timeout => 10,
);

column_aliase

カラムにエイリアスを張ります。
バイナリデータをデータベースに格納するカラムがあるとして、利用する時には文字列形式とバイナリ形式を使いたい場合に、カラムにエイリアスを張ると両方の形式で利用出来ます。

  columns qw( name nickname );
  alias_column name     => 'name_name';
  alias_column nickname => 'nickname_name'
      => {
          inflate => sub {
              my $value = shift;
              Name->new( name => $value );
# Name は name っていうメソッドを持ってるよ
          },
          deflate => sub {
              my $obj = shift;
              $obj->name;
          },
      };
こんな風にしておくと
$row->nickname;      # 普通に文字列が返る
$row->nickname_name; # Name オブジェクトが返る

$row->nickname('test');    # 文字列をセット
$row->nickname_name->name; # test が返る

$row->nickname_name(Name->new( name => 'おうっふ' ));    # オブジェクトをセット
$row->name; # おうっふ が返る
とエイリアス張る前と張った後のメソッドでも保持するデータを相互的に補完し合います。

MySQL の master - slave

当然対応しています。 Driver::DBI::MasterSlave です。
今どきのモダンなウェブアプリは slave の mysql は lvs 噛ましてると思うので、 slave は一個しか指定出来ません。このあたり Data::ObjectDriver 弄った事ある人なら、いかようにも read dbh 分散する仕組み作れると思います。

add_method, mixin

row object に メソッドを生やせる為の add_method って DSL がついてます。
同じ method をいっぱいの table に生やしたい時は mixin 機構があるので、サクサクメソッド生やせます。
DBIx::Class::ResultSet 的な所にメソッド生やしたい場合は、普通に use base 'Data::Model' してるクラスにメソッド生やせばおkです。

transaction

最近実装しました、 DBIx::Class::Storage::TxnScopeGuard インスパイアで以下のように書けます。

  sub foo {
      my $is_die = shift;
 
      my $model = Your::Model->new;
      my $txn = $model->txn_scope; # トランザクション開始
 
      # トランザクション中は $txn からしか DB の操作出来なくなりますよっと
      my $row = $txn->lookup( user => 1 );
      $row->name('transaction name');
      $txn->update( $row ); # update
      return if $is_die; # スコープ抜けて commit されてないのでロールバックされる
      if ($is_die) {
          $txn->rollback; # 明示的にロールバック
          return;
      }

      $txn->commit; # commit する
  }

  foo(1); # rollback されてる
  foo(0); # commit できる
eval {}; if ($@) {} みたいなトランザクションの実行だと、例外があってメソッドをそのままreturnして抜ける事が出来ないので、こっちのが簡潔で気持ちいいと思ってます。
もし die で抜ける事があったとしても DESTROY のタイミングで rollback するので、問題は起こりえない筈ですね。

一つのスキーマクラスに複数のテーブル定義ができる

ORM だと、よく1テーブル/1クラスファイルみたいな感じになっちゃいますが(普通にそうならなく出来るけど)、 Data::Model だと、1クラス=1データベース みたいな感じにする方向性なので、1つのクラスファイルに沢山テーブルの定義をやっちゃいます。

リレーション未実装

なんとリレーション周りを標準でサポートしてません!
add_method 使えば has_a くらいは楽に書けるよ。
よい実装手法があれば作りたい所。

Driver::Pacific

kazuho ware の新作 Pacificに対応予定。
リゾルバとのやり取りを Data::Model 側でやるかどうするかは考えてないけども、結構簡単にラッピング出来るイメージ。

予定つながりだと FriendFeedのアレとかも入れたいですね。

まとめ

まだ特徴はあるですが、面倒なのでまとめる。

作り始めてから半年以上経ってようやくリリースできました。
現在自分がバリバリのユーザですが、使ってみた感じだと徐々に良い感じにもなってるし、テストも充実させている、ドキュメントもそこそこ使い方がわかる程度には書いてあるので是非ともお試しください。

あ、あと重要な事ですが、何でも自分で作りたい病だから作った訳ではないです。

Posted by Yappo at 23:09 | Comments (1) | TrackBack