重构PL/pgSQL函数以返回各种SELECT查询的输出

wal*_*.ar 32 sql database postgresql dynamic-sql plpgsql

我写了一个函数,输出一个SELECT以文本形式组成的PostgreSQL 查询.现在我不想再输出文本,但实际上SELECT对数据库运行生成的语句并返回结果 - 就像查询本身一样.

到目前为止我所拥有的:

CREATE OR REPLACE FUNCTION data_of(integer)
  RETURNS text AS
$BODY$
DECLARE
   sensors varchar(100);   -- holds list of column names
   type    varchar(100);   -- holds name of table
   result  text;           -- holds SQL query
       -- declare more variables

BEGIN
      -- do some crazy stuff

      result := 'SELECT\r\nDatahora,' || sensors ||
      '\r\n\r\nFROM\r\n' || type ||
      '\r\n\r\nWHERE\r\id=' || $1 ||'\r\n\r\nORDER BY Datahora;';

      RETURN result;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
ALTER FUNCTION data_of(integer) OWNER TO postgres;
Run Code Online (Sandbox Code Playgroud)

sensors保存表的列名列表type.这些是在功能过程中声明和填写的.最终,他们拥有如下价值观:

  • sensors:'column1, column2, column3'
    Datahora(timestamp)所有列都是类型double precision.

  • type:'myTable'
    可以是四个表之一的名称.除了公共列之外,每个列都有不同的列Datahora.

基础表的定义.

该变量sensors将保存此处显示的相应表中的所有type.例如:如果typepcdmetsensors'datahora,dirvento,precipitacao,pressaoatm,radsolacum,tempar,umidrel,velvento'

变量用于构建SELECT存储在其中的语句result.喜欢:

SELECT Datahora, column1, column2, column3
FROM   myTable
WHERE  id=20
ORDER  BY Datahora;
Run Code Online (Sandbox Code Playgroud)

现在,我的函数返回此语句为text.我复制粘贴并在pgAdmin或psql中执行它.我想自动执行此操作,自动运行查询并返回结果.我怎样才能做到这一点?

Erw*_*ter 74

动态SQL和RETURN类型

(我最后保存了最好的,继续阅读!)
你想要执行动态SQL.原则上,在plpgsql的帮助下,这很简单EXECUTE.你不需要游标 - 实际上,大多数时候你没有明确的游标会更好.
通过搜索在SO上查找示例.

您遇到的问题:您想要返回尚未定义类型的记录.函数需要使用RETURNS子句(或带OUTINOUT参数)声明返回类型.在您的情况下,您将不得不回退到匿名记录,因为返回列的数量,名称类型各不相同.喜欢:

CREATE FUNCTION data_of(integer)
  RETURNS SETOF record AS ...
Run Code Online (Sandbox Code Playgroud)

但是,这不是特别有用.这样,您必须在每次调用函数时提供列定义列表.喜欢:

SELECT * FROM data_of(17)
AS foo (colum_name1 integer
      , colum_name2 text
      , colum_name3 real);
Run Code Online (Sandbox Code Playgroud)

但是,如果你事先不知道这些列,你甚至会怎么做呢?
你可以采取一个不太结构化文档数据类型,例如json,jsonb,hstorexml:

但是出于这个问题的目的,让我们假设你想尽可能多地返回单独的,正确输入和命名的列.

固定返回类型的简单解决方案

该列datahora似乎是给定的,我将假设数据类型timestamp,并且总有两列具有不同的名称和数据类型.

名称我们将放弃支持返回类型中的通用名称.
类型,我们会放弃,也和投所有text,因为每个数据类型可以转换为text.

CREATE OR REPLACE FUNCTION data_of(_id integer)
  RETURNS TABLE (datahora timestamp, col2 text, col3 text) AS
$func$
DECLARE
   _sensors text := 'col1::text, col2::text';  -- cast each col to text
   _type    text := 'foo';
BEGIN
   RETURN QUERY EXECUTE '
      SELECT datahora, ' || _sensors || '
      FROM   ' || quote_ident(_type) || '
      WHERE  id = $1
      ORDER  BY datahora'
   USING  _id;

