目次


現実の世界の Rails、第 3 回: ActiveRecord を最適化する

一般的なパフォーマンスの問題を解決する

Comments

Ruby on Rails でプログラミングをしていると悪い癖がつくかもしれません。この成長しつつあるフレームワークは、他のフレームワークでは一般的な、退屈な作業から開発者を解放してくれます。開発者は、これまで書いてきたコード行数の何分の一かで考えを表現することができます。そして ActiveRecord を使うようになります。

長年 Java™ でプログラミングしてきた私にとって、ActiveRecord は少し風変わりなものでした。私が Java フレームワークを使う場合には、独立したモデルとスキーマの間にマップを作成します。このようなフレームワークはマッピング・フレームワークです。しかし ActiveRecord では、SQL あるいはマイグレーションと呼ばれる Ruby クラスを使って、データベース・スキーマしか定義しません。データベースの構造に基づいてオブジェクト・モデルの設計をするフレームワークは、ラッピング・フレームワークと呼ばれます。しかし Rails は大部分のラッピング・フレームワークとは異なり、データベース・テーブルに対してクエリーを実行することでオブジェクト・モデルの機能を見つけることができます。複雑なクエリーを作成しなくても、SQL ではなくモデルを使って Ruby の関係をトラバースすることができます。つまりマッピング・フレームワークの威力の大部分と共に、ラッピング・フレームワークの単純さも手に入れられます。ActiveRecord は使いやすく、拡張も容易で、時にはその程度があまりにも極端なこともあります。

他のすべてのデータベース・フレームワークと同じく、ActiveRecord を使う場合も、トラブルを起こしがちな多くのことができてしまいます。例えば、取得する列を多くしすぎたり、重要な、構造的なデータベース機能 (索引やヌル制約など) を削除してしまったりする可能性があります。私は ActiveRecord が悪いフレームワークだと言っているのではありません。私は単に、アプリケーションをスケーラブルにする必要があるなら、そのアプリケーションを強固にする方法を知っておく必要があると言っているのです。この記事では、Rails という非正統的パーシスタンス・フレームワークで必要となりそうな、重要な最適化のいくつかについて解説します。

基本を学ぶ

スキーマに裏付けられたモデルを生成するのは簡単で、script/generate model model_name を使ってちょっとしたコードを生成するだけです。ご存じの通り、このコマンドは、モデルやマイグレーション、ユニット・テスト、さらにはデフォルトのフィクスチャーまで生成します。マイグレーションのいくつかのデータ列にデータを入力し、ちょっとしたテスト・データを入力し、テストをいくつか作成し、検証をいくつか追加し、そしてそれで終わり、というのは非常に魅力的です。しかし、注意しなければなりません。データベース全体の設計も考慮する必要があるのです。次のことを念頭に置いてください。

  • Rails によってデータベースの基本的なパフォーマンスの問題から解放されるわけではありません。データベースが適切に動作するためには、(通常は索引の形で) 情報が必要です。
  • Rails によってデータの完全性の問題から解放されるわけではありません。大部分の Rails 開発者はデータベースに制約を持たせることを嫌いますが、NULL 可能列などを考慮する必要があります。
  • Rails は多くの要素に対して、便利なデフォルトを持っています。しかし場合によるとテキスト・フィールドの長さのようなデフォルト属性は、大部分の現実的なアプリケーションには大きすぎます。
  • Rails は効率的にデータベースを設計するように強制はしません。

少しずつ ActiveRecord に深入りする前に、基礎が強固なことを確認し、また索引構造が適切なことを確認する必要があります。もし対象のテーブルが大きくなる可能性があり、id 以外の列を検索するのであれば、そしてもし索引が役立つなら (詳細については皆さんのデータベース・マネージャーのドキュメンテーションを見てください。データベースによって索引の使い方は異なります)、必ず索引を作成します。索引を作成するために SQL に入り込む必要はありません。単純にマイグレーションを使えばよいのです。create_table マイグレーションを使えば容易に索引を作成でき、あるいは索引を作成してくれる追加のマイグレーションも容易に作成することができます。私達が ChangingThePresent.org (「参考文献」を参照) で使っている、索引を作成するマイグレーションの一例を以下に示します。

