优化困难查询(可能带有吱吱声)

有这样的代码(使用PublicActivity gem&Squeel)

def index @activities = Activity.limit(20).order { created_at.desc } @one = @activities.where{trackable_type == 'Post'}.includes(trackable: [:author, :project]) @two = @activities.where{trackable_type == 'Project'}.includes trackable: [:owner] @activities = @one + @two end 

但它创建了8个 SQL请求:

  SELECT "activities".* FROM "activities" WHERE "activities"."trackable_type" = 'Post' ORDER BY "activities"."created_at" DESC LIMIT 20 SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (800, 799, 798, 797, 796, 795, 794, 793, 792, 791, 790, 789, 788, 787, 786, 785, 784, 783, 782, 781) SELECT "users".* FROM "users" WHERE "users"."id" IN (880, 879, 878, 877, 876, 875, 874, 873, 872, 871, 869, 868, 867, 866, 865, 864, 863, 862, 861, 860) SELECT "projects".* FROM "projects" WHERE "projects"."id" IN (80, 79) SELECT "activities".* FROM "activities" WHERE "activities"."trackable_type" = 'Project' ORDER BY "activities"."created_at" DESC LIMIT 20 SELECT "projects".* FROM "projects" WHERE "projects"."id" IN (80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61) SELECT "users".* FROM "users" WHERE "users"."id" IN (870, 859, 848, 837, 826, 815, 804, 793, 782, 771, 760, 749, 738, 727, 716, 705, 694, 683, 672, 661) 
  1. 活动请求未加入
  2. 一些用户(post所有者和项目所有者)被加载两次
  3. 有些项目加载了两次
  4. @activities是Array。 Rails关系合并方法(除了+ )不适用于上面的代码。

有什么想法来优化它吗?

简而言之,如果不使用SQL,则无法进一步优化。 这是Rails开展业务的方式。 它不允许访问提出查询的AR模型之外的连接字段。 因此,要在其他表中获取值,它会对每个表执行查询。

它也不允许UNION或花哨的WHERE条件提供解决问题的其他方法。

好消息是这些查询都是有效的(假设trackable_type被索引)。 如果结果的大小是任何实质性的(比如几十行),那么i / o时间将主导7个简单查询副1复杂的额外开销。

即使使用SQL,也很难在一个查询中获得所需的所有连接结果。 (可以这样做,但结果将是一个哈希而不是一个AR实例。所以依赖代码将是丑陋的。)每个一个查询表非常深入到Active Record中。

@ Mr.Yoshi的解决方案是使用最小SQL的一个很好的折衷方案,除了它不允许您根据trackable_type字段有选择地加载authorproject + owner

编辑

以上对于Rails 3都是正确的。对于Rails 4,如@CMW所说, eager_load方法将与includes使用外连接而不是单独查询的方法相同。 这就是我爱的原因! 我总是学到一些东西。

非导轨4,非镂空解决方案是:

 def index @activities = Activity.limit(20).order("created_at desc") @one = @activities.where(trackable_type: 'Post') .joins(trackable: [:author, :project]).includes(trackable: [:author, :project]) @two = @activities.where(trackable_type: 'Project').joins(trackable: [:owner]) .includes(trackable: [:owner]) @activities = @one + @two end 

joinsincludes的组合看起来很奇怪,但在我的测试中它的效果令人惊讶。

这会将它减少到两个查询,而不是一个。 并且@activities仍然是一个数组。 但也许使用这种方法与squeel也将解决这个问题。 不幸的是,我不使用squeel而无法测试它。

编辑:我完全错过了关于多态关联的观点。 以上作品给力

如果你想使用AR提供的东西,它有点hacky但你可以定义只读的相关项目和post:

 belongs_to :project, read_only: true, foreign_key: :trackable_id belongs_to :post, read_only: true, foreign_key: :trackable_id 

有了这些强迫负荷的提到的方法应该工作。 仍然需要条件,因此这些关联仅被称为正确的活动。

 def index @activities = Activity.limit(20).order("created_at desc") @one = @activities.where(trackable_type: 'Post') .joins(post: [:author, :project]).includes(post: [:author, :project]) @two = @activities.where(trackable_type: 'Project').joins(project: [:owner]) .includes(project: [:owner]) @activities = @one + @two end 

这不是一个干净的解决方案,关联应该是attr_protected以确保它们没有被意外设置(这将打破多态性,我期望),但从我的测试它似乎工作。

在SQL中使用简单的Switch案例:

 def index table_name = Activity.table_name @activities = Activity.where(trackable_type: ['Post', 'Project']) .order("CASE #{table_name}.owner_type WHEN 'Post' THEN 'a' ELSE 'z' END, #{table_name}.created_at DESC") end 

然后你可以轻松添加你想要的包括;)

我相信,由于limit(20)子句,您至少需要两次AR查询调用(因为您目前拥有)。 您的查询当前最多可为您提供20个post,最多可提供20个项目,因此在单个查询中对两种活动类型进行聚合限制都不会产生预期的结果。

我认为您需要做的就是在查询中使用eager_load而不是includes强制单个查询。 这里很好地介绍了 joinsincludespreloadeager_loadreferences方法之间的差异

因此,AR和squeel:

 def index @activities = Activity.limit(20).order { created_at.desc } @one = @activities.where{trackable_type == 'Post'}.eager_loads(trackable: [:author, :project]) @two = @activities.where{trackable_type == 'Project'}.eager_loads trackable: [:owner] @activities = @one + @two end 

没有吱吱声,只使用常规的ActiveRecord 4:

 def index @activities = Activity.limit(20).order(created_at: :desc) @one = @activities.where(trackable_type: 'Post').eager_loads(trackable: [:author, :project]) @two = @activities.where(trackable_type: 'Project').eager_loads(trackable: :owner) @activities = @one + @two end 

你不需要发出吱吱声,我最近把它从我的项目中删除了,因为根据我的经验,AR 4和Arel都没问题,它对于许多复杂的查询都不能正常工作。

那是一个非常大的问题…通过它的外观,你可以在一个选择中做到,但为了可读性,我将使用两个,一个用于项目,一个用于post。

假设活动与职位/项目之间存在1:1的关系。 如果这不正确,可以使用子查询解决问题

 select * from activities a where a.trackable_type = 'Post' left join posts p on p.id = a.trackable_id -- or whatever fields join these two tables left join users u on a.user_id = u.id --this is joining to the main table, may want to join trackable, not sure left join projects p on a.project_id = p.id order by a.created_at DESC LIMIT 20 

或者,如果存在1:多关系,则类似这样:

 select * from ( select * from activities a where a.trackable_type = 'Post' order by a.created_at DESC LIMIT 20 ) activities left join posts p ... 

编辑:当我读到这篇文章时,我意识到我有点老式……我想如果你要使用如此大的原始SQL查询,你应该创建一个数据库函数,而不是将它编码到你的应用程序中