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

时间:2013-10-11 16:07:22

标签: sql ruby-on-rails activerecord rails-activerecord squeel

有这样的代码(使用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. 一些用户(帖子所有者和项目所有者)加载了两次
  3. 某些项目已加载两次
  4. @activities是Array。 Rails关系合并方法(+除外)不适用于上面的代码。
  5. 任何优化它的想法?

5 个答案:

答案 0 :(得分:2)

非导轨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也将解决这个问题。不幸的是,我不使用吱吱声,也无法测试它。

编辑:我完全错过了关于多态关联的观点。以上工作强制

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

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

有了这些强制加载的方法应该有效。仍然需要where条件,因此只会在正确的活动中调用这些关联。

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以确保它们没有被意外设置(这会破坏多态性,我希望),但从我的测试看起来它似乎有效。

答案 1 :(得分:1)

在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

然后您可以轻松添加所需的包含;)

答案 2 :(得分:1)

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

我认为您需要做的就是在查询中使用eager_load而不是includes来强制执行单个查询。我们很好地涵盖了joinsincludespreloadeager_loadreferences方法之间的差异here

所以,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没问题,它对于许多复杂的查询都不能正常工作。

答案 3 :(得分:1)

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

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

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

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

@ Mr.Yoshi的解决方案是使用最小SQL的良好折衷方案,但它不允许您根据author有选择地加载projectowner + trackable_type字段。

修改

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

答案 4 :(得分:0)

这是一个非常大的问题......从它的外观你可以在一个选择中做到,但为了可读性,我将使用两个,一个用于项目,一个用于帖子。

这假定活动与职位/项目之间存在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查询,你应该创建一个数据库函数,而不是将它编码成你的申请