リスト 1. マイグレーションの中で索引を作成する
class AddIndexesToUsers < ActiveRecord::Migration
  def self.up
    add_index :members, :login
    add_index :members, :email
    add_index :members, :first_name
    add_index :members, :last_name
  end

  def self.down
    remove_index :members, :login
    remove_index :members, :email
    remove_index :members, :first_name
    remove_index :members, :last_name
  end
end

ActiveRecord はid の索引を処理してくれるので、私はさまざまな検索に使用する索引を明示的に追加しています。索引を追加する理由は、このテーブルが大きく、稀にしか更新されず、そして頻繁に検索されるためです。私達は対象のクエリーでの問題を測定できるまで待ってからアクションを取ることがよくあります。こうすることで、後からデータベース・エンジンを非難しなくてもすみます。しかしユーザー・テーブルの場合は、すぐにテーブルが何百万人ものユーザーにまで大きくなり、頻繁に検索される列を索引付けしない限り非効率になります。

マイグレーションに関連して、他にも 2 つの一般的な問題があります。ヌルであってはならないストリングと列がある場合には、マイグレーションが適切にコーディングされていることを確認します。大部分の DBA (データベース管理者) は、Rails はヌル列に対して不適切なデフォルトを持っていると思うかもしれません。つまりデフォルトで列がヌルになりうるのです。ヌルになりえない列を作りたい場合には、明示的に :null => false というパラメーターを追加する必要があります。また、ストリング列がある場合には、コードが適切な制限を設定していることを確認します。デフォルトで、Rails のマイグレーションは string 列を varchar(255) としてエンコードします。通常は、これは大きすぎます。データベース構造がアプリケーションを反映したものになるよう、最大限の努力をする必要があります。無制限のログインにするのではなく、もしアプリケーションがログインを 10 文字に制限しているのであれば、それに合わせて適切にデータベースをコーディングする必要があります (リスト 2)。

リスト 2. 制限と非 NULL 可能列を持つマイグレーションのコーディング
t.column :login, :string, :limit => 10, :null => false

また、デフォルト値や、その他安全に提供できる他の情報も考慮する必要があります。最初に少しばかり手間をかけることで、後でデータの完全性の問題を追跡するための大きな時間を節約できます。データベースの基本を検討する際には、どのページが静的でキャッシュしやすいかも考えます。クエリーの最適化とページのキャッシングのどちらかを選ぶとしたら、ページをキャッシングした方がずっと大きな効果がありますが、複雑になることは我慢しなければなりません。ページやフラグメントが、州のリストや FAQ (frequently asked question) などのように、完全に静的な場合には、キャッシングは確実に効果があります。しかし他の場合では、複雑になるのを避けてデータベースのパフォーマンスを追求した方がよいかもしれません。ChangingThePresent では、問題と状況に応じて両方を行いました。もしクエリーのパフォーマンスを追求したいのであれば、この先を読んでください。

N+1 問題

デフォルトで、ActiveRecord の関係は Lazy (遅延型) です。これは、このフレームワークが、実際に関係がアクセスされるまで関係へのアクセスを待つ、ということです。例えば、あるアドレスを持つメンバーを考えてください。コンソールを開き、コマンド member = Member.find 1 を入力します。そうすると、ログに下記が付加されるのがわかります (リスト 3)。

