ActiveRecord使用很方便,只需要声明映射关系,就可以方便地获取各个关联对象,而且是延迟加载。有时候这种关系延迟加载可能会严重影响性能,比如下面这个例子:
class User < ActiveRecord::Base
end
class Post < ActiveRecord::Base
has_many :replies
end
class Reply < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
我们在查询某一个帖子的同时,希望取到所有回复以及回复者。
@post = Post.find(params[:id])
<%= @post.body %>
<hr />
<% @post.replies.each do |reply| -%>
<%= reply.body %><br />Post by: <%= reply.user.name %><hr />
<% end -%>
如果你查看日志,就会发现一大堆的查询,类似于:
SELECT * FROM users WHERE id = xx limit 1
这个对性能的影响还是很严重的,所以需要改进查询,在查询出reply的同时把user也查询出来。把它修改为:
@post = Post.find(params[:id], :include => {:replies => :user})
这次所有查询都在一条语句中进行了,不过某些情况下你可以会发现它反而会降低查询效率,原因在于本来只有1条的post记录也被扩展到多条了,这时候还是分开查询为好:
@post = Post.find(params[:id])
@replies = @post.replies.find(:all, :include => :user)
是的,这次总共只有2条语句,一般情况下效率都会提升许多。
使用find with :include可以提高效率,但尽量不要对一对多关系使用。如果只是一对一关系,那么即便是很多层的关联,效率也还不错。比如这样:
Street.find(1, :include => {:city => {:province => {:country => :continent}}})
有时我们还是要查询一些一对多关系,这时候也可以采用其它一些技巧,例如下面这个查询:
Comment.find(1, :include => {:user => {:replies => :post}})
我把它理解为:查询某一评论(comment),同时查出发表这一评论的用户发表过的回复帖子,以及这些帖子所回复的主题。当然这种查询可能是不合理的,只是想探讨一下如何优化这种查询。
由于数据库查询的效率往往比服务器处理要低,而且服务器很容易扩展而数据库集群则难以部署,所以我们还是要尽量减少查询次数,同时不增加查询数据量。
这里我的想法是把它分成2次查询:
@comments = Comment.find(:all, :include => :user)
user_ids = [0].concat(@comments.map(&:user).map(&:id))
replies = Replies.find(:all, :conditions => ["user_id in (?)", user_ids], :include => :post)
@user_replies = {}
replies.each {|r| @user_replies[r.user_id] ||= []; @user_replies[r.user_id] << r}
然后直接使用就可以了,在要显示某用户的回复及这些回复的主题时,只要把user.id作为key从@user_replies里提取。使用[0].concat是防止可能构造出一个空数组,查询时会生成一个非法SQL语句,这里也假设0是一个无效的ID值。
我曾经在一些查询中使用这个做法,结果数据库查询时间缩短了近10倍,整个action的处理效率也因此提高了4倍。