【问题标题】:How to avoid multiple function evals with the (func()).* syntax in an SQL query?如何避免在 SQL 查询中使用 (func()).* 语法的多个函数评估?
【发布时间】:2013-08-24 13:17:56
【问题描述】:

上下文

当函数返回TABLESETOF composite-type 时,像这个示例函数:

CREATE FUNCTION func(n int) returns table(i int, j bigint) as $$
BEGIN
  RETURN QUERY select 1,n::bigint 
      union all select 2,n*n::bigint
      union all select 3,n*n*n::bigint;
END
$$ language plpgsql;

可以通过各种方法访问结果:

1) select * from func(3) 将产生这些输出列:

我 | j ---+--- 1 | 3 2 | 9 3 | 27

2) select func(3) 将只产生一个 ROW 类型的输出列。

功能 -------- (1,3) (2,9) (3,27)

3) select (func(3)).* 会产生类似 #1 的结果:

我 | j ---+--- 1 | 3 2 | 9 3 | 27

当函数参数来自表或子查询时,语法#3 是唯一可能的,如:

select N, (func(N)).* from (select 2 as N union select 3 as N) s;

或在这个相关的answer 中。如果我们有LATERAL JOIN,我们可以使用它,但在 PostgreSQL 9.3 发布之前,它不受支持,而且之前的版本仍然会使用多年。

问题

现在语法 #3 的问题是函数被调用的次数与结果中的列数一样多。没有明显的原因,但它发生了。 通过在函数中添加RAISE NOTICE 'called for %', n,我们可以在 9.2 版本中看到它。通过上面的查询,它输出:

注意:要求 2 注意:要求 2 注意:要求 3 注意:要求 3

现在如果函数更改为返回 4 列,如下所示:

CREATE FUNCTION func(n int) returns table(i int, j bigint,k int, l int) as $$
BEGIN
  raise notice 'called for %', n;
  RETURN QUERY select 1,n::bigint,1,1 
      union all select 2,n*n::bigint,1,1
      union all select 3,n*n*n::bigint,1,1;
END                                        
$$ language plpgsql stable;

那么同样的查询输出:

注意:要求 2 注意:要求 2 注意:要求 2 注意:要求 2 注意:要求 3 注意:要求 3 注意:要求 3 注意:要求 3

需要 2 个函数调用,实际调用了 8 个。比率是输出列的数量。

如果语法 #2 产生相同的结果(输出列布局除外),则不会发生这些多次调用:

select N,func(N) from (select 2 as N union select 3 as N) s;

给予:

注意:要求 2 注意:要求 3

随后是 6 个结果行:

n |功能 ---+------------ 2 | (1,2,1,1) 2 | (2,4,1,1) 2 | (3,8,1,1) 3 | (1,3,1,1) 3 | (2,9,1,1) 3 | (3,27,1,1)

问题

9.2 中是否有一种语法或构造可以通过只执行最少的所需函数调用来实现预期结果?

额外问题:为什么会发生多次评估?

【问题讨论】:

    标签: postgresql user-defined-functions


    【解决方案1】:

    您可以将其包装在子查询中,但如果没有OFFSET 0 hack,则无法保证安全。在 9.3 中,使用LATERAL。该问题是由解析器有效地将* 宏扩展为列列表引起的。

    解决方法

    地点:

    SELECT (my_func(x)).* FROM some_table;
    

    将对来自函数的n 结果列评估my_func n 次,这个公式:

    SELECT (mf).* FROM (
        SELECT my_func(x) AS mf FROM some_table
    ) sub;
    

    通常不会,也不会在运行时添加额外的扫描。为保证不会执行多重评估,您可以使用 OFFSET 0 hack 或滥用 PostgreSQL 无法跨 CTE 边界进行优化:

    SELECT (mf).* FROM (
        SELECT my_func(x) AS mf FROM some_table OFFSET 0
    ) sub;
    

    或:

    WITH tmp(mf) AS (
        SELECT my_func(x) FROM some_table
    )
    SELECT (mf).* FROM tmp;
    

    在 PostgreSQL 9.3 中,您可以使用 LATERAL 来获得更理智的行为:

    SELECT mf.*
    FROM some_table
    LEFT JOIN LATERAL my_func(some_table.x) AS mf ON true;
    

    LEFT JOIN LATERAL ... ON true 像原始查询一样保留所有行,即使函数调用没有返回任何行。

    演示

    创建一个不可内联的函数作为演示:

    CREATE OR REPLACE FUNCTION my_func(integer)
    RETURNS TABLE(a integer, b integer, c integer) AS $$
    BEGIN
        RAISE NOTICE 'my_func(%)',$1;
        RETURN QUERY SELECT $1, $1, $1;
    END;
    $$ LANGUAGE plpgsql;
    

    还有一个虚拟数据表:

    CREATE TABLE some_table AS SELECT x FROM generate_series(1,10) x;
    

    然后尝试上述版本。你会看到第一个每次调用都会引发三个通知;后者只加一个。

    为什么?

    好问题。太可怕了。

    看起来像:

    (func(x)).*
    

    扩展为:

    (my_func(x)).i, (func(x)).j, (func(x)).k, (func(x)).l
    

    在解析中,根据debug_print_parsedebug_print_rewrittendebug_print_plan看一下。 (修剪后的)解析树如下所示:

       :targetList (
          {TARGETENTRY 
          :expr 
             {FIELDSELECT 
             :arg 
                {FUNCEXPR 
                :funcid 57168 
                     ...
                }
             :fieldnum 1 
             :resulttype 23 
             :resulttypmod -1 
             :resultcollid 0
             }
          :resno 1 
          :resname i 
           ...
          }
          {TARGETENTRY 
          :expr 
             {FIELDSELECT 
             :arg 
                {FUNCEXPR 
                :funcid 57168 
                     ...
                }
             :fieldnum 2 
             :resulttype 20 
             :resulttypmod -1 
             :resultcollid 0
             }
          :resno 2 
          :resname j 
           ...
          }
          {TARGETENTRY 
          :expr 
             {FIELDSELECT 
             :arg 
                {FUNCEXPR 
                :funcid 57168 
                 ...
                }
             :fieldnum 3 
             :...
             }
          :resno 3 
          :resname k 
           ...
          }
          {TARGETENTRY 
          :expr 
             {FIELDSELECT 
             :arg 
                {FUNCEXPR 
                :funcid 57168 
                 ...
                }
             :fieldnum 4 
              ...
             }
          :resno 4 
          :resname l 
           ...
          }
       )
    

    所以基本上,我们使用了一个愚蠢的解析器黑客来通过克隆节点来扩展通配符。

    【讨论】:

    • 它必须是 LEFT JOIN LATERAL my_func(some_table.x) AS mf ON true 才能成为替代品。否则,如果函数调用没有返回任何行,我们可能会丢失行,因为 LATERAL 默认为 INNER JOIN
    猜你喜欢
    • 1970-01-01
    • 2020-09-22
    • 1970-01-01
    • 2018-01-29
    • 1970-01-01
    • 2019-07-16
    • 2010-10-03
    • 1970-01-01
    • 2016-07-04
    相关资源
    最近更新 更多