【问题标题】:Join of indexed ids over 3 tables unexpectedly slow超过 3 个表的索引 id 连接出乎意料地慢
【发布时间】:2019-12-28 21:22:16
【问题描述】:

我有三个构建层次结构的表:

  • customer 主键id
  • portfolio,主键id,外键fk_customer(索引)
  • position,主键id,外键fk_portfolio(索引)

客户有投资组合(但有些没有),投资组合有头寸(但有些没有)。

我有一个视图,它基本上选择了这个层次结构,通常使用客户 ID、投资组合 ID 或职位 ID 的子句进行查询。 使用此视图进行选择的性能出奇地差,并且在我希望执行时间低于 10 毫秒的情况下需要超过一秒钟。

为了分析性能,我将查询隔离并简化如下:

SELECT bp.id, ptf.id, pos.id FROM customer bp
left outer join portfolio ptf on ptf.fk_customer = bp.id
left outer join position pos on pos.fk_portfolio = ptf.id
WHERE ptf.id IN (1, 2)
OR pos.id IN (3, 4)

在具体设置中(70k 客户、100k 投资组合、600k 头寸),此查询需要将近一秒钟(返回大约 10 行)。我在 Oracle 和 Postgres 上重建了这个设置(相同的数据,相同数量的记录),都显示出相同的性能问题。
当我稍微改变一下视图时(WHERE pos.fk_portfolio IN (1, 2)),执行时间大约是 0.1ms,但是没有返回没有仓位的投资组合。

Postgres 上的执行计划:

Gather  (cost=22125.87..27689.07 rows=13 width=24) (actual time=703.717..782.415 rows=9 loops=1)
  Workers Planned: 2
  Workers Launched: 2
  ->  Parallel Hash Left Join  (cost=21125.87..26687.77 rows=5 width=24) (actual time=700.739..751.123 rows=3 loops=3)
        Hash Cond: (ptf.id = pos.fk_portfolio)
        Filter: ((ptf.id = ANY ('{1,2}'::bigint[])) OR (pos.id = ANY ('{3,4}'::bigint[])))
        Rows Removed by Filter: 202202
        ->  Parallel Hash Left Join  (cost=3057.84..5195.48 rows=42990 width=16) (actual time=70.319..171.940 rows=39930 loops=3)
              Hash Cond: (bp.id = ptf.fk_customer)
              ->  Parallel Index Only Scan using sys_c0011416 on customer bp  (cost=0.29..1440.43 rows=29642 width=8) (actual time=0.026..20.169 rows=23714 loops=3)
                    Heap Fetches: 0
              ->  Parallel Hash  (cost=2298.91..2298.91 rows=60691 width=16) (actual time=69.626..69.627 rows=34392 loops=3)
                    Buckets: 131072  Batches: 1  Memory Usage: 5920kB
                    ->  Parallel Seq Scan on portfolio ptf  (cost=0.00..2298.91 rows=60691 width=16) (actual time=0.027..38.559 rows=34392 loops=3)
        ->  Parallel Hash  (cost=13796.90..13796.90 rows=245690 width=16) (actual time=415.120..415.121 rows=196552 loops=3)
              Buckets: 131072  Batches: 16  Memory Usage: 2816kB
              ->  Parallel Seq Scan on "position" pos  (cost=0.00..13796.90 rows=245690 width=16) (actual time=0.009..222.681 rows=196552 loops=3)
Planning Time: 1.280 ms
Execution Time: 782.808 ms

禁用序列扫描 (set enable_seqscan = false) 也无济于事。 我也

  • 确保索引(在外键约束上)存在并且处于活动状态
  • 更新统计信息并压缩表(对所有 3 个表进行 VACUUM ANALYZE)
  • 重新索引表(所有 3 个表上的 REINDEX)