END
$func$ LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

这是如何运作的?

  • 变量_sensors_type可能的输入参数来代替.

  • 请注意该RETURNS TABLE条款.

  • 注意使用RETURN QUERY EXECUTE.这是从动态查询返回行的更优雅的方法之一.

  • 我使用函数参数的名称,只是为了使USING条款RETURN QUERY EXECUTE更容易混淆.$1在SQL-string中没有引用函数参数,而是引用随该USING子句传递的值.($1在这个简单的例子中,两者恰好都在各自的范围内.)

  • 请注意以下示例值_sensors:将每个列强制转换为类型text.

  • 这种代码非常容易受到SQL注入的攻击.我quote_ident()用来防止它.将变量中的几个列名集中在一起_sensors可以防止使用quote_ident()(并且通常是一个坏主意!).确保没有其他方式存在任何不良内容,例如通过单独运行列名称quote_ident().VARIADIC想到一个参数......

使用PostgreSQL 9.1+更简单

使用9.1或更高版本,您可以使用format()进一步简化:

RETURN QUERY EXECUTE format('
   SELECT datahora, %s  -- identifier passed as unescaped string
   FROM   %I            -- assuming the name is provided by user
   WHERE  id = $1
   ORDER  BY datahora'
  ,_sensors, _type)
USING  _id;
Run Code Online (Sandbox Code Playgroud)

同样,单个列名称可以正确转义,并且将是干净的方式.

可变数量的列共享相同的类型

在您的问题更新后,它看起来像您的返回类型

  • 可变数量的列
  • 但所有相同类型的double precision(别名float8)

因为我们必须定义RETURN一个函数的ARRAY类型,在这种情况下我求助于一个类型,它可以包含可变数量的值.另外,我返回一个包含列名的数组,因此您也可以解析结果中的名称:

CREATE OR REPLACE FUNCTION data_of(_id integer)
  RETURNS TABLE (datahora timestamp, names text[], values float8[] ) AS
$func$
DECLARE
   _sensors text := 'col1, col2, col3';  -- plain list of column names
   _type    text := 'foo';
BEGIN
   RETURN QUERY EXECUTE format('
      SELECT datahora
           , string_to_array($1)  -- AS names
           , ARRAY[%s]            -- AS values
      FROM   %s
      WHERE  id = $2
      ORDER  BY datahora'
    , _sensors, _type)
   USING  _sensors, _id;
END
$func$  LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)


各种完整的表格类型

如果您实际上是在尝试返回表的所​​有列(例如链接页面上的一个,那么请使用这个简单,非常强大的多态类型解决方案:

CREATE OR REPLACE FUNCTION data_of(_tbl_type anyelement, _id int)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE format('
      SELECT *
      FROM   %s  -- pg_typeof returns regtype, quoted automatically
      WHERE  id = $1
      ORDER  BY datahora'
    , pg_typeof(_tbl_type))
   USING  _id;
END
$func$ LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

呼叫:

SELECT * FROM data_of(NULL::pcdmet, 17);
Run Code Online (Sandbox Code Playgroud)

pcdmet使用任何其他表名替换呼叫.

这是如何运作的?

  • anyelement是一种伪数据类型,多态类型,任何非数组数据类型的占位符.anyelement函数中出现的所有内容都计算为运行时提供的相同类型.通过提供定义类型的值作为函数的参数,我们隐式定义了返回类型.

  • PostgreSQL自动为每个创建的表定义行类型(复合数据类型),因此每个表都有一个定义良好的类型.这包括临时表,便于临时使用.

  • 任何类型都可以NULL.所以我们交出一个NULL值,转换为表类型.

  • 现在函数返回一个明确定义的行类型,我们可以SELECT * FROM data_of(...)用来分解行并获取各个列.

  • pg_typeof(_tbl_type)返回表的名称作为对象标识符类型regtype.自动转换为text标识符时,标识符会自动双引号并在需要时进行模式限定.因此,SQL注入是不可能的.这甚至可以处理模式修饰表的名字在那里quote_ident()会失败.