Rails:连接记录的性能问题

Rails: Performance issue with joining of records

我使用 ActiveRecord 和 MySQL 进行了以下设置:

  1. 用户通过会员
  2. 有很多groups
  3. 群组 有许多users 会员

group_id 和 user_id 在 schema.rb 中也有一个索引:

add_index "memberships", ["group_id", "user_id"], name: "uugj_index", using: :btree

3 个不同的查询:

User.where(id: Membership.uniq.pluck(:user_id))

(3.8ms) SELECT DISTINCT memberships.user_id FROM memberships User Load (11.0ms) SELECT users.* FROM users WHERE users.id IN (1, 2...)

User.where(id: Membership.uniq.select(:user_id))

User Load (15.2ms) SELECT users.* FROM users WHERE users.id IN (SELECT DISTINCT memberships.user_id FROM memberships)

User.uniq.joins(:memberships)

User Load (135.1ms) SELECT DISTINCT users.* FROM users INNER JOIN memberships ON memberships.user_id = users.id

执行此操作的最佳方法是什么?为什么使用 join 的查询要慢得多?

下面是更有效的解决方案:

User.exists?(id: Membership.uniq.pluck(:user_id))

join 将从成员 table 中获取所有列,因此在其他查询中将花费更多时间。在这里,您只是从 memberships 中获取 rhe user_id。从 users 调用 distinct 会减慢查询速度。

@bublik42 和@user3409950 如果我必须选择生产环境查询,那么我会选择第一个:

User.where(id: Membership.uniq.pluck(:user_id))

原因:因为它会使用sqlDISTINCT关键字过滤掉数据库结果然后SELECT只从'user_id'列数据库和 return 数组形式的那些值([1,2,3..])。 结果的数据库级过滤总是比活动记录查询对象快。

对于您的第二个查询:

User.where(id: Membership.uniq.select(:user_id))

它与 'pluck' 的查询相同,但是对于 'select' 它将创建一个具有单个字段 'user_id' 的活动记录关系对象。在这个查询中,它有一个构建活动记录对象的开销:([#<Membership user_id: 1>, #<Membership user_id: 2>, ... ],这不是第一个查询的情况。虽然我没有为两者做任何真正的基准测试,但结果是显而易见的查询后跟步骤。

第三种情况在这里很昂贵,因为使用'Join'函数它将从memberships table中获取所有列并且需要更多时间来处理结果的过滤与其他查询相比。

谢谢

这是一个很好的例子,演示了 Include VS Join :

http://railscasts.com/episodes/181-include-vs-joins

请尝试包含。我非常确定。花费的时间相对较少。

User.uniq.includes(:memberships)

我认为您的索引声明有问题。

您将索引声明为:

add_index "memberships", ["group_id", "user_id"], name: "uugj_index", using: :btree 如果您的主键是 ["user_id","group_id"] - 您很高兴,但是....

在 rails 中完成这项工作并非易事。

因此,为了使用 JOIN 和用户 table 查询数据 - 您需要有 2 个索引:

add_index "memberships", ["user_id", "group_id" ]

这是因为 MySQL 处理索引的方式(它们被视为连接的字符串)

您可以在此处阅读更多相关信息 Multiple-Column Indexes

根据您的所有情况,还有其他技术可以使其更快,但建议使用 ActiveRecord 的简单方法

此外 - 我认为您不需要此处的 .uniq,因为根据 table 上的条款,结果应该是唯一的。 添加 .uniq 可以使 MySQL 使用 filesort 执行不必要的排序,通常它还会在磁盘上放置一个临时的 table。

你可以运行直接在mysql上rails生成的命令用EXPLAIN

检查

EXPLAIN <your command goes here>

带连接的查询很慢,因为它从数据库加载所有列,尽管 rails 不会以这种方式预加载它们。如果您需要预加载,那么您应该使用 includes (或类似的)。但是 includes 会更慢,因为它会为所有关联构造对象。你也应该知道 User.where.not(id: Membership.uniq.select(:user_id)) 将 return 空集,以防至少有一个 user_id 等于 nil 的成员资格,而 pluck 的查询将 return正确的关系。

第一个查询很糟糕,因为它将所有用户 ID 吸取到一个 Ruby 数组中,然后将它们发送回数据库。如果你有很多用户,那就是一个巨大的数组和大量的带宽,再加上 2 次到数据库的往返而不是一次。此外,数据库没有办法有效地处理那个巨大的数组。

第二种和第三种方式都是高效的数据库驱动方案(一种是子查询,一种是连接),但是需要有合适的索引。您需要 memberships table user_id 上的索引。

add_index :memberships, :user_id

您已有的索引仅在您想要查找属于特定组的所有用户时才有用。

更新:

如果您的 users table 中有很多列和数据,则第三个查询中的 DISTINCT users.* 会相当慢,因为 MySQL 有比较大量数据以确保唯一性。

需要说明的是:这不是 JOIN 固有的缓慢,而是 DISTINCT 的缓慢。例如:这是一种避免 DISTINCT 并仍然使用 JOIN:

的方法
SELECT users.* FROM users
INNER JOIN (SELECT DISTINCT memberships.user_id FROM memberships) AS user_ids
ON user_ids.user_id = users.id;

鉴于所有这些,在这种情况下,我相信第二个查询将是最适合您的方法。如果添加上述索引,第二个查询 应该 比原始结果中报告的更快。如果您在添加索引后还没有这样做,请重试第二种方法。

尽管第一个查询本身存在一些缓慢的问题,但从您的评论来看,很明显它仍然比第三个查询快(至少对于您的特定数据集而言)。这些方法的权衡将取决于您的特定数据集,即您拥有多少用户以及您拥有多少会员资格。一般来说,我认为第一种方法仍然是最差的,即使它最终更快。

另外,请注意我推荐的索引是专门为您在问题中列出的三个查询而设计的。如果您对这些 tables 有其他类型的查询,您可能会更好地使用其他索引,或者可能是多列索引,如 his/her 答案中提到的@tata。

SELECT  DISTINCT users.*
    FROM  users
    INNER JOIN  memberships
       ON memberships.user_id = users.id

比较慢,因为它是这样执行的:

  1. 通读所有内容 table,一边收集资料。
  2. 对于第 1 步中的每个条目,进入另一个 table。
  3. 将这些东西放入 tmp table
  4. dedup (DISTINCT) table 交付结果

如果有 1000 个用户,每个用户有 100 个成员资格,那么第 3 步中的 table 将有 100000 行,即使答案只有 1000 行。

这是一个"semi-join",只检查用户是否至少有一个会员资格;它更有效率:

SELECT  users.*
    FROM  users  -- no DISTINCT needed
    WHERE  EXISTS 
      ( SELECT  *
            FROM  memberships ON memberships.user_id = users.id 
      ) 

如果你真的不需要那个检查,那么这会更快:

SELECT users.*
    FROM  users

如果Rails不能生成这些查询,那就抱怨吧。