【问题标题】:ActiveRecord OR operator slows down query by factor of 10. Why?ActiveRecord OR 运算符将查询速度减慢 10 倍。为什么?
【发布时间】:2021-05-22 23:59:16
【问题描述】:

我有一个 ActiveRecord 查询,它使用 OR 运算符将 2 个查询链接在一起。结果恢复正常,但执行组合查询的速度大约是单独执行 2 个查询中的任何一个的 10 倍。

我们有一个Event 模型和一个Invitation 模型。可以通过邀请过滤器将User 邀请到Event,或者通过拥有Invitation 记录单独邀请。

因此,在确定邀请多少用户参加特定活动时,我们必须查看所有带有Invitations 的用户以及所有符合过滤条件的用户。我们在这里这样做:

@invited_count = @invited_by_individual.or(@invited_by_filter).distinct.count(:id)

需要注意的是,@invited_by_individual@invited_by_filter 关系中都有 referencesincludes 语句。

现在,问题是当我们执行该查询时,大约需要 1200 毫秒。如果我们单独进行查询,每个查询只需要大约 80 毫秒。所以@invited_by_filter.distinct.count@invited_by_individual.distinct.count 都在大约 80 毫秒内返回结果,但这些都不是单独完成的。

有什么方法可以加快 OR 运算符的查询速度?为什么会发生这种情况?

这是 ActiveRecord 查询生成的 SQL:

快速、单一的查询:

(79.7ms)  
SELECT COUNT(DISTINCT "users"."id") 
FROM "users" 
LEFT OUTER JOIN "invitations" 
ON "invitations"."user_id" = "users"."id" 
WHERE "invitations"."event_id" = $1  [["event_id", 732]]

慢,结合查询:

(1220.7ms)  
SELECT COUNT(DISTINCT "users"."id") 
FROM "users" 
LEFT OUTER JOIN "invitations" 
ON "invitations"."user_id" = "users"."id" 
WHERE ("invitations"."event_id" = $1 OR "users"."organization_id" = $2)  [["event_id", 732], ["organization_id", 13]]

更新,这里是解释:

(1418.2ms)  SELECT COUNT(DISTINCT "users"."id") FROM "users" LEFT OUTER JOIN "invitations" ON "invitations"."user_id" = "users"."id" WHERE ("users"."root_organization_id" = $1 OR "invitations"."event_id" = $2)  [["root_organization_id", -1], ["event_id", 749]]
 => 
EXPLAIN for: SELECT COUNT(DISTINCT "users"."id") FROM "users" LEFT OUTER JOIN "invitations" ON "invitations"."user_id" = "users"."id" WHERE ("users"."root_organization_id" = $1 OR "invitations"."event_id" = $2) [["root_organization_id", -1], ["event_id", 749]]

 #=> QUERY PLAN
                                                     
 Aggregate  (cost=121781.56..121781.57 rows=1 width=8)
   ->  Hash Right Join  (cost=113248.88..121778.64 rows=1165 width=8)
         Hash Cond: (invitations.user_id = users.id)
         Filter: ((users.root_organization_id = '-1'::integer) OR (invitations.event_id = 749))
         ->  Seq Scan on invitations  (cost=0.00..1299.70 rows=63470 width=8)
         ->  Hash  (cost=93513.28..93513.28 rows=1135328 width=12)
               ->  Seq Scan on users  (cost=0.00..93513.28 rows=1135328 width=12)
(7 rows)

更新 2,EXPLAIN 用于单独运行的查询,确实使用索引:

(91.5ms)  SELECT COUNT(*) FROM "users" INNER JOIN "invitations" ON "invitations"."user_id" = "users"."id" WHERE "users"."root_organization_id" = $1  [["root_organization_id", -1]]
 => 
EXPLAIN for: SELECT COUNT(*) FROM "users" INNER JOIN "invitations" ON "invitations"."user_id" = "users"."id" WHERE "users"."root_organization_id" = $1 [["root_organization_id", -1]]

 #=> QUERY PLAN

 Aggregate  (cost=19.05..19.06 rows=1 width=8)
   ->  Nested Loop  (cost=0.72..19.05 rows=1 width=0)
         ->  Index Scan using index_users_on_root_organization_id on users  (cost=0.43..4.45 rows=1 width=8)
               Index Cond: (root_organization_id = '-1'::integer)
         ->  Index Only Scan using index_invitations_on_user_id on invitations  (cost=0.29..14.57 rows=3 width=4)
               Index Cond: (user_id = users.id)
(6 rows)

EXPLAIN for: SELECT COUNT(DISTINCT "users"."id") FROM "users" LEFT OUTER JOIN "invitations" ON "invitations"."user_id" = "users"."id" WHERE "invitations"."event_id" = $1 [["event_id", 749]]

 #=> QUERY PLAN

 Aggregate  (cost=536.34..536.35 rows=1 width=8)
   ->  Nested Loop  (cost=0.72..536.19 rows=62 width=8)
         ->  Index Scan using index_invitations_on_event_id on invitations  (cost=0.29..11.98 rows=62 width=4)
               Index Cond: (event_id = 749)
         ->  Index Only Scan using users_pkey on users  (cost=0.43..8.45 rows=1 width=8)
               Index Cond: (id = invitations.user_id)
