【问题标题】:postgres performance issue with nested sql sub-queries嵌套 sql 子查询的 postgres 性能问题
【发布时间】:2015-02-23 21:57:58
【问题描述】:

为了简化案例,我们假设有以下 3 个表格

A(a_id), B(b_id,val_b), C(a_id,b_id,val_c)

我需要从 B 和 C 中找到所有具有特定值对的 a_id。示例找到所有具有记录的 a_id (val_b='1' and val_c='2' and B.b_id=C.b_id) AND (val_b ='3' and val_c='4' and B.b_id=C.b_id) AND ...

select A.a_id
from A
where (A.a_id in 
        (select C.a_id 
         from B, C 
         where B.b_id=C.b_id and B.val_b='1' and C.val_c='2') and
       A.a_id in 
        (select C.a_id 
         from B, C 
         where B.b_id=C.b_id and B.val_b='3' and C.val_c='4') and
       A.a_id in 
        (select C.a_id 
         from B, C 
         where B.b_id=C.b_id and B.val_b='5' and C.val_c='6'));

我注意到的是,通过添加更多 (val_b,val_c) 附加对,postgres 需要大量时间来执行查询。请注意,id、val_b 和 val_c 都有索引。

有没有办法优化查询?尝试了显式内部连接,但无助于提高性能。

提前致谢

更多信息:

  • postgres 版本 8.2.4
  • 只有一对标准在 77.621 毫秒内运行
  • 使用 2 对标准 - 151.588 毫秒
  • 使用 3 对标准 - 49483.979 毫秒

  • 请注意,单独的子查询本身在 ~62ms 下运行。

更新:

下面 Vladimir Baranov 建议的单独 INTERSECT 查询版本和 Clodoaldo Neto 使用 bool_or 聚合函数的具有子句的版本都表现得更好。谢谢!

但是,问题仍然存在,为什么 postgres 8.2 在原始查询以 3 对标准开始时具有如此出色的性能?

顺便说一句,我注意到 Vladimir Baranov 的第一个建议是使用干净连接重写查询也产生了同样的火花。见下文:

SELECT A.a_id
FROM
    A
    INNER JOIN (SELECT C.a_id FROM B INNER JOIN C ON B.b_id=C.b_id WHERE B.val_b='1' and C.val_c='2') Set1 ON Set1.a_id = A.a_id
    INNER JOIN (SELECT C.a_id FROM B INNER JOIN C ON B.b_id=C.b_id WHERE B.val_b='3' and C.val_c='4') Set2 ON Set2.a_id = A.a_id
    INNER JOIN (SELECT C.a_id FROM B INNER JOIN C ON B.b_id=C.b_id WHERE B.val_b='5' and C.val_c='6') Set3 ON Set3.a_id = A.a_id
;

使用 3 个集合时,查询运行速度非常快,但一旦添加另外 3-4 个集合,查询性能就会下降到大约 30-40 秒。

【问题讨论】:

  • 这些值实际上是字符串。我在这个例子中只使用了数值,但可以是最长 255 个字符的任何字符串。
  • 8.2?真的吗? 8.x 是no longer maintained(=支持)。您应该尽快升级到 9.4。

标签: sql postgresql database-performance in-subquery


【解决方案1】:
select a_id
from
    a
    inner join
    c using (a_id)
    inner join
    b using (b_id)
group by a_id
having
    bool_or((val_b, val_c) = (1,2)) and
    bool_or((val_b, val_c) = (3,4)) and
    bool_or((val_b, val_c) = (5,6))

http://www.postgresql.org/docs/8.2/static/functions-aggregate.html

