ActiveRecord::Base storeの速度検証

Rails 3.2.0から追加された機能

3.2.0リリースから1年近くたっていて、
今更な感じですが、最近使ってパフォーマンス的に痛い思いをしたので、
ちゃんと計測してみました。


機能/用途の説明は、こちらにおまかせ。
http://d.hatena.ne.jp/hichiriki/20120229


さっそく計測。(rails 3.2.11)


用意したテーブル


$ rails g model store3 # storeで3カラム (実際には textカラム1つ)
$ rails g model store10 # storeで10カラム (実際には textカラム1つ)
$ rails g model store30 # storeで30カラム (実際には textカラム1つ)

$ rails g model serialize3 # serializeで3カラム (実際には textカラム1つ)
$ rails g model serialize10 # serializeで10カラム (実際には textカラム1つ)
$ rails g model serialize30 # serializeで30カラム (実際には textカラム1つ)

$ rails g model normal3 # 実際に3カラム
$ rails g model normal10 # 実際に10カラム
$ rails g model normal30 # 実際に30カラム

各カラム数での、カラム名


3カラム: num1, str1, time1
10カラム: num1, num2, num3, num4, str1, str2, str3, str4, time1, time2
30カラム: num1-15, str1-10, time1-5

insert

require 'benchmark'

now = Time.now

three_columns = {num1: 1, str1: 'a', time1: now}
ten_columns = {num1: 1, num2: 2, num3: 3, num4: 4, str1: 'a', str2: 'b', str3: 'c', str4: 'd', time1: now, time2: now}
thirty_columns = {num1: 1, num2: 2, num3: 3, num4: 4, num5: 5, num6: 6, num7: 7, num8: 8, num9: 9,
  num10: 10, num11: 11, num12: 12, num13: 13, num14: 14, num15: 15,
  str1: 'a', str2: 'b', str3: 'c', str4: 'd', str5: 'e', str6: 'f', str7: 'g', str8: 'h', str9: 'i', str10: 'j',
  time1: now, time2: now, time3: now, time4: now, time5: now}

n = 1000
Benchmark.bm do |x|
  x.report('store3') { n.times{Store3.create!(three_columns)} }
  x.report('store10') { n.times{Store10.create!(ten_columns)} }
  x.report('store30') { n.times{Store30.create!(thirty_columns)} }

  x.report('serialize3') { n.times{Serialize3.create!(text: three_columns)} }
  x.report('serialize10') { n.times{Serialize10.create!(text: ten_columns)} }
  x.report('serialize30') { n.times{Serialize30.create!(text: thirty_columns)} }

  x.report('normal3') { n.times{Normal3.create!(three_columns)} }
  x.report('normal10') { n.times{Normal10.create!(ten_columns)} }
  x.report('normal30') { n.times{Normal30.create!(thirty_columns)} }
end


$ rails runner script/benchmark.rb
user system total real
store3 2.150000 0.160000 2.310000 ( 3.218724)
store10 2.600000 0.130000 2.730000 ( 3.445697)
store30 4.980000 0.170000 5.150000 ( 6.126592)
serialize3 1.770000 0.120000 1.890000 ( 2.484414)
serialize10 2.390000 0.130000 2.520000 ( 3.181018)
serialize30 4.060000 0.150000 4.210000 ( 5.224046)
normal3 1.330000 0.120000 1.450000 ( 1.945207)
normal10 1.800000 0.120000 1.920000 ( 2.531964)
normal30 3.240000 0.150000 3.390000 ( 4.348845)

たった1000回でもそれなりに差が出る。

select

require 'benchmark'

now = Time.now

three_columns = {num1: 1, str1: 'a', time1: now}
ten_columns = {num1: 1, num2: 2, num3: 3, num4: 4, str1: 'a', str2: 'b', str3: 'c', str4: 'd', time1: now, time2: now}
thirty_columns = {num1: 1, num2: 2, num3: 3, num4: 4, num5: 5, num6: 6, num7: 7, num8: 8, num9: 9,
  num10: 10, num11: 11, num12: 12, num13: 13, num14: 14, num15: 15,
  str1: 'a', str2: 'b', str3: 'c', str4: 'd', str5: 'e', str6: 'f', str7: 'g', str8: 'h', str9: 'i', str10: 'j',
  time1: now, time2: now, time3: now, time4: now, time5: now}

