Rails:加入记录的性能问题

Posted

技术标签:

【中文标题】Rails:加入记录的性能问题【英文标题】:Rails: Performance issue with joining of records 【发布时间】:2016-01-12 16:56:09 【问题描述】:

我对 ActiveRecord 和 mysql 进行了以下设置:

    用户通过会员拥有许多groups 通过会员拥有许多users

schema.rb 中还描述了 group_id 和 user_id 的索引:

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

3 个不同的查询:

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

(3.8ms)从memberships中选择不同的memberships.user_id 用户负载 (11.0ms) SELECT users.* FROM users WHERE users.id IN (1, 2...)

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

用户负载 (15.2ms) SELECT users.* FROM users WHERE users.id IN (SELECT DISTINCT memberships.user_id FROM memberships)

User.uniq.joins(:memberships)

用户负载 (135.1ms) SELECT DISTINCT users.* FROM users INNER JOIN memberships ON memberships.user_id = users.id

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

【问题讨论】:

请尝试包含。我很确定。这将花费相对较少的时间。 User.uniq.includes(:memberships) 除非您计划让您的用户成为同一组的成员两次 - 您应该使您的索引唯一。 【参考方案1】:

第一个查询很糟糕,因为它将所有用户 id 吸入一个 Ruby 数组,然后将它们发送回数据库。如果您有很多用户,那将是一个庞大的数组和大量的带宽,加上到数据库的 2 次往返而不是一次。此外,数据库无法有效地处理这个庞大的数组。

第二种和第三种方法都是高效的数据库驱动解决方案(一种是子查询,一种是连接),但您需要有适当的索引。您需要user_id 上的memberships 表上建立索引。

add_index :memberships, :user_id

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

更新:

如果您的users 表中有很多列和数据,则第三个查询中的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;

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

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

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

【讨论】:

'用户负载 (44.7ms) SELECT DISTINCT users.* FROM users INNER JOIN memberships ON memberships.user_id = users.`id' 谢谢,索引有帮助,但这个查询仍然比 pluck 或 select 慢 3 倍 @user3409950 我已经更新了我的答案以解决您的评论。【参考方案2】:

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

【讨论】:

没有。带有连接的查询从两个表中加载所有列。它很慢,因为它不能使用给定的索引。【参考方案3】:

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

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

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

【讨论】:

取决于在其上运行.uniq 的Membership 表的大小也会减慢查询速度。 没有。使用连接不会自动从成员资格表中获取所有列。此外,您提出的解决方案返回 truefalse,基本上回答了“是否至少有一个用户拥有会员资格?”的问题,这与原始查询完全不同。【参考方案4】:

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

您将索引声明为:

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

在 Rails 中实现这一点并非易事。

因此,要使用 Users 表使用 JOIN 查询数据 - 您需要有 2 个索引:

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

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

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

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

此外 - 我认为您在这里不需要.uniq,因为由于表格上的条款,结果无论如何都应该是唯一的。 添加.uniq 可以使MySQL 使用filesort 进行不必要的排序,通常它还会在磁盘上放置一个临时表。

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

EXPLAIN <your command goes here>

【讨论】:

你说得对,问题出在索引上。但是,您不需要 2 个索引,也不需要多列索引,至少对于这些特定查询而言。 user_id 上的单个索引对查询 2 和 3 最有帮助。 这取决于您的使用情况。如果您需要一直查询所有会员属性 - 是的。就够了。但是,如果您需要获取有关单个组的信息,并且您的用户可能有成千上万的关系 - 您的方法是不够的,而我的方法 - 在这两种情况下都适用。【参考方案5】:

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

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

原因: 因为它会使用 sql DISTINCT 关键字过滤出数据库结果,然后从数据库中仅 SELECT 'user_id' 列并以数组形式返回这些值([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表中获取所有列,与其他查询相比,处理结果过滤需要更多时间。

谢谢

【讨论】:

没有。对于第二个查询,ActiveRecord 实际上足够聪明,可以执行子查询(查看问题中发布的实际 SQL),因此 Rails 不会加载这些成员记录。 谢谢@Nathan 我同意你的观点,即第一次查询大表会很慢。【参考方案6】:
SELECT  DISTINCT users.*
    FROM  users
    INNER JOIN  memberships
       ON memberships.user_id = users.id

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

    遍历一张桌子,边走边收集东西。 对于第 1 步中的每个条目,请访问另一个表。 将这些内容放入 tmp 表中 删除 (DISTINCT) 该表以提供结果

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

这是一种“半加入”,仅检查用户是否至少拥有一个成员资格;效率更高:

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

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

SELECT users.*
    FROM  users

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

【讨论】:

【参考方案7】:

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

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

请尝试包含。我很确定。这将花费相对较少的时间。

User.uniq.includes(:memberships)

【讨论】:

原始示例查找至少拥有一个成员资格的用户。此查询返回所有用户,无论他们是否有会员资格。

以上是关于Rails:加入记录的性能问题的主要内容,如果未能解决你的问题,请参考以下文章

Rails/MySQL:使用 LEFT JOINS 的 Group/Distinct 使查询时间加倍/性能降低

加入表时性能缓慢

Rails,mongoid,heroku 性能

DB2 性能案例与 COALESCE

Rails 低级缓存不适用于活动记录

减少 Mongrel Rails 内存占用并提高性能?