Skip to content

JOINは遅い?

MySQLのJOINが遅いというのはよく言われるが、複数回SQLを発行するのとJOINするのがどちらがましなのか疑問だったので、実際どのくらい遅いのか試してみた。

Rails3.2のプロジェクトを作成し、適当なモデルを作ってconsoleでSQLの実行時間を見る。
サンプルコードはgithubに置いた
モデルはUser -< Item -< Extraで、Userは100、Itemは10ずつで1000、Extraは10ずつで合計10000のレコードを作成することにする。
seed_fuのフィクスチャを使い、rake db:seed_fuで一気にレコードが作成されるようにした。

この状態(commit:830f6dae26189d4ea83b8753471a166712d50568)で全てのエントリを取得させてみた。

User全件(100件)

> User.all.count
# SELECT `users`.* FROM `users`
=> 1.0ms

Item全件(100*10件)
1.JOINした場合

> User.joins(:items).all.count
# SELECT `users`.* FROM `users` INNER JOIN `items` ON `items`.`user_id` = `users`.`id`
=> 3.2ms

2.JOINせず複数回SQLした場合

> Item.where(:user_id => User.where(:id => {:not => nil}).pluck(:id)).all.count
# SELECT id FROM `users` WHERE (`users`.`id` IS NOT NULL)
# SELECT `items`.* FROM `items` WHERE `items`.`user_id` IN (1..100)
=> 0.6ms + 4.5ms = 5.1ms

Extra全件(100*10*10件)
1.JOINした場合

> User.joins(:items => :extras).all.count
# SELECT `users`.* FROM `users` INNER JOIN `items` ON `items`.`user_id` = `users`.`id` INNER JOIN `extras` ON `extras`.`item_id` = `items`.`id`
=> 35.4ms

2.JOINせず複数回SQLした場合

> Extra.where(:item_id => (Item.where(:user_id => User.where(:id => {:not => nil}).pluck(:id)).pluck(:id))).all.count
# SELECT id FROM `users` WHERE (`users`.`id` IS NOT NULL)
# SELECT id FROM `items` WHERE `items`.`user_id` IN (1..100)
# SELECT `extras`.* FROM `extras` WHERE `extras`.`item_id` IN (1..1000)
=> 0.6ms + 1.8ms + 30.9ms = 33.3ms