n = 1000
Benchmark.bm do |x|
  x.report('store3') { Store3.limit(n).all.map(&:num1) }
  x.report('store10') { Store10.limit(n).all.map(&:num1) }
  x.report('store30') { Store30.limit(n).all.map(&:num1) }

  x.report('serialize3') { Serialize3.limit(n).all.map{|s|s.text[:num1]} }
  x.report('serialize10') { Serialize10.limit(n).all.map{|s|s.text[:num1]} }
  x.report('serialize30') { Serialize30.limit(n).all.map{|s|s.text[:num1]} }

  x.report('normal3') { Normal3.limit(n).all.map(&:num1) }
  x.report('normal10') { Normal10.limit(n).all.map(&:num1) }
  x.report('normal30') { Normal30.limit(n).all.map(&:num1) }
end

.allでやめると、ロードされてなさそうだったので、カラムに触るという意味でmapしてます。


$ rails runner script/benchmark.rb
user system total real
store3 0.650000 0.040000 0.690000 ( 0.725642)
store10 0.630000 0.010000 0.640000 ( 0.652668)
store30 1.670000 0.010000 1.680000 ( 1.684106)
serialize3 0.330000 0.020000 0.350000 ( 0.385289)
serialize10 0.650000 0.010000 0.660000 ( 0.668932)
serialize30 1.690000 0.010000 1.700000 ( 1.716288)
normal3 0.090000 0.000000 0.090000 ( 0.093420)
normal10 0.130000 0.000000 0.130000 ( 0.132085)
normal30 0.200000 0.010000 0.210000 ( 0.216454)

オブジェクトにするところがとても重い。

容量

show table statusのData_length (1000件)


store3s: 131,072
store10s: 245,760
store30s: 1,589,248

serialize3s: 131,072
serialize10s: 245,760
serialize30s: 1,589,248

normal3s: 65,536
normal10s: 98,304
normal30s: 196,608

storeとserializeがまったく同じ。どっちもyamlかな?
当然ながらstoreとserializeは、とても大きい。

結論

基本的に遅いし容量も大きくなるので、使う場所はよく考えた方がいいかも

ハッシュの値を宣言と同時に参照する方法

久々に更新しますが、かなり自分用のメモですorz


ruby だと以下のように書くのをperlでどう書けばいいのか。。

# ruby
# 無名ハッシュ(という呼び方でいいかわからないけど)を作成と同時に値を引っ張る
{:k1 => 'v1', :k2 => 'v2'}[:k2]  # ==> "v2"


一応、こんな感じで書けたけど、リファレンスにせずに引っ張る方法はわからず。。

# perl
{k1 => 'v1', k2 => 'v2'}->{k2}  # ==> "v2"

(k1 => 'v1', k2 => 'v2'){k2}  # syntax error...


配列だとrubyと同じ感じでできるんだけどな〜。

# ruby
# 無名配列を作成と同時に値を引っ張る
%w[one two three][1]  # ==> "two"
# perl
qw(one two three)[1]  # ==> "two"

image_tagで付加される画像のタイムスタンプを、production環境でも画像ファイルが更新されたタイミングで更新されるようにする

image_tagで画像を表示した際に、srcの後ろに付くタイムスタンプは
基本的には File.mtime(image_path) なので、画像ファイルの最終更新時刻です。

image_tag 'rails.png'  #=> <img src="/images/rails.png?1298610241" alt="Rails" />


なので、developement環境では画像ファイルをtouchしてあげたりすると、タイムスタンプも更新されます。
しかし、production環境ではtouchしても更新されません。(passengerなり、サーバをリスタートすれば更新されます)


なぜこうなるかは、ActionView::Helpers::AssetTagHelper の rails_asset_id メソッドを見るとわかります。

