SELECT 中的廉价函数如何使整个查询变慢?

LaV*_*che 1 postgresql cte parallelism functions postgresql-performance

我将 Postgres 13.3 与内部和外部查询一起使用,它们都只产生一行(只是一些关于行数的统计数据)。

我不明白为什么下面的 Query2 比 Query1 慢得多。它们基本上应该几乎完全相同,最多可能只有几毫秒的差异......

查询 1:需要 49 秒

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)

查询 2:需要 4 分 30 秒

我只添加了一行:

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包含在内似乎没有任何区别。

Query2 中调用的函数的定义:

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 输出:

Erw*_*ter 7

函数内联很重要,在这里也适用。您的 PL/pgSQL 函数不能被内联。(除了为琐碎的表达式调用另一个函数也是矫枉过正。)但是由于它仍然非常便宜并且只调用一次,所以这里不是问题

无论您使用OFFSET 0hack 还是WITH CTE t1 AS MATERIALIZED,都可以防止重复评估。(如果您打算使用OFFSET 0hack,您不妨使用稍微便宜一点的子查询,但现代 Postgres 中的简洁方式是MATERIALZEDCTE。)这也不是问题。(或者,在您成功阻止重复评估之后,准确地说不再是。)

最重要的问题是并行性。用户功能是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 次

最近记录:

4 年,2 月 前