如何在SQL查询中使用(func()).*语法避免多重函数的演绎?

Dan*_*ité 14 postgresql user-defined-functions

上下文

当函数返回a TABLE或a时SETOF 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;
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

 func  
-------
 (1,3)
 (2,9)
 (3,27)

3)select (func(3)).*会产生#1:

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

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

select N, (func(N)).* from (select 2 as N union select 3 as N) s;
Run Code Online (Sandbox Code Playgroud)

或者在这个相关的答案中.如果我们LATERAL JOIN可以使用它,但是在PostgreSQL 9.3出来之前,它不受支持,并且以前的版本仍将使用多年.

问题

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

NOTICE:  called for 2
NOTICE:  called for 2
NOTICE:  called for 3
NOTICE:  called for 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;
Run Code Online (Sandbox Code Playgroud)

然后相同的查询输出:

NOTICE:  called for 2
NOTICE:  called for 2
NOTICE:  called for 2
NOTICE:  called for 2
NOTICE:  called for 3
NOTICE:  called for 3
NOTICE:  called for 3
NOTICE:  called for 3

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

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

select N,func(N) from (select 2 as N union select 3 as N) s;
Run Code Online (Sandbox Code Playgroud)

得到:

NOTICE:  called for 2
NOTICE:  called for 3

然后是6个结果行:

 n |    func    
---+------------
 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的结构,通过只做最小的所需函数调用来实现预期的结果?

奖金问题:为什么多重评估会发生?

Cra*_*ger 18

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

解决方法

哪里:

SELECT (my_func(x)).* FROM some_table;
Run Code Online (Sandbox Code Playgroud)

将评估函数的结果列的my_func n时间n,这个公式:

SELECT (mf).* FROM (
    SELECT my_func(x) AS mf FROM some_table
) sub;
Run Code Online (Sandbox Code Playgroud)

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

SELECT (mf).* FROM (
    SELECT my_func(x) AS mf FROM some_table OFFSET 0
) sub;
Run Code Online (Sandbox Code Playgroud)

要么:

WITH tmp(mf) AS (
    SELECT my_func(x) FROM some_table
)
SELECT (mf).* FROM tmp;
Run Code Online (Sandbox Code Playgroud)

在PostgreSQL 9.3中,您可以使用LATERAL以获得更健全的行为:

SELECT mf.*
FROM some_table
LEFT JOIN LATERAL my_func(some_table.x) AS mf ON true;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

和一个虚拟数据表:

CREATE TABLE some_table AS SELECT x FROM generate_series(1,10) x;
Run Code Online (Sandbox Code Playgroud)

然后尝试上面的版本.你会看到第一个每次调用都会引发三个通知; 后者只提出一个.

为什么?

好问题.这太糟糕了.

看起来像:

(func(x)).*
Run Code Online (Sandbox Code Playgroud)

扩展为:

(my_func(x)).i, (func(x)).j, (func(x)).k, (func(x)).l
Run Code Online (Sandbox Code Playgroud)

解析,根据一看debug_print_parse,debug_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 
       ...
      }
   )
Run Code Online (Sandbox Code Playgroud)

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

  • 它必须是`LEFT JOIN LATERAL my_func(some_table.x)as mf ON true`才能替代。否则,如果函数调用未返回任何行,则由于“ LATERAL”默认为“ INNER JOIN”,我们可能会丢失行。 (2认同)