Rails:连接记录的性能问题
Rails: Performance issue with joining of records
我使用 ActiveRecord 和 MySQL 进行了以下设置:
- 用户通过会员
有很多groups
- 群组 有许多
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
比较慢,因为它是这样执行的:
- 通读所有内容 table,一边收集资料。
- 对于第 1 步中的每个条目,进入另一个 table。
- 将这些东西放入 tmp table
- 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不能生成这些查询,那就抱怨吧。
我使用 ActiveRecord 和 MySQL 进行了以下设置:
- 用户通过会员 有很多
- 群组 有许多
users
会员
groups
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
FROMmemberships
User Load (11.0ms) SELECTusers
.* FROMusers
WHEREusers
.id
IN (1, 2...)
User.where(id: Membership.uniq.select(:user_id))
User Load (15.2ms) SELECT
users
.* FROMusers
WHEREusers
.id
IN (SELECT DISTINCTmemberships
.user_id
FROMmemberships
)
User.uniq.joins(:memberships)
User Load (135.1ms) SELECT DISTINCT
users
.* FROMusers
INNER JOINmemberships
ONmemberships
.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
比较慢,因为它是这样执行的:
- 通读所有内容 table,一边收集资料。
- 对于第 1 步中的每个条目,进入另一个 table。
- 将这些东西放入 tmp table
- 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不能生成这些查询,那就抱怨吧。