暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

SQL Postgres 和粗略游标的奇怪案例

原创 CiciLee 2022-08-29
335

最近,我被要求研究一个案例,在同一个数据库上以两种不同的方式执行相同的 SQL 会导致两个完全不同的延迟:133 毫秒与 750 毫秒。

一种是客户可以接受的,另一种是不能接受的。

同一个 SQL 怎么会产生两个这样不同的延迟呢?首先让我解释一下设置:第一个延迟数字,即快速数字,是一个普通的 SQL。第二个是完全相同的 SQL,但是它在 PLpgSQL 过程中,并且使用了引用。两者都返回了大约 60.000 行。两种情况下的 SQL 语句都没有任何变量,它们完全相同。

对于普通的 SQL,很容易诊断执行计划和花费的时间:只需将解释分析放在 SQL 前面,执行它,然后查看统计信息。但是,对于在 PLpgSQL 过程中执行的 SQL,无法从 PLpgSQL 过程内部获取执行计划。处理这种情况的一般方法是使用普通 SQL 方法,即从过程中复制 SQL 并对其进行解释。这就是问题所在:一旦作为普通 SQL 执行,它就会暴露出可接受的延迟,只有当作为 PLpgSQL 执行时,它才会暴露出不同的、不可接受的延迟。

所以你能对它做点啥?在我发现问题之前,我花了一些时间来解决它。

我们为此创建了一个问题,一位同事发现,如果从语句中删除 order by,它会再次正确运行。

我采用了一种非常适合我的方法,并运行 linux perf 以使用调用图 (perf record -g -p ) 记录执行堆栈,以查看它在做什么。

执行计划的许多主要部分都可以在 C 函数中看到。一种方法是查看函数 ExecScan 的性能输出(使用性能报告),并查看该函数调用了什么。这是执行计划:

Sort  (cost=14603.24..14764.65 rows=64562 width=20)
  Sort Key: tile_row, tile_col, block_row, block_col
  ->  Bitmap Heap Scan on block_tbl b  (cost=2139.33..9445.26 rows=64562 width=20)
        Recheck Cond: ((world_key = 19301) AND (perimeter_num = 8007) AND (updated_ts = '1655419238118'::bigint))
        Filter: ((updated_user_key = 0) AND (frozen_flag = 0))
        ->  Bitmap Index Scan on block_perimeter  (cost=0.00..2123.18 rows=65108 width=0)
              Index Cond: ((world_key = 19301) AND (perimeter_num = 8007) AND (updated_ts = '1655419238118'::bigint))

如果我分配 ExecScan,并查看它周围的调用,像这样:

- 27.44% ExecSort
   - 12.93% tuplesort_gettupleslot
      - 9.15% tuplesort_gettuple_common
         - 4.42% mergereadnext
            - 2.84% readtup_heap
                 1.58% __mcount_internal
            - 0.95% getlen
                 0.63% __mcount_internal
              0.63% __mcount_internal
         - 3.47% tuplesort_heap_replace_top
            - 2.84% comparetup_heap
                 1.58% __mcount_internal
           1.26% __mcount_internal
        2.52% __mcount_internal
        0.95% _mcount
   - 6.31% ExecScan
      - 5.36% BitmapHeapNext
         - 3.15% MultiExecBitmapIndexScan
              index_getbitmap
            - btgetbitmap
               - 2.21% _bt_next
                    _bt_steppage
                    _bt_readnextpage
                  - _bt_readpage
                     - _bt_checkkeys
                          0.63% FunctionCall2Coll
               - 0.95% tbm_add_tuples
                    0.63% tbm_get_pageentry
           1.26% tbm_iterate
           0.63% heap_hot_search_buffer
      - 0.63% ExecInterpExpr
           slot_getsomeattrs
           slot_deform_tuple

在那里我们得到了排序第一(ExecSort)和扫描第二(ExecScan),它执行一个称为MultiExecBitmapIndexScan的函数。 我可以看到功能和执行计划的相似之处。

好的,现在程序的性能报告怎么样? 像这样:

