如何避免SQL查询中的(func())。*语法的多个函数evals?

上下文

当一个函数返回一个TABLE或一个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; 

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

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

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

2) select func(3)只会产生一个ROWtypes的输出列。

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

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

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

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

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

或者在这个相关的答案 。 如果我们有了LATERAL JOIN我们可以使用它,但是直到PostgreSQL 9.3出来,它不被支持,以前的版本仍然会被使用多年。

问题

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

注意:要求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 |  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的语法或构造可以通过只做最小的函数调用来达到预期的效果?

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

你可以将它包装在一个子查询中,但是如果没有OFFSET 0攻击,这并不能保证安全。 在9.3中,使用LATERAL 。 问题是由parsing器有效地将macros扩展到列表中引起的。

解决方法

哪里:

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

my_func n n结果列n次评估my_func ,此expression式为:

 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保留原始查询的所有行,即使函数调用不返回任何行。

演示

创build一个不能作为示例进行embedded的函数:

 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 

在parsing中,根据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 ... } ) 

所以基本上,我们正在使用愚蠢的parsing器破解克隆节点来扩展通配符。