(6 rows)

【问题讨论】:

  • 您的索引是什么样的?该查询的 EXPLAIN 输出是什么?
  • 我刚刚用慢查询的解释更新了原帖。回复:索引,我已经确保查询中包含的每一列都有一个索引。
  • 这是对其他内容的解释(可能是使用 includes 的查询),而不是您要询问的 count(distinct ...) 查询。
  • 抱歉,那是没有count. 的查询的解释我已经用正确的解释更新了帖子。澄清一下,@invited_by_individual@ invited_by_filter ActiveRecord 关系在invitations 表上都有一个includes
  • 我在usersinvitations 上有很多索引,包括invitations.user_idinvitations.event_id。当我单独对查询运行 EXPLAIN 时,我可以看到它使用了这些索引。但是当我运行结合or 运算符的查询时,它没有。使用单独的 EXPLAIN 输出更新帖子

标签: sql ruby-on-rails database postgresql activerecord


【解决方案1】:

UNION 使您能够利用这两个索引,同时仍然防止重复。

User.from(
"(#{@invited_by_individual.to_sql} 
UNION 
#{@invited_by_filter.to_sql})"
).count

【讨论】:

  • 谢谢,当我尝试运行此查询时,我收到ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR: each UNION query must have the same number of columns). 我怀疑这是因为其中一个查询出于某种原因单独选择字段,而另一个查询正在选择“用户”。"* ”。一个查询看起来像"users"."id" AS t0_r0, "users"."email" AS t0_r1, "users"."encrypted_password...。我尝试了 unscope(:select) 无济于事,知道如何克服这个障碍吗?
  • @D-Nice User.from( "(#{@invited_by_individual.select(:id).to_sql} UNION #{@invited_by_filter.select(:id).to_sql})" ).count
  • 解决了最后一个错误,但我现在看到了ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR: subquery in FROM must have an alias),我还无法克服:/有什么想法吗?
  • @D-Nice try User.from( "(#{@invited_by_individual.select(:id).to_sql} UNION #{@invited_by_filter.select(:id).to_sql}) AS invitations" ).count
  • 添加别名后它恢复为ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR: each UNION query must have the same number of columns)。我怀疑这是因为@invited_by_individual 关系解析为SELECT "users"."id", "users"."id" AS t0_r0, "users"."email" AS t0_r1, "users"."encrypted_password" AS t0_r2,... 即使我们使用select(:id). 我也尝试在.select(:id) 之前添加unscope(:select) 没有运气。知道为什么还要像这样单独选择所有 User 列吗?
【解决方案2】:

这是您使用 OR 的查询:

SELECT COUNT(DISTINCT "users"."id") 
FROM "users" 
LEFT OUTER JOIN "invitations" 
ON "invitations"."user_id" = "users"."id" 
WHERE ("invitations"."event_id" = $1 OR "users"."organization_id" = $2)  

如果您在 Postgres 中尝试以下查询,我希望它产生相同的结果,但工作速度更快:

SELECT
    COUNT(DISTINCT id) AS cc
FROM
    (
        SELECT
            "invitations"."user_id" AS id
        FROM
            "invitations"
        WHERE
            ("invitations"."event_id" = $1)

        UNION ALL

        SELECT
            "users"."id"
        FROM
            "users" 
        WHERE
            ("users"."organization_id" = $2)
    ) AS T
;

如果您在"invitations"."event_id""users"."organization_id" 上有索引,引擎应该使用它们。如果您没有此类索引,请创建它们。

OR 的查询很慢,因为优化器不够聪明,无法执行此转换并将原始查询分成两部分。当您单独运行每个部分时,引擎会看到它可以使用适当的索引。当查询连接两个表并在WHERE 过滤器中有OR 条件时,没有单个索引可以返回所需的行,因此引擎不会尝试使用任何索引。它从users 表中读取所有1135328 行,并从invitations 表中读取所有63470 行。当然,它很慢。

我不知道如何将此查询转换为 ActiveRecord 语法。