# File actionpack/lib/action_view/helpers/asset_tag_helper.rb
        def rails_asset_id(source)
          if asset_id = ENV["RAILS_ASSET_ID"]
            asset_id
          else
            if @@cache_asset_timestamps && (asset_id = @@asset_timestamps_cache[source])
              asset_id
            else
              path = File.join(ASSETS_DIR, source)
              asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : ''

              if @@cache_asset_timestamps
                @@asset_timestamps_cache_guard.synchronize do
                  @@asset_timestamps_cache[source] = asset_id
                end
              end

              asset_id
            end
          end
        end

最初1回は、File.mtime(path) をちゃんと取ってくれるのですが、
その際に、@@asset_timestamps_cache[source] = asset_id で、キャッシュして、
以降は、そのキャッシュが使われます。


で、@@cache_asset_timestamps が何かを見ると

# File actionpack/lib/action_view/helpers/asset_tag_helper.rb
      def self.cache_asset_timestamps
        @@cache_asset_timestamps
      end

      # You can enable or disable the asset tag timestamps cache.
      # With the cache enabled, the asset tag helper methods will make fewer
      # expense file system calls. However this prevents you from modifying
      # any asset files while the server is running.
      #
      #   ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
      def self.cache_asset_timestamps=(value)
        @@cache_asset_timestamps = value
      end

      @@cache_asset_timestamps = true

booleanの値です。
さらにセッターの上に今回の答えがコメントで書かれてますが、
ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false すれば、
毎回 File.mtime(path) してくれるようになります。


なので、initializersとかに、適当なファイルを作って、

# config/intializers/cache_asset_timestamp.rb とか
ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false

だけ書いておけば、production環境でもtouchしたらタイムスタンプが更新されるようになります。


ちなみに、development環境は、config/environments/development.rb で

config.cache_classes = false

になってると思いますが、cache_classesがfalseだと、
ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
が実行されるので、development環境では何もしなくてもタイムスタンプが更新されるっぽいです。

jpmobileを使って、au/softbankでも、controller内で、ドコモ絵文字にマッピング済みのutf8値を得る方法

非常にわかりにくいタイトルですが、
ユーザがテキストフォームに絵文字を入れて送ってきたときの話しです。

以下のようなコードがあった際に、

class HogeController < ApplicationController
  mobile_filter

  def hoge
    # 例えば、ドコモの「晴れ」に対応する絵文字は、jpmobileの
    # before_filterを通った、actionの時点で、
    #   docomo   : \xEE\x98\xBE
    #   au       : \xEE\x92\x88
    #   softbank : \xEF\x81\x8A
    # となっている
  end
end

そのままhogeアクションのviewで表示するなら、jpmobileのafter_filterがかかって、


docomo : \xF8\x9F (sjis)
au : \xF8\x9F (sjis)
softbank : \xEE\x98\xBE (utf8)

になるのですが、hogeアクションの中で、au/softbankでもdocomoの値を手に入れるにはどうしたらよいでしょうか。


一応、下記の方法で手に入れることが出来たので、メモっておきます。

class HogeController < ApplicationController
  mobile_filter

  def hoge
    # strが変換したい文字列とすると
    str = Jpmobile::Emoticon::utf8_to_unicodecr(str)
    str = Jpmobile::Emoticon::unicodecr_to_external(str, Jpmobile::Emoticon::CONVERSION_TABLE_TO_DOCOMO, false)
    # これで strは
    #   docomo   : \xEE\x98\xBE
    #   au       : \xEE\x98\xBE
    #   softbank : \xEE\x98\xBE
    # となります
  end
end

単純に、jpmobileのafter_filterをかけているだけですが、
unicodecr_to_externalの3つ目の引数をfalseにしてutf8-->sjis変換を行なわないようにしています

主キー(primary key)の id を任意に指定して、createする

# ruby script/console

# 普通に指定しても無視される
>> User.create(:id => 100)
=> #<User id: 1, created_at: "2010-12-13 02:47:03", updated_at: "2010-12-13 02:47:03">

# ブロックで指定すると
>> User.create do |u|
?>   u.id = 100
>> end
=> #<User id: 100, created_at: "2010-12-13 02:48:22", updated_at: "2010-12-13 02:48:22">

使う機会は・・・orz