我发现表达查询的其他方法(使用两个单独的选择和 id 过滤,然后将两者联合)显示出出色的性能,但 没有一种方法可以让我创建视图 之后我可以按客户/投资组合/职位 ID 进行过滤。
联合示例(执行时间

(select bp.id, ptf.id, pos.id from customer bp
    left outer join portfolio ptf on ptf.fk_customer = bp.id
    left outer join position pos on pos.fk_portfolio = ptf.id
    where ptf.id IN (1, 2))
UNION
(select bp.id, ptf.id, pos.id from customer bp
    left outer join portfolio ptf on ptf.fk_customer = bp.id
    left outer join position pos on pos.fk_portfolio = ptf.id
    where pos.id IN (3, 4))

我束手无策 - 我本来预计查询会非常快,因为:

  • 它并没有真正获取任何数据(仅获取主键)
  • 仅按主键过滤,并且
  • 通过索引外键列连接。

我希望你们中的任何人都可以了解为什么性能如此糟糕(在 Postgres 和 Oracle 上),并就如何解决这个问题提出建议。

编辑:
我正在通过不支持联合的 JPA(Java Persistence API)查询数据。但是,我可以将联合用作视图定义的一部分,因为我只需要通过 JPA 传递标准(在子句中)。

【问题讨论】:

  • 关于返回没有匹配投资组合的客户,您必须将这些包含在 where 子句中,即添加 OR ptf.id IS NULLOR pos.id IS NULL
  • @Adder 在我对投资组合和/或头寸有限制的情况下,我特别想排除没有投资组合和头寸的客户,因为他们既不符合投资组合也不符合 id 标准。
  • 条件pos.id IN (3, 4) 将外部连接变为position 回到内部连接 ​​- 这真的是您想要的吗?

标签: postgresql performance jpa


【解决方案1】:

您肯定需要带有UNION 的变体,因为OR 不能很好地执行。

问题在于 PostgreSQL 无法从 IN 列表中推断出它可以将外连接转换为内连接。

尝试编写内部连接:

(select bp.id, ptf.id, pos.id from customer bp
    join portfolio ptf on ptf.fk_customer = bp.id
    left outer join position pos on pos.fk_portfolio = ptf.id
    where ptf.id IN (1, 2))
UNION
(select bp.id, ptf.id, pos.id from customer bp
    join portfolio ptf on ptf.fk_customer = bp.id
    join position pos on pos.fk_portfolio = ptf.id
    where pos.id IN (3, 4));

或者尝试添加一个可能给 PostgreSQL 提供线索的条件:

(select bp.id, ptf.id, pos.id from customer bp
    left outer join portfolio ptf on ptf.fk_customer = bp.id
    left outer join position pos on pos.fk_portfolio = ptf.id
    where ptf.id IN (1, 2))
      and ptf.id IS NOT NULL
UNION
(select bp.id, ptf.id, pos.id from customer bp
    left outer join portfolio ptf on ptf.fk_customer = bp.id
    left outer join position pos on pos.fk_portfolio = ptf.id
    where pos.id IN (3, 4)
      and pos.id IS NOT NULL);

我不确定第二个查询是否能解决问题。

【讨论】:

  • 此解决方案不能用作视图(我可以使用不同的投资组合或职位 ID 过滤器进行查询),不幸的是我在查询时无法使用联合,因为我使用的是 JPA(除非我跌倒回到原生查询)。
  • 我建议您不要使用视图。创建所有数据的“世界视图”并将其视为查询中的非规范化表是一种常见的误解。您不能以这种方式有效地使用视图;如您所见,条件很重要。但是,您可以为每种类型的查询创建一个视图,作为隐藏实际表定义的界面的一种方式。至于您选择 ORM 所施加的限制:要么使用更灵活的 ORM,要么使用原生查询。
  • 我最终使用了该视图,但查询了两次(一次按仓位 ID 过滤,一次按投资组合 ID 过滤)。最后,它就像使用一个联合(这需要我将它实现为使用 JPA 的本机查询,因为不直接支持联合),只有这样我才能获得完整的 JPA 支持,但需要执行两个(简单)查询(其中但是可以并行完成)然后组合结果。接受你的回答,谢谢。
猜你喜欢
  • 2012-06-26
  • 2014-07-01
  • 2020-10-27
  • 2014-06-23
  • 1970-01-01
  • 1970-01-01
  • 2017-01-08
  • 1970-01-01
  • 2017-12-01
相关资源
最近更新 更多