リスト 3. Member.find(1) のログ
^[[4;35;1mMember Columns (0.006198)^[[0m   ^[[0mSHOW FIELDS FROM members^[[0m
^[[4;36;1mMember Load (0.002835)^[[0m   ^[[0;1mSELECT * FROM members WHERE
 (members.`id` = 1) ^[[0m

Member はあるアドレスに対して、has_one :address, :as => :addressable, :dependent => :destroy というマクロで定義された関係を持ちます。ActiveRecord が Member をロードした時にはログを見てもアドレス・フィールドがないことに注意してください。しかしコンソールで member.address と入力すると、リスト 4 の内容を development.log の中で見ることができます。

リスト 4. リレーションにアクセスすると強制的にデータベースがアクセスされる
  ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;35;1mAddress Load (0.252084)^[[0m   ^[[0mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 1 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m

つまり ActiveRecord は、実際に member.address がアクセスされるまでアドレスの関係へのクエリーを実行しません。このパーシスタンス・フレームワークはメンバーをロードするために大量のデータを移動する必要はないため、通常はこの Lazy な設計で問題はありません。しかし、一連のメンバーと、彼らのすべてのアドレスにアクセスしたい場合を考えてみてください (リスト 5)。

リスト 5. アドレスを持つ複数のメンバーを取得する
Member.find([1,2,3]).each {|member| puts member.address.city}

これらの全アドレスへのクエリーが表示されるはずなので、パフォーマンスの面から見ると結果は良いものではないでしょう。リスト 6 を見るとそれがわかります。

リスト 6. N+1 問題に対するクエリー
^[[4;36;1mMember Load (0.004063)^[[0m   ^[[0;1mSELECT * FROM members WHERE
 (members.`id` IN (1,2,3)) ^[[0m
  ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;35;1mAddress Load (0.000989)^[[0m   ^[[0mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 1 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;36;1mAddress Columns (0.073840)^[[0m   ^[[0;1mSHOW FIELDS FROM addresses^[[0m
^[[4;35;1mAddress Load (0.002012)^[[0m   ^[[0mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 2 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;36;1mAddress Load (0.000792)^[[0m   ^[[0;1mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 3 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m

結果は私が言った通り、ひどいものです。全メンバーに対するクエリーが 1 つあり、そしてそれぞれのアドレスに対するクエリーが別にあります。ここでは 3 人のメンバーを取得しましたが、クエリーは 4 つです。つまり N 人のメンバーに対して N+1 のクエリーです。これが恐ろしい N+1 問題です。大部分のパーシスタンス・フレームワークは、この問題を Eager Association (積極的な関連) で解決しています。Rails も例外ではありません。関係にアクセスしなければならないことがわかっているのであれば、それを最初のクエリーに含めてしまうことができます。ActiveRecord は、このために :include オプションを使います。クエリーを Member.find([1,2,3], :include => :address).each {|member| puts member.address.city} に変更したとすると、次のようにもっと良い結果を得ることができます。

リスト 7. N+1 問題を解決する
^[[4;35;1mMember Load Including Associations (0.004458)^[[0m   ^[
   [0mSELECT members.`id` AS t0_r0, members.`type` AS t0_r1,
   members.`about_me` AS t0_r2, members.`about_philanthropy`

   ...

   addresses.`id` AS t1_r0, addresses.`address1` AS t1_r1,
   addresses.`address2` AS t1_r2, addresses.`city` AS t1_r3,

   ...

   addresses.`addressable_id` AS t1_r8 FROM members
   LEFT OUTER JOIN addresses ON addresses.addressable_id
   = members.id AND addresses.addressable_type =
   'Member' WHERE (members.`id` IN (1,2,3)) ^[
   [0m
 ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:
  98:in `find'^[[0m

このクエリーの方がずっと高速です。1 つのクエリーがすべてのメンバーとアドレスを取得することがわかります。これが Eager Association の動作です。

また、ActiveRecord では :include オプションをネストすることもできますが、ネスト深さは 1 レベルのみです。例えば、contacts (連絡先) を多数持つ 1 人の Member (メンバー) と、address (住所) を 1 つ持つ 1 つの Contact を考えてみてください。あるメンバーの連絡先の住む都市をすべて表示したい場合には、リスト 8 のコードを使うことができます。

リスト 8. メンバーの連絡先の都市を取得する
member = Member.find(1)
member.contacts.each {|contact| puts contact.address.city}

このコードは確かに動作しますが、このままでは、そのメンバーと各連絡先、そして各連絡先の住所を照会しなければなりません。:include => :contacts を使って :contacts を積極的にインクルード (eagerly include) することで、少しパフォーマンスを改善することができます。両方をインクルードすると、もっとパフォーマンスを改善することができます (リスト 9)。

リスト 9. メンバーの連絡先の都市を取得する
member = Member.find(1)
member.contacts.each {|contact| puts contact.address.city}

ネストしたインクルード・オプションを使うと、さらに良くなります。

member = Member.find(1, :include => {:contacts => :address})
member.contacts.each {|contact| puts contact.address.city}

このネストしたインクルードは Rails に対して、contactsaddress という両方の関係を積極的にインクルードするように命令しています。対象のクエリーで関係を使うことがわかっている場合には、いつでも Eager Loading の方法を使うことができます。この方法は 私達が ChangingThePresent.org で最も頻繁に使用するパフォーマンス最適化方法ですが、制限もあります。3 つ以上のテーブルにわたってテーブルを結合しなければならない場合には、SQL を使った方が得策です。もしレポートを作成する必要がある場合には、ActiveRecord::Base.execute("SELECT * FROM...") を使って単純にデータベース接続を利用し、ActiveRecord を完全にバイパスした方が、まず確実に得策です。一般的に、Eager Association は十二分の効果を発揮します。それではここからは話を切り替えて、Rails 開発者が恐れる、もう 1 つのもの、継承を見てみましょう。

継承と Rails

大部分の Rails 開発者は、初めて Rails に出会ったときに、その魅力にとりつかれます。Rails はそれほど容易なのです。単純にデータベース・テーブルに type 列を作成し、すべてのサブクラスを親から継承すれば、あとは Rails が処理してくれます。例えば、Person というクラスを継承する、Customer というテーブルがあるかもしれません。Customer は Person のすべての列と、loyalty number (どの程度の上顧客かを示す数字) と注文履歴を持っています。リスト 10 を見ると、このソリューションが簡潔で美しいことがわかります。マスター・テーブルは、親とすべてのサブクラスに関して、すべての列を持っています。

リスト 10. 継承を実装する
create_table "people" do |t|
  t.column "type", :string
  t.column "first_name", :string
  t.column "last_name", :string
  t.column "loyalty_number", :string
end

class Person < ActiveRecord::Base
end

class Customer < Person
  has_many :orders
end

こうしたソリューションは、ほぼあらゆる点で適切です。コードは単純であり、反復的ではありません。クエリーは単純でパフォーマンスが優れていますが、これは複数のサブクラスにアクセスするために結合を行う必要がなく、ActiveRecord が type 列を使ってどのレコードを返すかを判断できるためです。

ただしある面で、ActiveRecord の継承は非常に限定されています。継承の階層構造が広すぎると、継承は壊れてしまいます。例えば ChangingThePresent ではいくつかのコンテンツ・タイプがあり、それぞれが、名前と、簡略説明と詳細説明、いくつかの一般的な表示属性、そしていくつかのカスタム属性を持っています。私達はすべてのコンテンツ・タイプを同じ方法で扱えるように、cause (動機) や nonprofit (非営利)、gift (贈り物)、member (メンバー)、drive (運動)、registry (登録)、その他多くの型のオブジェクトが共通のベース・クラスを継承するようにしたいと思っています。しかしそうすることはできません。もしそうしてしまうと、Rails モデルは私達のオブジェクト・モデルの中身を 1 つのテーブルの中に持つことになるからです。これは現実的なソリューションではありません。

他の方法を探る

私達はこの問題に対して、3 つのソリューションを試しました。最初のソリューションは、それぞれ独自のテーブルの中に適当な各クラスを持つようにし、ビューを使ってコンテンツ用に共通のテーブルを作る方法です。このソリューションはすぐに放棄されました。理由は Rails がデータベース・ビューをうまく処理できないためです。

2 番目のソリューションは、単純なポリモーフィズムを使う方法です。この方法では、適当な各サブクラスが独自のテーブルを持ちます。そして共通の列を各テーブルの中に持たせます。例えば、Gift、Cause、Nonprofit というサブクラスを持ち、name プロパティーしか持たない Content というスーパークラスが必要だとすると、GiftNonprofitCause はすべて name プロパティーを持つことになります。Ruby は動的な型付けをするため、これらは共通のベース・クラスを継承する必要はありません。これらは同じ一連のメソッドに応答すればよいだけです。ChangingThePresent は (特に画像を処理する場合には) 何カ所かでポリモーフィズムを使って共通の動作を提供しています。

3 番目の方法は、共通の機能を提供しますが、継承の代わりに関連を使って共通機能を提供します。ActiveRecord には Polymorphic Association (多相的な関連) という機能があります。この機能は、あるクラスに継承を使わずに共通の動作を付加する上で理想的です。Polymorphic Association の例は、先ほどのAddress で見ました。同じ方法を使って (継承を使わず)、コンテンツ管理用に共通属性を付加することができます。ContentBase というクラスを考えてみてください。通常、このクラスを別のクラスに関連付けるためには、has_one 関係と単純な外部キーを使います。しかし皆さんはおそらく、この ContentBase が複数のクラスを処理できるようにしたいはずです。そのためには、外部キーと、対象とするクラスの型を定義する列も必要です。ActiveRecord の Polymorphic Association は、正にそのように動作するのです。リスト 11 のクラスを見てください。

リスト 11. サイトのコンテンツの関係の両側
class Cause < ActiveRecord::Base
  has_one :content_base, :as => :displayable, :dependent => :destroy
  ...
end

class Nonprofit < ActiveRecord::Base
  has_one :content_base, :as => :displayable, :dependent => :destroy
  ...
end


class ContentBase < ActiveRecord::Base
  belongs_to :displayable, :polymorphic => true
end

通常、belongs_to 関係は 1 つのクラスと関係するだけですが、ContentBase の関係は多相的です。外部キーは、レコードを識別するための ID 以外に、テーブルを識別するための型も持っています。この方法を使うと、継承の利点を最大限に利用することができます。共通の機能はすべて 1 つのクラスの中にあります。しかし他にも、いくつかの副次的な効果があります。CauseNonprofit のすべての列を 1 つのテーブルの中に持つ必要がありません。

Polymorphic Association は真の外部キーを使わないため、データベース管理者の中には Polymorphic Association を好まない人もいますが、ChangingThePresent では Polymorphic Association を自由に使っています。実のところ、このデータ・モデルは、理論的には思ったほど美しくありません。参照整合性などのデータベース機能を使うことができず、ツールを使って列名を元に関係を見つけることもできません。しかし私達にとっては、この方法による問題よりも、クリーンで単純なオブジェクト・モデルの利点の方が重要なのです。

create_table "content_bases", :force => true do |t|
  t.column "short_description",          :string

  ...

  t.column "displayable_type", :string
  t.column "displayable_id",   :integer
end

まとめ

ActiveRecord は完璧に有能なパーシスタンス・フレームワークです。ActiveRecord を使えばスケーラブルで信頼性の高いシステムを構築することができます。しかし他のデータベース・フレームワークと同じく、このフレームワークが生成する SQL に注目する必要があります。時々問題が起きているのであれば、方法を調整する必要があります。索引を調整すること、include で Eager Loading を使うこと、また継承の代わりに Polymorphic Association を適切に使うことは、コード・ベースを改善するための方法のうちの、単なる 3 つにすぎません。来月は、現実の世界の Rails を作成するための、もう 1 つの例について解説する予定です。


ダウンロード可能なリソース


関連トピック

  • 「現実の世界の Rails」シリーズの他の記事も読んでください。
  • 『Java から Ruby へ ― マネージャのための実践移行ガイド』(2006 年、Pragmatic Bookshelf 刊) は、この記事の著者による本です。Java プログラミングから Ruby on Rails に切り替える意味があるのは、いつ、どのような場合か、そしてその方法について解説しています。
  • Changing The Presentは非営利のマーケットであり、1 エーカーの熱帯雨林、目の見えない人のための視力、あるいは癌研究者の 1 時間などから成る寄付を行うことができます。この記事シリーズは、このサイトを基に解説しています。
  • 「Rolling with Ruby on Rails」「Learn all about Ruby on Rails」を読んで、インストールの手順を含めて Ruby and Rails について学んでください。
  • ActiveRecordは Ruby on Rails フレームワークのためのパーシスタンス・フレームワークです。
  • Rails のウィキにある Understanding Polymorphic Associationsのページは、Polymorphic Association の使い方を説明しています。
  • 「Common performance problems with Ruby on Rails」は、Ruby on Rails の一般的なパフォーマンス問題を説明し、私が取り上げた 1 つの方法、Eager Association について解説しています。また、私が使っていない、ピギーバック属性 (piggy backed attribute) という方法も取り上げています。
  • オープンソースのRuby on RailsWeb フレームワークをダウンロードしてください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=251594
ArticleTitle=現実の世界の Rails、第 3 回: ActiveRecord を最適化する
publish-date=07172007