[Rails]findメソッドの挙動とincludeオプション

[Rails]findメソッドの挙動とincludeオプション


Rails使いには常識らしいのだが、最近知ったのでメモ。
findメソッドを利用する際にちょっとした手間を加えることで発行されるSQLを
減少させることができ、ちょっとしたチューニングが可能になります。


サンプル


以下のようなモデルを例にとります。
モデル図


サンプルデータとしてユーザを2レコード、アイテムを5レコード、アイテム詳細を22レコード投入しておきます。
表示させたいのは以下のような画面です。
表示したい画面


View側のソースコード


<table class="list">
<tr>
<th>Id</th>
<th>User Name</th>
<th>Item Name</th>
<th>Detail Name</th>
</tr>

<% @items.each do |item| %>
<% item.details.each do |detail| %>
<tr>
<td><%=h item.user.id.to_s + "-" + item.id.to_s + "-" + detail.id.to_s %></td>
<td><%=h item.user.name if item.user %></td>
<td><%=h item.name %></td>
<td><%=h detail.name %></td>
</tr>
<% end %>
<% end %>
</table>

モデル側


モデル : ユーザ => Userというクラス名
モデル : アイテム => Itemというクラス名
モデル : アイテム詳細 => ItemDetailというクラス名
とします


class User < ActiveRecord::Base
has_many :items
end

class Item < ActiveRecord::Base
belongs_to :user
has_many :details, :class_name => 'ItemDetail', :dependent => :delete_all
end

class ItemDetail < ActiveRecord::Base
belongs_to :item
end

ItemsControllerのfindの実装


Item.allで指定しただけの場合


以下のようにItemクラスの全レコードを取得し、上記Viewで表示させるとします


  def index
@items = Item.all
end

発行されるSQL文


Item.allでselect * from itemsが発行されます。
その後View側のitem.detailsの箇所でselect * from item_details where item_details.item_id = 1が発行されます。
さらにそのeach文内のitem.user.id.to_sでselect * from users where users.id = 1が発行されます。
それが、Itemの個数分実行されるので、全部で11回のSQLが発行されます。
以下のSQLの発行履歴の中でselect * from users where user.id = 1 or 2が複数回発行されていることに注目してください。


SELECT * FROM `items`
SELECT * FROM `item_details` WHERE (`item_details`.item_id = 1)
SELECT * FROM `users` WHERE (`users`.`id` = 1)
SELECT * FROM `item_details` WHERE (`item_details`.item_id = 2)
SELECT * FROM `users` WHERE (`users`.`id` = 2)
SELECT * FROM `item_details` WHERE (`item_details`.item_id = 3)
SELECT * FROM `users` WHERE (`users`.`id` = 1)
SELECT * FROM `item_details` WHERE (`item_details`.item_id = 4)
SELECT * FROM `users` WHERE (`users`.`id` = 2)
SELECT * FROM `item_details` WHERE (`item_details`.item_id = 5)
SELECT * FROM `users` WHERE (`users`.`id` = 1)

includeオプションを指定した場合


indexアクション内のItem.allの部分を以下のように変更します。


@items = Item.all(:include => :user)

発行されるSQLは以下のように変更されます。
select * from itemsの後にselect * from users where users.id in (1,2)が発行されています。
そしてselect * from item_details where item_details.item_id = 1の後などに発行されていた
usersへのselect文が無くなっていることがわかります。


SELECT * FROM `items`
SELECT * FROM `users` WHERE (`users`.`id` IN (1,2))
SELECT * FROM `item_details` WHERE (`item_details`.item_id = 1)
SELECT * FROM `item_details` WHERE (`item_details`.item_id = 2)
SELECT * FROM `item_details` WHERE (`item_details`.item_id = 3)
SELECT * FROM `item_details` WHERE (`item_details`.item_id = 4)
SELECT * FROM `item_details` WHERE (`item_details`.item_id = 5)

ItemDetailから検索を行う場合


根っことなるのがItemDetailであるため、以下のようにItemDetailから検索を始めます。
この際に、includeオプションをネストさせると発行されるSQLが3つになります。
※View側の変更が必要となります。


@details = ItemDetail.all(:include => {:item => :user})

発行されるSQL文


SELECT * FROM `item_details`
SELECT * FROM `items` WHERE (`items`.`id` IN (1,2,3,4,5))
SELECT * FROM `users` WHERE (`users`.`id` IN (1,2))

変更後のView


<table class="list">
<tr>
<th>Id</th>
<th>User Name</th>
<th>Item Name</th>
<th>Detail Name</th>
</tr>

<% @details.each do |detail| %>
<tr>
<td><%= detail.item.user.id.to_s + "-" + detail.item.id.to_s + "-" + detail.id.to_s %>
<td><%= detail.item.user.name %></td>
<td><%= detail.item.name %></td>
<td><%= detail.name %></td>
</tr>
<% end %>
</table>

しかしこのままだと、表示順序が変更されてしまいます。
その為、以下のようにitem_id順に並べることにします。


@details = ItemDetail.all(:include => {:item => :user}, :order => 'item_id')

発行されるSQLはorder byが付属するだけです。


SELECT * FROM `item_details` ORDER BY item_id
SELECT * FROM `items` WHERE (`items`.`id` IN (1,2,3,4,5))
SELECT * FROM `users` WHERE (`users`.`id` IN (1,2))

orderにItemDetailが持たないフィールドを指定した場合


今度はUserのidでソートすることにします。


@details = ItemDetail.all(:include => {:item => :user}, :order => 'users.id')

当然の如く、LEFT OUTER JOINで結合され、発行されるSQL文は1つとなります。


SELECT item_details.id, item_details.item_id, item_details.name, items.id, items.user_id, items.name, users.id, users.name FROM item_details LEFT OUTER JOIN items ON items.id = item_details.item_id LEFT OUTER JOIN users ON users.id = items.user_id ORDER BY users.id

allではなく、firstを指定し、includeを指定した場合のSQL


以下のように1レコードだけ取得する場合に発行されるSQLを見てみます。


@details = ItemDetail.first(:include => {:item => :user}, :order => 'users.id')

発行されるSQL文
※不要な項目及びASを除去しています。


orderでusers.idでソートしているので、当たり前といえば当たり前なのですが、
まず、テーブル結合とSELECT DISTINCTでitem_detailsのidを取得後にその取得したidを用いて
さらにテーブル結合をしつつ、where句で取得したIDでフィルタをかけています。
今回はレコード数が少ないので容易ですが、レコード数が多くなったらと思うとぞっとしますね。


SELECT DISTINCT item_details.id FROM item_details LEFT OUTER JOIN items ON items.id = item_details.item_id LEFT OUTER JOIN users ON users.id = items.user_id ORDER BY users.id LIMIT 1

SELECT item_details.id, item_details.item_id, item_details.name, items.id, items.user_id, items.name, users.id, users.name, FROM item_details LEFT OUTER JOIN items ON items.id = item_details.item_id LEFT OUTER JOIN users ON users.id = items.user_id WHERE item_details.id IN (20) ORDER BY users.id
スポンサーサイト
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。