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) SELECTusers
.* FROMusers
WHEREusers
.id
IN (1, 2...)
User.where(id: Membership.uniq.select(:user_id))
用户负载 (15.2ms) SELECT
users
.* FROMusers
WHEREusers
.id
IN (SELECT DISTINCTmemberships
.user_id
FROMmemberships
)
User.uniq.joins(:memberships)
用户负载 (135.1ms) SELECT DISTINCT
users
.* FROMusers
INNER JOINmemberships
ONmemberships
.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 DISTINCTusers
.* 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 表的大小也会减慢查询速度。
没有。使用连接不会自动从成员资格表中获取所有列。此外,您提出的解决方案返回 true
或 false
,基本上回答了“是否至少有一个用户拥有会员资格?”的问题,这与原始查询完全不同。【参考方案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:加入记录的性能问题的主要内容,如果未能解决你的问题,请参考以下文章