如何避免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 %', n
的RAISE 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_parse
, debug_print_rewritten
和debug_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器破解克隆节点来扩展通配符。