このケースだとあまり差が出ないので、Extraの件数を10倍にしてみた。(commit:96ee43e9319a363666033ac725950283bb80fab8

Extra全件(100*10*100件)
1.JOINした場合

> User.joins(:items => :extras).all.count
# SELECT `users`.* FROM `users` INNER JOIN `items` ON `items`.`user_id` = `users`.`id` INNER JOIN `extras` ON `extras`.`item_id` = `items`.`id`
=> 358.9ms

2.JOINせず複数回SQLした場合

> Extra.where(:item_id => (Item.where(:user_id => User.where(:id => {:not => nil}).pluck(:id)).pluck(:id))).all.count
# SELECT id FROM `users` WHERE (`users`.`id` IS NOT NULL)
# SELECT id FROM `items` WHERE `items`.`user_id` IN (1..100)
# SELECT `extras`.* FROM `extras` WHERE `extras`.`item_id` IN (1..1000)
=> 0.7ms + 1.9ms + 303.0ms = 305.6ms

件数が増えるほど差が広がっている。件数が多い時はJOINしない方が良さそうではある。
まぁ、300msもかかるようだと他の高速化手段を取るべきだろうけど。

ついでにindexを追加してみた。(commit:bac53fb2569b5a681a6bacad3e8c13535e4aa141

Item全件(100*10件)
1.JOINした場合

> User.joins(:items).all.count
# SELECT `users`.* FROM `users` INNER JOIN `items` ON `items`.`user_id` = `users`.`id`
=> 4.0ms

2.JOINせず複数回SQLした場合

> Item.where(:user_id => User.where(:id => {:not => nil}).pluck(:id)).all.count
# SELECT id FROM `users` WHERE (`users`.`id` IS NOT NULL)
# SELECT `items`.* FROM `items` WHERE `items`.`user_id` IN (1..100)
=> 0.7ms + 4.7ms = 5.4ms

Extra全件(100*10*100件)
1.JOINした場合

> User.joins(:items => :extras).all.count
# SELECT `users`.* FROM `users` INNER JOIN `items` ON `items`.`user_id` = `users`.`id` INNER JOIN `extras` ON `extras`.`item_id` = `items`.`id`
=> 212.1ms

2.JOINせず複数回SQLした場合

> Extra.where(:item_id => (Item.where(:user_id => User.where(:id => {:not => nil}).pluck(:id)).pluck(:id))).all.count
# SELECT id FROM `users` WHERE (`users`.`id` IS NOT NULL)
# SELECT id FROM `items` WHERE `items`.`user_id` IN (1..100)
# SELECT `extras`.* FROM `extras` WHERE `extras`.`item_id` IN (1..1000)
=> 0.6ms + 3.3ms + 556.9ms = 560.8ms

む、逆転した。

以上はローカル環境で適当にやっただけなので、そのうちAWSのインスタンスでも立ててちゃんと調べよう。

追記:分かりやすいようにSQL入れました。

続々assets:precompile問題

先日のエントリの最後に書いたprecompileが失敗する問題の続き。
Rails3.2.3でも解決していなかったので本腰入れて調べてみた。

続きを読む ‘続々assets:precompile問題’ »

Emacs23.4

久々にEmacsをビルドした。もちろんCocoa Emacs。
手順はほぼこの通りでOK。

久しぶりにinline patchを当ててみたが、やはりauto-saveとぶつかった時に落ちる。
(auto-save-buffersは使っていないんだけど)
これを回避するパッチがmacemacs-jpで流れていたので当てる。

一応、patch -p0で当てられるパッチを作ったので置いておく。

--- a/src/nsterm.m 2012-03-20 12:48:57.000000000 +0900
+++ b/src/nsterm.m 2012-03-20 13:22:16.000000000 +0900
@@ -4708,9 +4708,7 @@
   if (NS_KEYLOG)
     NSLog (@"firstRectForCharRange request");
     
- if (NILP (Feval (Fcons (intern ("ns-in-echo-area"), Qnil))))
- win = XWINDOW (FRAME_SELECTED_WINDOW (emacsframe));
- else if (WINDOWP (echo_area_window))
+ if (WINDOWP (echo_area_window))
     win = XWINDOW (echo_area_window);
   else
     win = XWINDOW (FRAME_SELECTED_WINDOW (emacsframe));

一通りパッチを揃えたら以下の手順でビルド。

patch -p1 < ../emacs-23-lion-fullscreen-test.patch
patch -p0 < ../inline_patch-svn/emacs-inline.patch
patch -p1 < ../fix-shiftmodifier-patch-for-emacs-inline-patch.diff
patch -p0 < ../fix-auto-save-crash.patch
./configure --with-ns --without-x
make bootstrap
make install

Mac Mini(Core2Duo 2.4GHz/8G/SSD)でビルド所要時間13分くらい。
思ったよりも速くなってないけど、まぁCore2Duoだしこんなもんか。i7のMiniかiMac出たら欲しいな。

varnish&nginx

varnishとnginxを使っているサーバをマイグレーションすることになり、今まで外部のLBを通していたのを直接処理することになった。
そこで問題になるのはSSLの扱い。varnishはSSLを扱えないため(作者はopensslのコードはクソだ、と一刀両断している)、httpsのフロントエンドとしては使えない。
そこで順序を入れ替え、nginxをフロントエンドに、varnishを挟んでunicornに投げるようにした。

続きを読む ‘varnish&nginx’ »

真空断熱マグ

真空断熱マグがへこんだので買い換えた。
何となく毎回別々のメーカーのを買っているので、ここらでちょっとレビューしてみる。
保温性能とか重量とかはだいたい一緒なので、その他を中心に。

サーモス「ケータイマグ」

少し太めで持ちにくいが、その分高さは低い。
塗装がはげやすく、すぐボロボロになる。リペイントとか楽しめる人にはいいかも。
蓋のロックはあけやすく、ゆっくり開くのも良い。
飲み口のパーツが掃除しにくく汚れる。

TIGER「サハラマグ」

蓋のロックが固くて開けにくい。勢いよく開く上に、角度が足りず鼻にぶつかる。(サイズや人によるかもしれない)
飲み口は一体化されていて掃除しやすい。
ボディがコーティングされているので塗装がはげない。

象印「ステンレスマグ」

容量が少しだけ少ないが、その分細身で持ちやすい。
底面がわずかに膨らんでいて安定する。
蓋のロックが独特で、開けにくい人もいるかも知れない。勢いよく開くが角度がつけられていて飲みやすい。
飲み口は一体化されていて掃除しやすく、簡易な空気弁がついている。
ボディはコーティングされているようだ。塗装は今のところはげてないが、時間が経ってないのでまだ不明。
なお、たまたまかも知れないけど買ったばかりは妙な匂いがあってなかなか取れなかった。

capistrano/multistageとdeploy_to

前回のエントリでnamespaceに起因すると勘違いした不具合。
capistranoのmultistageでdeploy_toにstageごとに異なるパスを指定すると変になるという話。

multistageを使っている時に、config/deploy/staging.rbに以下のように書いたとする。

set :rails_env, 'staging'
set :branch, 'trunk'
set :deploy_to, "/var/www/#{application}-#{rails_env}"
role :web, "staging-server"
role :app, "staging-server"
role :db, "staging-server", :primary => true

view raw staging.rb This Gist brought to you by GitHub.

が、これは期待した動作をしない。deploy:symlinkの時にオーバーライドしたはずのdeploy_toが復活する。もしdeploy.rbでdeploy_toを指定していなければ、デフォルトの/u/apps/#{application}が使われる。

これを回避するには、staging.rbに以下を追加する必要がある。

set(:deploy_to) { "/var/www/#{application}-#{rails_env}" }
set(:releases_path) { File.join(deploy_to, version_dir) }
set(:shared_path) { File.join(deploy_to, shared_dir) }
set(:current_path) { File.join(deploy_to, current_dir) }
set(:release_path) { File.join(releases_path, release_name) }

# unicornを使っているなら以下も
set(:unicorn_config) { "#{current_path}/config/unicorn.rb" }
set(:unicorn_pid) { "#{current_path}/tmp/pids/unicorn.pid" }

view raw staging.rb This Gist brought to you by GitHub.

これならdeploy_toが呼び出された時点で評価される。

しかし、全てのrails_envについて(staging.rb,production.rbなど)同じ内容を書かないといけないのでよろしくない。
ぱっと見だと難しそうなので、ゆっくりソースを追って考えてみます。

続assets:precompile問題

先日のエントリは色々間違ってました。

まず、Rails3.2でDBに接続しに行く問題はapplication.rbにconfig.assets.initialize_on_precompile = falseを追加することで回避できるみたい。RailsGuidesくらいちゃんと読めって話ですよね。このへんは先日の勉強会で教えて貰いました。


あとdeploy.rbでnamespaceを定義して更新時だけprecompileするコードだけど、multistageを使っているとパスがデフォルトになってしまったりでうまく動作しない。(multistageしてない時には正常動作するはず)
こちらはちょっと考え直す必要がありそう。うまく行ったらまた紹介します。

上記、別の問題とごっちゃになってました。そのままのコードで問題なしです。

assets:precompile問題

Rails3.1+capistranoの続き的なもの。
しばらくローカルでassets:precompileする運用をやっていたが、色々問題があった。具体的には以下のような感じ。

  • デザインの細かな修正ごとにprecompileするので、コミットがassets:precompileだらけになる。
  • precompileした状態でdevelopment環境をロードすると、coffeescriptで書いた関数が2回実行されたりおかしなことになる。
  • Rails3.2だとprecompile時にデフォルトでproductionデータベースに接続しようとするので、そのままのdatabase.ymlのままローカルで実行するとエラーになる。(これは一応RAILS_ENV=development rake assets:precompileで解決可能)

続きを読む ‘assets:precompile問題’ »

ドラッグ&ドロップでアップロード

Rails3.2でドラッグ&ドロップするとファイルをアップロードできるようにしてみた。

大体のところは

http://kray.jp/blog/rails3-fileupload/

を参考にさせて頂きました。以下、いくつかハマった所だけ。

続きを読む ‘ドラッグ&ドロップでアップロード’ »

AASMでInvalidTransitionが出る件

AASMをアップデートしたら、テストで例外が出るようになった。
正しくない状態遷移を行うと例外を返すようになったらしい。
AASMの初期化オプションに:whiny_transitions => falseを与えてやればいい。
以下は例。

aasm :column => :aasm_status, :whiny_transitions => false do
  state :prepared, :initial => true
  state :queued
 state :accepted
...
  event :accept do
    transitions :to => :accepted, :from => [:queued]
  end
...
end

この例だと、whiny_transitionsがtrueならpreparedから直接accept!とかすると例外を投げる。
以前は単に遷移せずpreparedのままだった。

まぁ、例外を投げるのが正しい動作だとは思う。作者もそう言ってるみたい
あくまで既存のコードをいじりたくない場合ってことで。