【讨论】:

    【解决方案3】:

    主要问题似乎是对显然有大约 1m 行的用户进行顺序扫描。由于它适用于单个查询,似乎 dbms 估计由于加入,通过顺序扫描执行这些操作更有效。

    你可能想尝试什么:

    我。如果可以,请确保数据库已清空

    二。尝试使用来自两个子选择或 UNION 的计数

    SELECT count(id) FROM (
      SELECT users.id FROM users WHERE "users"."root_organization_id" = -1 
      UNION
       SELECT invitations.user_id AS id FROM invitations WHERE invitations.event_id = 749
    ) AS x
    

    【讨论】:

    • 我清空了数据库,但没有效果:/。我已经尝试过这个 UNION 查询,但一直遇到问题。我怀疑这是因为我的子查询@invited_by_individual@invited_by_filter 都使用includes. 有没有办法继续使用includes 同时也采用UNION 方法? includesUNION 的组合似乎总是给我错误 ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR: each UNION query must have the same number of columns)
    • @D-Nice 这以某种方式连接到用例。首先:这个功能只是为了显示计数器吗?因为如果是的话——你可能想重新考虑如何使用它。请注意,包含不会影响计数,因为它只是单独的查询。所以基本上:1)检查纯SQL查询是否比你的查询更快,不要专注于activerecord 2)重新考虑将此选择的值直接传递给视图/虚拟附加到模型。我想你的控制器可能看起来像 @invitation = Invitation.find(...); @counter = result_of_this_sql(@邀请)
    • 换句话说-不要专注于通过activerecord获取它,检查运行此查询是否直接解决您的问题,然后将您的变量单独传递给视图。当然以后你可能想用 Arel 试试这个
    【解决方案4】:

    使用 Or 进行过滤通常会导致性能不佳,更好的选择是使用 union,但是 union 会导致两次命中所有表。

    但是当你必须 count(distinct) 时,通常这表明数据由于连接而膨胀,这不是最好的。

    所以你可以将你的查询重写为这个,所以它有两个好处,数据不会被夸大(重复),因为不需要加入,这将有助于性能和数据库引擎仍然可以使用索引:

    FROM "users" u
    where u.id in (select user_id from "invitations" i where i."event_id" = $1)
    or u."organization_id" = $2
    

    确保 user 表中的 organization_id 有正确的索引,并且 邀请表中的 event_id

    但如果你用 union 分隔条件,你会得到更好的性能:

    SELECT COUNT(*)
    from (
    select id 
    FROM "users" u
    where u.id in (select user_id from "invitations" i where i."event_id" = $1)
    union 
    select id
    FROM "users" u
    where u."organization_id" = $2
    ) t 
    

    【讨论】:

    • 我正在尝试使用您的 SQL 使用 ActiveRecord 重写查询。澄清一下,这个查询既用作count(distinct),又用于检索实际的User 记录。这是否会改变您对该 SQL 查询所采用的方法的有效性?
    • @D-Nice 没有按照您的意思说,但它基本上确实计算了具有两个条件的用户数(如您的原始查询)
    • @D-Nice 再想一想,如果你使用 in statement 会稍微提高性能,因为你会忽略 join ,但 union 仍然会给你更好的性能。
    • 谢谢,您的第二个查询单独运行时效果很好,但我很难将其合并到我的应用程序中,因为我的子查询每个都有 include 语句(根据用户输入动态生成) - 这使UNION. 的使用变得复杂,无论我明确select 是什么列,我都会收到错误ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR: each UNION query must have the same number of columns)
    • @D-Nice 嗯...似乎您在每个联合选择中选择的列数不平衡。确保您只选择“Id”,我不是 ActiveRecord 方面的专家,但可能会在您的问题中添加您的代码,我也许能够指出问题
    【解决方案5】:

    这个问题基本上可以用postgres中的复合索引来解决

    我注意到你在这里使用的第一件事“users”.“root_organization_id”。而在慢查询中,您使用的是 "users"."organization_id"

    (91.5ms)  SELECT COUNT(*) FROM "users" INNER JOIN "invitations" ON "invitations"."user_id" = "users"."id" WHERE "users"."root_organization_id" = $1  [["root_organization_id", -1]]
    

    其次,您应该在所有这些列上都有一个复合索引

    • invitations.user_id

    • invitations.event_id

    • users.organization_id

    • users.root_organization_id

    • users.id

    在 Rails 中,您可以借助 link 添加复合索引

    完成后,登录 Postgres 控制台并针对两个表运行此命令 \d table_name 并共享结果。然后对慢查询结果运行解释查询并共享结果。

    更新:您应该在所有 5 列上都有索引。另外,重新创建索引

    【讨论】:

    • 嗨,当你说所有这些列都应该有一个复合索引时,我假设你的意思是 invitations.event_idinvitations.event_id 在一起,users.organization_id 分开?我的印象是你不能有一个包含超过 1 个表的复合索引?
    • 您需要在invitations.user_id 和invitations.event_id 上添加复合索引。和 users.organization_id 上的正常索引
    • 我添加了复合索引(用户的organization_id索引已经存在),但是查询仍然表现不佳(没有效果):/
    • 你能通过运行 \d table_name 显示两个表中的索引
    【解决方案6】:

    这是一个长镜头,但你可以尝试修改organization_id列的统计信息,然后分析表格。

    ALTER TABLE users ALTER COLUMN organization_id SET STATISTICS 1000;
    ANALYZE users;
    

    【讨论】:

      猜你喜欢
      • 2019-04-05
      • 2011-01-02
      • 2020-10-03
      • 1970-01-01
      • 2021-10-09
      • 1970-01-01
      • 2011-12-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多