Arel の include の挙動が面白い

Rails3.0 で導入された Arel を試してみたら、なかなか面白い動きをしていました。


Unit を継承した Expense モデルが Reason に紐づいている(belongs_to)として、以下のような scope を作ります。

scope :monthly, lambda {|month|
  includes(:reason).where(
    {:occured_on => (month.beginning_of_month)..(month.end_of_month)}
  )
}

で、適当にいくつかの Expense を作ってみて development.log で SQL の具合を確認(なお、以下の SQL で時間の指定が9時間ずれているのはコンフィグでタイムゾーンを東京にしたはずなのにうまくいかないため。そちらは別途調査予定です)。

  Expense Load (0.5ms)  SELECT "units".* FROM "units" WHERE ("units"."type" = 'Expense') AND ("units"."occured_on" BETWEEN '2010-09-30 15:00:00.000000' AND '2010-10-31 14:59:59.999999')
  Reason Load (0.3ms)  SELECT "reasons".* FROM "reasons" WHERE ("reasons"."id" = 1)

おや、確かに includes を指定しているのに LEFT JOIN がかかっていません。それならば、と思って他の Reason に紐づいた Expense を作って再度実行。

  Expense Load (0.5ms)  SELECT "units".* FROM "units" WHERE ("units"."type" = 'Expense') AND ("units"."occured_on" BETWEEN '2010-09-30 15:00:00.000000' AND '2010-10-31 14:59:59.999999')
  Reason Load (0.5ms)  SELECT "reasons".* FROM "reasons" WHERE ("reasons"."id" IN (6,1,3,4,5))

やはり JOIN は使われていませんが Reason のロードに注目。私が登録した Expense のデータに紐づく Reason は一つの SQL できちんと取得されています。

さらに色々なケースを検証してみないときちんとした事は言えませんが、内部で判断して最速と思われるケースを選択してくれたり、過去の事例から最も効率が良いと思われる方式を採用しているなどの理由で、単純な JOIN を書くだけの ActiveRecord とは一味違うのが Arel なのかもしれません。