その他
    ホーム 技術発信 DoRuby ActiveRecordの速さを追い求める. 1
    ActiveRecordの速さを追い求める. 1
     

    ActiveRecordの速さを追い求める. 1

    この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

    2があるかどうかは微妙。

     こんにちは、HelloWorld+です。
     今回もまた、ActiveRecord関連の話をしようと思います。

    状況

     どんなゲームにおいても、キャラクターは無くてはない存在です。
     そして、以下のような人間マスタと、人間パラメータマスタがあるとします。

    HumanMaster
    
    id:                          integer
    code:                        integer
    name:                        string
    hp_parameter_code:           integer
    power_parameter_code:        integer
    intelligence_parameter_code: integer
    speed_parameter_code:        integer
    
    
    HumanParameterMaster
    
    id:        integer
    code:      integer
    age:       integer
    parameter: integer
    

     才能とか、画像コードとか、職業とか年収とか余命とか、そんなものも実際のHumanMasterには含まれますが、今回は簡略化してこんな感じで。
     そして、上記のマスタからだと、人間そのもののデータの形は以下のようになります。

    Human
    id:             integer
    human_code:     integer
    hp:             integer
    power:          integer
    intelligence:   integer
    speed:          integer
    age:            integer
    

     Humanそのもので保存するデータは、HumanMasterと紐付ける為の最小限のデータ(今回だと、HumanMasterのcodeとHumanのhuman_codeが紐付いている形になります)と、Human一体一体で違った値になるパラメータ周りになります。
     そして、Humanが加齢した際には、HumanMasterのパラメータコード、そしてHumanの年齢を参照し、HumanParameterMasterそれぞれのパラメータの値を取得、更新する形になります。
     今回は、パラメータはHuman単位ではなく、Parameter単位で管理されている、とします。どちらにも利点と欠点があるのですが、そこあたりは割愛します。

    加齢の工程

    Human加齢!

    Humanのcodeから該当するHumanMasterを取得。

    Humanのage、HumanMasterのそれぞれのparameter_codeからHumanParameterMasterのパラメータを取得。

    Humanのパラメータを上書きして加齢による能力値変動が完了。

    この工程を、サーバー側で行う為の、Railsのコードに起こしてみましょう。

    def birthday!(human)
      #人間マスタ取得
      human_master = HumanMaster.find_by(code: human.code)
      #パラメータ上書き
      ["hp", "power", "intelligence", "speed"].each do |param_str|
        parameter_code = human_master[param_str + "_parameter_code"]
        parameter_master = HumanParameterMaster.find_by(code: parameter_code, age: human.age)
        human[param_str] = parameter_master.parameter
      end
      #保存
      human.save
    end
    

     基本的には、まあ、こんな感じでしょう。でも、これだとやや冗長なところがあります。
     以下の一文になります。

    parameter_master = HumanParameterMaster.find_by(code: parameter_code, age: human.age)
    

     え? 単純に取得してきてるだけじゃん、と思うかもしれませんが、これだとまだ冗長なのです。
     パラメータマスタを取得してきて、使っているのはparameterだけです。それ以外のデータは必要ありません。
     そして、レコードそのものを取ってくるより、カラムの一つのデータを取ってくる方が速いのです。
     今回はパラメータを参照する回数はhp, power, intelligence, speedの4回だけですが、もっと数が増えてきた場合、同じように一々レコードそのものを参照していると如実に処理速度の差が出てきます。
     ただ、ここで問題が生じます。
     find_byからでは、カラム単体のデータを取ってくる術はありません。
     では、どうしたら一つのデータを取って来る事が可能か?
     方法は二つあります。
     一つ目は、whereからのselectとかpluckとか。

    parameter = HumanParameterMaster.where(code: parameter_code, age: human.age).select(:parameter).take.parameter
    

     もう一つは、SQL直打ち。

    parameter = ActiveRecord::Base.connection.select_value("SELECT parameter FROM human_parameter_masters WHERE code = #{parameter_code} AND age = #{human.age}")
    

     SQL直打ちの方がちょっとだけ速いのですが、まあ、コードの中にSQL直打ちするのは美しくないと色々言われたりするので、本当に速さを求めない限りは、上で大丈夫でしょう。上に置き換えるだけでも、処理速度はかなり速くなります。

     さて、最後にRailsで実際に作った人間パラメータマスタで以下のようなコードを打ってみて、実行時間の差を感じて貰って終わりにしたいと思います。

    def get_time
      time = Time.now
      yield
      p Time.now - time
    end
    
    #レコードそのまんま
    get_time do
      1000.times do |n|
        HumanParameterMaster.find_by(id: n + 1)
      end
    end
    #3.851174sec
    
    #絞り込み
    get_time do
      1000.times do |n|
        HumanParameterMaster.where(id: n + 1).select(:parameter).take.parameter
      end
    end
    #0.714646sec
    
    #SQL直打ち
    get_time do
      1000.times do |n|
        ActiveRecord::Base.connection.select_value("SELECT parameter FROM human_parameter_masters WHERE id = #{n}")
      end
    end
    #0.369945sec
    

     では、より速さを求める(時にはコードの綺麗さを犠牲にして)Railsライフを送りましょう。