在Postgres13.1中对具有少量行的表并行化非常昂贵的操作

我有一个包含少量行的表,其中一个非常昂贵的函数需要在单独的工作进程中的每一行上运行,因为该函数非常占用 CPU。我可以通过将存储参数设置parallel_workersmax_worker_processes我在下面创建了一个易于重现的示例来强制对表进行并行顺序扫描,唯一的显着区别是值列的大小实际上是多个 MB。

CREATE OR REPLACE FUNCTION very_expensive_operation(value anyelement, sleep_time integer=2) RETURNS integer as $$
    BEGIN
        perform pg_sleep(sleep_time);
        return sleep_time;
    END;
$$ LANGUAGE plpgsql immutable strict parallel safe cost 10000;

CREATE UNLOGGED TABLE expensive_rows (
    id  serial PRIMARY KEY,
    value uuid
) WITH (parallel_workers = 8);
INSERT INTO expensive_rows(value) select gen_random_uuid() identifier from generate_series(1,16);

EXPLAIN ANALYSE VERBOSE

    SELECT
        very_expensive_operation(value,2)
    FROM
      expensive_rows
;
Gather  (cost=0.00..5312.12 rows=1700 width=4) (actual time=2010.650..32042.558 rows=16 loops=1)
"  Output: (very_expensive_operation(value, 2))"
  Workers Planned: 8
  Workers Launched: 7
  ->  Parallel Seq Scan on public.expensive_rows  (cost=0.00..5312.12 rows=212 width=4) (actual time=286.078..4575.903 rows=2 loops=7)
"        Output: very_expensive_operation(value, 2)"
        Worker 0:  actual time=0.001..0.001 rows=0 loops=1
        Worker 1:  actual time=0.001..0.001 rows=0 loops=1
        Worker 2:  actual time=0.001..0.001 rows=0 loops=1
        Worker 3:  actual time=2002.537..32031.311 rows=16 loops=1
        Worker 4:  actual time=0.001..0.001 rows=0 loops=1
        Worker 5:  actual time=0.002..0.002 rows=0 loops=1
        Worker 6:  actual time=0.002..0.002 rows=0 loops=1
Planning Time: 0.086 ms
Execution Time: 32042.609 ms

正如你从EXPLAIN ANALYZE输出中看到的,我确实得到了一个并行计划,但very_expensive_function仍然莫名其妙地按顺序执行。如果并行化得当,这个查询应该需要 4 秒,但尽管产生了 7 个额外的工作人员,但需要 32 秒。

如何强制 postgres 为每行/昂贵的函数调用分配 1 个 CPU 内核而不诉诸使用 dblink?

回答

这里的问题是所有 16 个表行都在一个表块中,并行顺序扫描将块范围分配给每个工作人员进行扫描。

所以只有其中一个扫描单个块并执行所有 16 个函数调用。

在具有更多行的实际示例中,工作负载将更均匀地分布。如果表很小,您可以通过设置fillfactor为 10 并VACUUM (FULL)在表上运行来人为地膨胀表。对于像这样小的表,这不会有太大作用,但可以改善更大表的并行化。

另一个愚蠢的想法是对表进行分区,以便将行拆分为多个分区。

  • @THX1138 I think toast_tuple_target doesn't do what you think it does. You probably still only had one page for the whole table. It stopped after toasting your dummy column, not before. By combining a dummy column that is large but under the toast limit with a small fillfactor, I get the desired behavior.

以上是在Postgres13.1中对具有少量行的表并行化非常昂贵的操作的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>