sqlite3-ruby の version が 1.2.x だと、select AS で名前を指定したときに integer 型のカラムのはずが string 型で取れてくる件

sqlite3-ruby : version 1.2.5

$ rails testapp1
$ cd testapp1
$ ruby script/generate model user
$ rake db:migrate
$ ruby script/console
>> User.create
=> #<User id: 1, created_at: "2010-11-25 08:49:43", updated_at: "2010-11-25 08:49:43">
>> User.first.id.class
=> Fixnum
>> User.find(:first, :select => "id AS user_id").user_id.class
=> String  # あらら


sqlite3-ruby : version 1.3.0

$ rails testapp2
$ cd testapp2
$ ruby script/generate model user
$ rake db:migrate
$ ruby script/console
>> User.create
=> #<User id: 1, created_at: "2010-11-25 08:57:44", updated_at: "2010-11-25 08:57:44">
>> User.first.id.class
=> Fixnum
>> User.find(:first, :select => "id AS user_id").user_id.class
=> Fixnum  # OK


まぁ findで:selectを使うことがそうないし、ましてAS使うこととかほぼないですが。


ちなみに rails は 2.3.10

パラメータ(params, query_paramters, path_parameters)取得方法メモ

params だとpathパラメータまで混じってたので、クエリパラメータだけ取る方法などを調べてみたメモ。


hoge コントローラに indexアクションが存在している状態で、
config/routes.rb は以下とします。

ActionController::Routing::Routes.draw do |map|
  map.edit_hoge '/hoge/:id/edit', :controller => 'hoge', :action => 'index'

  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end




/hoge?aaa=1&bbb=2 にアクセスした際の、各パラメータハッシュなどの中身は以下。

{"aaa"=>"1", "bbb"=>"2", "action"=>"index", "controller"=>"hoge"}    # params
{"aaa"=>"1", "bbb"=>"2"}                     # request.query_parameters
{"action"=>"index", "controller"=>"hoge"}    # request.path_parameters
Parameters: {"aaa"=>"1", "bbb"=>"2"}         # log に出力されるやつ




/hoge/1234/edit?aaa=1&bbb=2

{"aaa"=>"1", "bbb"=>"2", "action"=>"index", "id"=>"1234", "controller"=>"hoge"}    # params
{"aaa"=>"1", "bbb"=>"2"}                                 # request.query_parameters
{"action"=>"index", "id"=>"1234", "controller"=>"hoge"}  # request.path_parameters
Parameters: {"aaa"=>"1", "bbb"=>"2", "id"=>"1234"}       # log に出力されるやつ


つまり、request.path_parameters では、URLのパス内に含まれているパラメータのみが取得でき、
request.query_parameters では、クエリパラメータ(クエリストリングのパラメータ)のみが取得できます。
で、params は、その両方が取れると。
development.log などに出力されている Parameters は特殊な感じで、
path_parametersとquery_parametersの両方が出力されてるけど、actionとかcontrollerは入ってません。


このlogのParameters と同じ値が得られそうな変数は見当たらなかったので、
必要になることがあるかは謎だけど、出力しているであろう箇所を調べてみました。


たぶん actionpack の action_controller/base.rb の以下のメソッド。

      def log_processing_for_parameters
        parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup
        parameters = parameters.except!(:controller, :action, :format, :_method)

        logger.info "  Parameters: #{parameters.inspect}" unless parameters.empty?
      end

で、filter_parameters はログりたくないパラメータ(passwordとか)を設定してログにはかないようにすることができるものっぽいので、設定をしていなければ関係なくて、

基本的に、log の「Parameters:」は、paramsからcontroller, action, format, _method を除外して出力しているものと思ってよさそうです。

      • -

【2010-11-15 追記】

query_parameters はあくまでもquery_stringをハッシュ化したもので、
formからPOSTで送信したときなどのパラメータは含んでいませんでしたorz

なので、その際にも純粋(?)なパラメータだけを得るには、以下のようにするしかないのかなと思いました。

query_params = params.dup
request.path_parameters.each_key{|k| query_params.delete(k)}

こんなことしなくても、何か良いメソッドなり変数なりがありそうな気もするんですけどね、自分が知らないだけで。。。