- 13.55% ExecScan
   - 10.57% IndexNext
      - 9.76% index_getnext
         - 9.49% index_fetch_heap
            - 3.25% ReadBufferExtended
               - ReadBuffer_common
                  - 1.08% BufTableLookup
                       0.54% __mcount_internal
                       0.54% hash_search_with_hash_value
                    0.81% PinBuffer
                    0.54% LWLockAcquire
              1.90% heap_hot_search_buffer
              1.63% heap_page_prune_opt
              0.81% __mcount_internal
            - 0.81% ReleaseAndReadBuffer
                 __mcount_internal
              0.54% _mcount
              0.54% LWLockAcquire
      - 0.81% ExecStoreTuple
           0.54% __mcount_internal
   - 1.63% ExecInterpExpr
        0.54% slot_getsomeattrs

这看起来不一样。它正在执行 IndexNext 而不是 BitmapHeapNext,因此它肯定使用不同的执行计划。另外,我找不到 ExecSort 函数。所以 SQL 执行没有执行排序。

在这一点上,我可以通过查看低级别执行来证明还有另一个执行计划。在这一点上,我的怀疑是计划器可能会针对 PLpgSQL 进行优化,并将一些工作(例如排序)留给 PLpgSQL,这可能就是它消失的原因,并且选择了另一个计划。但是 PLpgSQL 不是 postgres 代码的核心组件,它是一种可加载的过程语言,您可以通过查看扩展来验证它。

接下来我尝试将 SQL 创建为函数,然后将该函数用作过程中的表。函数以能够成为规划器优化 SQL 的“障碍”而闻名,并被视为“黑匣子”,这意味着它不会受到影响。这确实奏效了!通过创建一个返回表的函数,我接近了 SQL 性能。

但是:这意味着将过程中的所有 SQL 恢复为单个函数,并将 SQL 与该函数交换的大量工作。当然,它并没有解释为什么该过程确实会影响 SQL 优化。

然后客户回到我身边说如果他使用 refcursor 来获取一个或少量的行,它实际上非常快…但这对他没有用,因为它的使用方式,它需要所有 (60.000) 行。

这让我开始思考:等一下……如果不是程序影响这一点,而是创建的光标怎么办?并且可以解释一个游标!解释游标的方式如下:

postgres=# begin;
BEGIN
postgres=# explain declare tt cursor for select b.tile_row, b.tile_col, b.block_row, b.block_col, b.block_value
postgres-#     from block_tbl b
postgres-#    where b.world_key        = 19301
postgres-#     and b.updated_ts       = 1655419238118
postgres-#     and b.updated_user_key = 0
postgres-#     and b.frozen_flag      = 0
postgres-#     and b.perimeter_num    = 8007
postgres-# order by b.tile_row, b.tile_col, b.block_row, b.block_col;
                                                QUERY PLAN
----------------------------------------------------------------------------------------------------------
 Index Scan using block_tile on block_tbl b  (cost=0.42..40080.63 rows=64562 width=20)
   Index Cond: ((world_key = 19301) AND (frozen_flag = 0))
   Filter: ((updated_ts = '1655419238118'::bigint) AND (updated_user_key = 0) AND (perimeter_num = 8007))

答对了!这显示了与之前使用排序和位图扫描不同的执行计划。

好的,这证明了另一个计划是基于用于游标的 SQL 捕获的。但是为什么会这样呢?

事实证明,基于游标的 SQL 根据参数 cursor_tuple_fraction 自动优化了第一行的百分比,默认为 0.1,别名 10%,而常规 SQL 执行针对获取 100% 进行了优化。

这在 parsenodes.h 和 planner.c 的源代码中可见。使用游标,或使用 SPI_prepare_cursor 函数。可以设置 CURSOR_OPT_FAST_PLAN 标志,然后执行第一行优化。可悲的是,不同的优化目标没有显示出来,所以从执行计划中看不到。

概括

postgres 计划程序优化以获得 100% 的行执行。这对于游标是不同的,游标针对获取前 10% 的行进行了优化,如果您不希望 10% 的行尽可能快,但 100% 的行尽可能快,这可能会导致不同的执行计划和不同的延迟尽可能快。

原文标题:Postgres and the curious case of the cursory cursors
原文作者:Frits Hoogland
原文地址:https://dev.to/yugabyte/postgres-and-the-curious-case-of-the-cursory-cursors-3l3h

最后修改时间:2022-08-29 22:50:12
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论