执行动态交叉表查询

时间:2016-04-22 22:57:30

标签: postgresql plpgsql dynamic-sql crosstab

我在Postgres数据库中实现了这个功能:http://www.cureffi.org/2013/03/19/automatically-creating-pivot-table-column-names-in-postgresql/

这里的功能是:

create or replace function xtab (tablename varchar, rowc varchar, colc varchar, cellc varchar, celldatatype varchar) returns varchar language plpgsql as $$
declare
    dynsql1 varchar;
    dynsql2 varchar;
    columnlist varchar;
begin
    -- 1. retrieve list of column names.
    dynsql1 = 'select string_agg(distinct '||colc||'||'' '||celldatatype||''','','' order by '||colc||'||'' '||celldatatype||''') from '||tablename||';';
    execute dynsql1 into columnlist;
    -- 2. set up the crosstab query
    dynsql2 = 'select * from crosstab (
 ''select '||rowc||','||colc||','||cellc||' from '||tablename||' group by 1,2 order by 1,2'',
 ''select distinct '||colc||' from '||tablename||' order by 1''
 )
 as ct (
 '||rowc||' varchar,'||columnlist||'
 );';
    return dynsql2;
end
$$;

所以现在我可以调用函数:

select xtab('globalpayments','month','currency','(sum(total_fees)/sum(txn_amount)*100)::decimal(48,2)','text');

返回(因为函数的返回类型是varchar):

select * from crosstab (
   'select month,currency,(sum(total_fees)/sum(txn_amount)*100)::decimal(48,2)
    from globalpayments
    group by 1,2
    order by 1,2'
 , 'select distinct currency
    from globalpayments
    order by 1'
   )  as ct ( month varchar,CAD text,EUR text,GBP text,USD text );

如何让这个函数不仅生成动态交叉表的代码,还能执行结果?即,我手动复制/粘贴/执行的结果就是这个。但是我想让它在没有额外步骤的情况下执行:函数应该组装动态查询执行它:

query result

修改1

此功能接近,但我需要它返回的不仅仅是第一条记录的第一列

取自:Are there any way to execute a query inside the string value (like eval) in PostgreSQL?

create or replace function eval( sql  text ) returns text as $$
declare
  as_txt  text;
begin
  if  sql is null  then  return null ;  end if ;
  execute  sql  into  as_txt ;
  return  as_txt ;
end;
$$ language plpgsql

用法:select * from eval($$select * from analytics limit 1$$)

然而,它只返回第一条记录的第一列:

eval
----
2015

当实际结果如下:

Year, Month, Date, TPV_USD
---- ----- ------ --------
2016, 3, 2016-03-31, 100000

2 个答案:

答案 0 :(得分:8)

您要求的是 不可能 。 SQL是一种严格类型的语言。 PostgreSQL函数需要在创建时声明一个返回类型(RETURNS ..)。

有限的方法是使用多态函数。如果您可以在调用 功能的时提供退货类型。但是你的问题并不明显。

可以使用匿名记录返回完全动态的结果。但是,您需要为每次调用提供列定义列表。你怎么知道返回的列?赶上22。

根据您的需要或可以使用的方法,有各种解决方法。由于您的所有数据列似乎共享相同的数据类型,因此我建议您返回 数组 text[]。或者,您可以返回hstorejson等文档类型。相关:

但是使用两个调用可能更简单:1:让Postgres构建查询。 2:执行并检索返回的行。

我不会使用Eric Minikel中的函数,如你在所有中提出的那样。通过恶意格式错误的标识符来防止SQL注入是不安全的。使用format()构建查询字符串,除非您运行的是比Postgres 9.1更旧的过时版本。

更短更清洁的实现可能如下所示:

CREATE OR REPLACE FUNCTION xtab(_tbl regclass, _row text, _cat text
                              , _expr text  -- still vulnerable to SQL injection!
                              , _type regtype)
  RETURNS text AS
$func$
DECLARE
   _cat_list text;
   _col_list text;
BEGIN

-- generate categories for xtab param and col definition list    
EXECUTE format(
 $$SELECT string_agg(quote_literal(x.cat), '), (')
        , string_agg(quote_ident  (x.cat), %L)
   FROM  (SELECT DISTINCT %I AS cat FROM %s ORDER BY 1) x$$
 , ' ' || _type || ', ', _cat, _tbl)
INTO  _cat_list, _col_list;

-- generate query string
RETURN format(
'SELECT * FROM crosstab(
   $q$SELECT %I, %I, %s
      FROM   %I
      GROUP  BY 1, 2  -- only works if the 3rd column is an aggregate expression
      ORDER  BY 1, 2$q$
 , $c$VALUES (%5$s)$c$
   ) ct(%1$I text, %6$s %7$s)'
, _row, _cat, _expr  -- expr must be an aggregate expression!
, _tbl, _cat_list, _col_list, _type
);

END
$func$ LANGUAGE plpgsql;

与原始版本相同的函数调用。函数crosstab()由必须安装的附加模块tablefunc提供。基本信息:

这会安全地处理列名和表名。请注意对象标识符类型regclassregtype的使用。也适用于模式限定名称。

但是,在原始查询中传递要作为表达式_expr - cellc执行的字符串时, 不完全安全 )。这种输入本身对SQL注入不安全,不应该暴露给普通公众。

对于两个类别列表,仅扫描一次表,并且应该更快一些。

仍然无法返回完全动态的行类型,因为这绝对不可能。

答案 1 :(得分:2)

并非完全不可能,你仍然可以执行它(从查询执行字符串并返回SETOF RECORD。

然后你必须指定返回记录格式。在这种情况下,原因是规划人员需要知道返回格式才能做出某些决定(想象实现)。

因此,在这种情况下,您将执行查询,返回行并返回SETOF RECORD。

例如,我们可以使用包装函数执行类似的操作,但可以将相同的逻辑折叠到您的函数中:

CREATE OR REPLACE FUNCTION crosstab_wrapper
(tablename varchar, rowc varchar, colc varchar, 
 cellc varchar, celldatatype varchar) 
returns setof record language plpgsql as $$
    DECLARE outrow record;
    BEGIN
       FOR outrow IN EXECUTE xtab($1, $2, $3, $4, $5)
       LOOP
           RETURN NEXT outrow
       END LOOP;
    END;
 $$;

然后在调用函数时提供记录结构,就像使用交叉表一样。 然后当你所有的查询时,你必须提供一个记录结构(如(col1类型,col2类型等),就像你使用connectby。