LaV*_*che 1 postgresql cte parallelism functions postgresql-performance
我将 Postgres 13.3 与内部和外部查询一起使用,它们都只产生一行(只是一些关于行数的统计数据)。
我不明白为什么下面的 Query2 比 Query1 慢得多。它们基本上应该几乎完全相同,最多可能只有几毫秒的差异......
WITH t1 AS (
SELECT
(SELECT COUNT(*) FROM racing.all_computable_xformula_bday_combos) AS all_count,
(SELECT COUNT(*) FROM racing.xday_todo_all) AS todo_count,
(SELECT COUNT(*) FROM racing.xday) AS xday_row_count
OFFSET 0 -- this is to prevent inlining
)
SELECT
t1.all_count,
t1.all_count-t1.todo_count AS done_count,
t1.todo_count,
t1.xday_row_count
FROM t1;
Run Code Online (Sandbox Code Playgroud)
我只添加了一行:
WITH t1 AS (
SELECT
(SELECT COUNT(*) FROM racing.all_computable_xformula_bday_combos) AS all_count,
(SELECT COUNT(*) FROM racing.xday_todo_all) AS todo_count,
(SELECT COUNT(*) FROM racing.xday) AS xday_row_count
OFFSET 0 -- this is to prevent inlining
)
SELECT
t1.all_count,
t1.all_count-t1.todo_count AS done_count,
t1.todo_count,
t1.xday_row_count,
-- the line below is the only difference to Query1:
util.divide_ints_and_get_percentage_string(todo_count, all_count) AS todo_percentage
FROM t1;
Run Code Online (Sandbox Code Playgroud)
在此之前,并且在外部查询中有一些额外的列(应该几乎为零差异),整个查询非常慢,比如 25 分钟,我认为这可能是由于内联?因此OFFSET 0
被添加到两个查询中(这确实有很大帮助)。
我也一直在使用上述 CTE 与子查询之间进行交换,但OFFSET 0
包含在内似乎没有任何区别。
CREATE OR REPLACE FUNCTION util.ratio_to_percentage_string(FLOAT, INTEGER) RETURNS TEXT AS $$ BEGIN
RETURN ROUND($1::NUMERIC * 100, $2)::TEXT || '%';
END; $$ LANGUAGE plpgsql IMMUTABLE;
CREATE OR REPLACE FUNCTION util.divide_ints_and_get_percentage_string(BIGINT, BIGINT) RETURNS TEXT AS $$ BEGIN
RETURN CASE
WHEN $2 > 0 THEN util.ratio_to_percentage_string($1::FLOAT / $2::FLOAT, 2)
ELSE 'divide_by_zero'
END
;
END; $$ LANGUAGE plpgsql IMMUTABLE;
Run Code Online (Sandbox Code Playgroud)
正如你所看到的,这是一个非常简单的函数,它只被调用一次,从整个事情产生的单行开始。这怎么会导致如此大规模的放缓?为什么它会影响 Postgres 是否内联初始子查询/CTE?(或者这里还有什么可能发生的事情?)
此外,该函数的作用根本无关紧要,只需将其替换为一个只返回一个TEXT
字符串的函数,hello
就会导致初始内部查询的速度完全相同。因此,这与函数“做”的任何事情无关,而更像是某种“薛定谔的猫”效应,其中外部查询中的内容会影响内部查询最初的执行方式。为什么外部查询中的一个简单的微小变化(基本上对性能的影响为零)会影响初始内部查询?
EXPLAIN ANALYZE
输出:函数内联很重要,在这里也适用。您的 PL/pgSQL 函数不能被内联。(除了为琐碎的表达式调用另一个函数也是矫枉过正。)但是由于它仍然非常便宜并且只调用一次,所以这里不是问题。
无论您使用OFFSET 0
hack 还是WITH CTE t1 AS MATERIALIZED
,都可以防止重复评估。(如果您打算使用OFFSET 0
hack,您不妨使用稍微便宜一点的子查询,但现代 Postgres 中的简洁方式是MATERIALZED
CTE。)这也不是问题。(或者,在您成功阻止重复评估之后,准确地说不再是。)
最重要的问题是并行性。用户功能是PARALLEL UNSAFE
默认的。手册:
PARALLEL UNSAFE
表示无法在并行模式下执行该函数,并且 SQL 语句中存在此类函数会强制执行串行执行计划。这是默认设置。
大胆强调我的。
您的第一个(快速)查询计划显示 2xParallel Seq Scan
和 1x Parallel Index Only Scan
。
您的第二个(慢速)查询计划没有并行查询。造成的伤害。
标记您的函数PARALLEL SAFE
(因为它们符合条件!),问题就会消失。有关的:
我用几个变体进行了性能测试。看:
这个等效的函数要快得多,并且可以内联:
CREATE OR REPLACE FUNCTION util.divide_ints_and_get_percentage_string(bigint, bigint)
RETURNS text
LANGUAGE sql IMMUTABLE PARALLEL SAFE AS
$func$
SELECT CASE WHEN $2 = 0 THEN 'divide_by_zero'
ELSE round($1 * 100 / $2::numeric, 2)::text || '%' END -- explicit cast!
$func$;
Run Code Online (Sandbox Code Playgroud)
最重要的是,它LANGUAGE sql
允许函数内联,(与 不同LANGUAGE plpgsql
)。看:
值得注意的是,我们需要显式转换::text
。连接运算符||
被解析为几个内部函数之一,具体取决于涉及的数据类型,并非所有函数都是IMMUTABLE
. 如果没有显式转换,Postgres 会选择一个只有 的变体,STABLE
这会不同意函数声明并阻止函数内联。偷偷摸摸的细节!有关的:
修复了一个逻辑问题:$2 = 0
正确检查除以零(与 不同$2 > 0
)。现在,count(*)
永远不会是负数,但是由于您将逻辑放入函数中,因此它与该前提条件隔离开来。
或者直接将简单的表达式放入查询中。没有函数调用。这不受上述任何问题的影响。
归档时间: |
|
查看次数: |
160 次 |
最近记录: |