【讨论】:

    【解决方案2】:

    看看下面的运行速度是否更快会很有趣:

    SELECT A.a_id
    FROM A
    WHERE
        A.a_id IN
        (
            SELECT C.a_id
            FROM B INNER JOIN C ON B.b_id=C.b_id
            WHERE B.val_b='1' and C.val_c='2'
    
            INTERSECT
    
            SELECT C.a_id
            FROM B INNER JOIN C ON B.b_id=C.b_id
            WHERE B.val_b='3' and C.val_c='4'
    
            INTERSECT
    
            SELECT C.a_id
            FROM B INNER JOIN C ON B.b_id=C.b_id
            WHERE B.val_b='5' and C.val_c='6'
        )
    ;
    

    实际上,这里不是多个IN,而是多个子集的显式交集。

    我的原始答案有一个与问题的原始查询不等价的查询。

    这里是SQL Fiddle,其中包含一些示例数据和原始查询,以检查我的变体是否产生与原始查询相同的结果。

    编辑

    还有一条调查途径。如果每个子查询运行得很快,但是INTERSECT在一个长查询中重复多次变得非常慢,那么您可以尝试用子查询的结果填充一个临时表,然后将这个临时表与主查询一起使用表A。有效地,使用显式临时表手动执行INTERSECT 一次一组。根据子查询返回的行数,为临时表添加索引可能会有所帮助。

    更新

    至于您的问题,为什么当查询变得复杂时 Postgres 性能会下降......您的 Postgres 版本相当旧,不太可能有人有足够的兴趣进行详细调查。我只能提供一些一般性的想法。最新版本很可能会有不同的表现,自 8.2 以来有很多变化。

    在每个 RDBMS 中,查询优化器分析查询的资源和时间都有限,因此它们使用了大量的启发式方法。随着查询中连接数量的增加,寻找最优执行计划的问题的复杂性呈指数级增长,因此必须有一个阈值,在该阈值之后优化器放弃并选择他得到的任何计划。

    你应该能够观察到它。检查快速查询的执行计划,添加另一个连接以使查询变慢并比较计划。这些计划很可能会非常不同。您应该能够确定优化器在每种情况下选择的路径。

    可能是当给定一个带有很少joins 的查询时,优化器能够将其转换为等效于使用intersect 的变体,但是如果有大量的连接,它就不能再这样做了,只能遵循加入后进行加入的查询流。它甚至可能效率低下,最终在循环内循环内循环......,换句话说,复杂性从线性跳跃到二次或更糟。

    因此,实际上,此类性能问题的唯一答案是:检查执行计划

    顺便说一句,最新版本的 Postgres 有WITH,它有效地创建了一个带有中间结果的临时表。它应该对您的情况有很大帮助,因为您的每个子查询都很简单,如果系统首先单独运行所有子查询,那么将结果组合在一起很容易。

    【讨论】:

    • 弗拉基米尔,感谢您的反馈。我的不支持 WITH 但根据您的建议,我内联了选择。因此,对于 3 套,它的性能肯定比原始版本好很多倍,但是当我尝试 7-8 套时,它再次花费了大约 4 分钟来执行 sql。我想知道也许我现在正在使用 from_collapse_limit 和 join_collapse_limit postgres 默认配置值?
    • @user1039384,我的第一个答案不正确。它不等同于您的查询。如果a_idC 中重复,它将产生重复的行。我使用INTERSECT 的第二个变体应该是正确的。请尝试一下。
    • @user1039384,你最终选择做什么?如果此处提供的任何答案有用,请点赞并接受最有用的答案。
    • @user1039384,我对查询复杂性如何影响性能的额外解释是否令人满意?您是否有机会检查您的执行计划并注意到它们如何随着查询复杂性的增加而变化?
    【解决方案3】:

    每个子查询都必须再次命中索引,这使查询的开销增加了数倍。如果我理解您的要求,这是 Or 运算符的情况:

    select a.a_id
    from A
      join c on a.a_id = c.a_id
      join b on b.b_id = c.b_id
    where 
    (
      (b.val_b = '1' and c.val_c = '2')
      or (b.val_b = '3' and c.val_c = '4')
      or (b.val_b = '5' and c.val_c = '6')
    )
    

    这将为您提供链接到 C 记录的所有 A 记录,其中 c 和 b 值是您提到的集合之一。希望这会有所帮助:)

    edit 好像是多对一:

    Select a.a_id
        , sum(case when b.val_b = '1' and c.val_c = '2' then 1 else 0 end) as Condition1
        , Sum(case when b.val_b = '3' and c.val_c = '4' then 1 else 0 end) as Condition2
        , Sum(case when b.val_b = '5' and c.val_c = '6' then 1 else 0 end) as Condition3
    from A
      join c on a.a_id = c.a_id
      join b on b.b_id = c.b_id
    group by a.a_id
    having sum(case when b.val_b = '1' and c.val_c = '2' then 1 else 0 end) > 0
        and Sum(case when b.val_b = '3' and c.val_c = '4' then 1 else 0 end) > 0
        and Sum(case when b.val_b = '5' and c.val_c = '6' then 1 else 0 end) > 0
    

    希望你能做到,

    【讨论】:

    • 更短:where (b.val_b, c.val_c) in ( ('1', '2'), ('3', '4'), ('5', '6'))
    • 我实际上只需要满足所有标准的 a_ids。您提出的解决方案将找到符合任何标准的记录。
    • 那么必须是多对一的关系,所以我们必须变得更聪明一些。让我在多对一解决方案中进行编辑,因为我对字符有限制。
    【解决方案4】:
    1. 升级到最新版本
    2. 为了清楚起见,使用JOIN 语法
    3. 使用EXISTS(...) 代替IN(...) 以获得速度和舒适度
    4. PK/FK 和索引确实有帮助!

    SELECT A.a_id
    FROM A
    WHERE EXISTS (
            SELECT *
            FROM B
            JOIN C ON B.b_id = C.b_id AND B.val_b = '1' 
            WHERE C.a_id = A.a_id AND C.val_c = '2'
            )
    AND EXISTS (
            SELECT *
            FROM B
            JOIN C ON B.b_id = C.b_id AND B.val_b = '3' 
            WHERE C.a_id = A.a_id AND C.val_c = '4'
            )
    AND EXISTS (
            SELECT *
            FROM B
            JOIN C ON B.b_id = C.b_id AND B.val_b = '5' 
            WHERE C.a_id = A.a_id AND C.val_c = '6'
            )
            ;
    

    【讨论】:

    • 我对这个版本进行了基准测试,但它产生的性能与原始查询相似。不幸的是,EXISTS 对这种情况没有多大帮助。试图了解性能突然下降的根源。也许是 postgres 配置问题??
    • 您的性能不佳是由于缺少索引。您需要至少键/连接字段的索引。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-10-08
    • 2022-08-04
    • 1970-01-01
    • 2013-02-18
    • 2012-10-29
    • 2019-11-05
    • 1970-01-01
    相关资源
    最近更新 更多