【问题标题】:Nested Loop Left Join cost too much time?嵌套循环左连接花费太多时间?
【发布时间】:2020-04-07 14:51:32
【问题描述】:

这是query

EXPLAIN (analyze, BUFFERS, SETTINGS)
SELECT
    operation.id
FROM
    operation
RIGHT JOIN(
    SELECT uid, did FROM (
            SELECT uid, did FROM operation where id = 993754
        ) t
    ) parts ON (operation.uid = parts.uid AND operation.did = parts.did)

EXPLAIN 信息:

Nested Loop Left Join  (cost=0.85..29695.77 rows=100 width=8) (actual time=13.709..13.711 rows=1 loops=1)
  Buffers: shared hit=4905
  ->  Unique  (cost=0.42..8.45 rows=1 width=16) (actual time=0.011..0.013 rows=1 loops=1)
        Buffers: shared hit=5
        ->  Index Only Scan using oi on operation operation_1  (cost=0.42..8.44 rows=1 width=16) (actual time=0.011..0.011 rows=1 loops=1)
              Index Cond: (id = 993754)
              Heap Fetches: 1
              Buffers: shared hit=5
  ->  Index Only Scan using oi on operation  (cost=0.42..29686.32 rows=100 width=24) (actual time=13.695..13.696 rows=1 loops=1)
        Index Cond: ((uid = operation_1.uid) AND (did = operation_1.did))
        Heap Fetches: 1
        Buffers: shared hit=4900
Settings: max_parallel_workers_per_gather = '4', min_parallel_index_scan_size = '0', min_parallel_table_scan_size = '0', parallel_setup_cost = '0', parallel_tuple_cost = '0', work_mem = '256MB'
Planning Time: 0.084 ms
Execution Time: 13.728 ms

为什么Nested Loop 花费的时间越来越多于孩子花费的总和?我能为此做些什么? Execution Time 应该小于 1 毫秒吧?


更新:

Nested Loop Left Join  (cost=5.88..400.63 rows=101 width=8) (actual time=0.012..0.012 rows=1 loops=1)
  Buffers: shared hit=8
  ->  Index Scan using oi on operation operation_1  (cost=0.42..8.44 rows=1 width=16) (actual time=0.005..0.005 rows=1 loops=1)
        Index Cond: (id = 993754)
        Buffers: shared hit=4
  ->  Bitmap Heap Scan on operation  (cost=5.45..391.19 rows=100 width=24) (actual time=0.004..0.005 rows=1 loops=1)
        Recheck Cond: ((uid = operation_1.uid) AND (did = operation_1.did))
        Heap Blocks: exact=1
        Buffers: shared hit=4
        ->  Bitmap Index Scan on ou  (cost=0.00..5.42 rows=100 width=0) (actual time=0.003..0.003 rows=1 loops=1)
              Index Cond: ((uid = operation_1.uid) AND (did = operation_1.did))
              Buffers: shared hit=3
Settings: max_parallel_workers_per_gather = '4', min_parallel_index_scan_size = '0', min_parallel_table_scan_size = '0', parallel_setup_cost = '0', parallel_tuple_cost = '0', work_mem = '256MB'
Planning Time: 0.127 ms
Execution Time: 0.028 ms

谢谢大家,当我将索引拆分为btree(id)btree(uid, did) 时,一切正常,但是是什么导致它们不能一起使用?有什么细节或规则吗?

顺便说一句,该sql用于实时计算,这里有一些窗口函数代码没有显示。

【问题讨论】:

  • 重复执行会发生什么?另外,能否将 track_io_timing 设置为开启?
  • 主要时间花在Index Only Scan上。嵌套循环只增加了大约 0.015 毫秒。每个节点的总时间也包括所有子节点
  • 多列索引中的列顺序很重要。只有在最左边的列上有约束时,才能有效地使用索引。还更新了我的答案。考虑在将来为您的更新创建一个新问题,因为您的原始问题已得到回答,并且包含不同的问题。

标签: postgresql performance explain


【解决方案1】:

为什么嵌套循环花费的时间比子项总和花费的时间越来越多?

根据您的示例,它没有。你能详细说明是什么让你认为它是这样的吗?

无论如何,访问 4900 个页面来获取 1 个元组似乎很奢侈。我猜你的桌子没有得到足够的吸尘。

虽然现在我更喜欢 Florian 的建议,但“uid”和“did”不是索引的前导列,这就是它慢的原因。它基本上是在进行全索引扫描,将索引用作表的精简版本。遗憾的是,EXPLAIN 输出没有明确说明何时以这种方式使用索引,而不是传统的“跳转到索引的特定部分”

所以你有一个缺失的索引。

【讨论】:

  • 4900页确实有点多。索引oi 在计划中使用了两次。一次是id 的条件,一次是uiddid。由于id 的访问是合理的,我得出的结论是该索引涵盖了类似(id, uid, did) 的内容,因此仅在uiddid 上就相当慢。你觉得这个结论合理吗?
  • 是的,这似乎是合理的。
【解决方案2】:

Nested Loop 实际上并不需要太多时间。 13.709..13.711 的实际时间意味着 13.709 ms 直到准备好从该节点发出第一行,并且需要 0.002 ms 直到完成。

注意13.709ms 的启动成本包括它的两个子节点的成本。在嵌套循环开始之前,两个子节点都需要发出至少一行。

Unique 子节点在 0.011 毫秒后开始发射它的第一行(也是唯一的)。然而,Index Only Scan 子代在13.695 毫秒之后才开始发出它的第一行(也是唯一的)。这意味着您实际花费的大部分时间都在这个Index Only Scan 上。

here 有一个很好的答案,它深入解释了成本和实际时间。

https://explain.depesz.com 也有一个很好的工具,它计算每个节点的包含和独占时间。 Here 它用于您的查询计划,这清楚地表明大部分时间都花在了Index Only Scan


由于查询几乎将所有时间都花在仅索引扫描中,因此优化将具有最大的好处。为operation 表上的uiddid 列创建单独的索引应该会大大缩短查询时间。

CREATE INDEX operation_uid_did ON operation(uid, did);

当前执行计划包含 2 次仅索引扫描。

一个慢的:

  ->  Index Only Scan using oi on operation  (cost=0.42..29686.32 rows=100 width=24) (actual time=13.695..13.696 rows=1 loops=1)
        Index Cond: ((uid = operation_1.uid) AND (did = operation_1.did))
        Heap Fetches: 1
        Buffers: shared hit=4900

还有一个快速的:

  ->  Index Only Scan using oi on operation operation_1  (cost=0.42..8.44 rows=1 width=16) (actual time=0.011..0.011 rows=1 loops=1)
        Index Cond: (id = 993754)
        Heap Fetches: 1
        Buffers: shared hit=5

两者都使用索引oi,但索引条件不同。请注意,使用id 作为索引条件的快速用户只需要加载 5 页数据(Buffers: shared hit=5)。慢的需要加载 4900 页而不是 (Buffers: shared hit=4900)。这表明该索引已针对id 进行了优化,但对于uiddid 的查询则没有那么多。可能索引oi 按此顺序覆盖了id, uid, did 的所有3 列。


只有在查询中对最左边的列有约束时,才能有效地使用多列 btree 索引。 The official documentation about multi-column indexes 非常深入地解释了这一点。

【讨论】:

  • 我明白了,谢谢。我会更仔细地阅读官方文档。
猜你喜欢
  • 2016-12-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-20
  • 1970-01-01
  • 1970-01-01
  • 2021-06-05
相关资源
最近更新 更多