PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Refatorar uma função PL/pgSQL para retornar a saída de várias consultas SELECT

SQL Dinâmico e RETURN tipo



Você deseja executar SQL dinâmico . Em princípio, isso é simples em plpgsql com a ajuda de EXECUTE . Você não precisa um cursor. Na verdade, na maioria das vezes você fica melhor sem cursores explícitos.

O problema que você encontra:você deseja retornar registros de tipo ainda indefinido . Uma função precisa declarar seu tipo de retorno no RETURNS cláusula (ou com OUT ou INOUT parâmetros). No seu caso, você teria que recorrer a registros anônimos, porque número , nomes e tipos de colunas retornadas variam. Como:
CREATE FUNCTION data_of(integer)
  RETURNS SETOF record AS ...

No entanto, isso não é particularmente útil. Você precisa fornecer uma lista de definição de coluna com cada chamada. Como:
SELECT * FROM data_of(17)
AS foo (colum_name1 integer
      , colum_name2 text
      , colum_name3 real);

Mas como você faria isso, se você não conhece as colunas de antemão?
Você poderia usar tipos de dados de documentos menos estruturados, como json , jsonb , hstore ou xml . Ver:
  • Como armazenar uma tabela de dados no banco de dados?

Mas, para o propósito desta pergunta, vamos supor que você deseja retornar colunas individuais, digitadas e nomeadas corretamente, tanto quanto possível.

Solução simples com tipo de retorno fixo


A coluna datahora parece ser um dado, vou assumir o tipo de dados timestamp e que sempre há mais duas colunas com nomes e tipos de dados variados.

Nomes abandonaremos em favor de nomes genéricos no tipo de retorno.
Tipos também abandonaremos e converteremos tudo em text uma vez que todo tipo de dados pode ser convertido em text .
CREATE OR REPLACE FUNCTION data_of(_id integer)
  RETURNS TABLE (datahora timestamp, col2 text, col3 text)
  LANGUAGE plpgsql 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$;

As variáveis ​​_sensors e _type poderia ser parâmetros de entrada em vez disso.

Observe a RETURNS TABLE cláusula.

Observe o uso de RETURN QUERY EXECUTE . Essa é uma das maneiras mais elegantes de retornar linhas de uma consulta dinâmica.

Eu uso um nome para o parâmetro da função, apenas para fazer o USING cláusula de RETURN QUERY EXECUTE menos confuso. $1 na string SQL não se refere ao parâmetro da função, mas ao valor passado com o USING cláusula. (Ambos são $1 em seu respectivo escopo neste exemplo simples.)

Observe o valor de exemplo para _sensors :cada coluna é convertida para digitar text .

Esse tipo de código é muito vulnerável à injeção de SQL . Eu uso quote_ident() para se proteger contra isso. Agrupando alguns nomes de coluna na variável _sensors impede o uso de quote_ident() (e normalmente é uma má ideia!). Certifique-se de que nenhuma coisa ruim possa estar lá de outra maneira, por exemplo, executando individualmente os nomes das colunas por meio de quote_ident() em vez de. Uma VARIADIC parâmetro vem à mente ...

Mais simples desde o PostgreSQL 9.1


Com a versão 9.1 ou posterior você pode usar format() para simplificar ainda mais:
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;

Novamente, nomes de colunas individuais poderiam ser escapados corretamente e seriam a maneira mais limpa.

Número variável de colunas que compartilham o mesmo tipo


Depois que sua pergunta for atualizada, parece que seu tipo de retorno foi
  • uma variável número de colunas
  • mas todas as colunas do mesmo tipo double precision (alias float8 )

Use um ARRAY digite neste caso para aninhar um número variável de valores. Além disso, retorno um array com nomes de colunas:
CREATE OR REPLACE FUNCTION data_of(_id integer)
  RETURNS TABLE (datahora timestamp, names text[], values float8[])
  LANGUAGE plpgsql 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$;

Vários tipos de tabelas completas


Para retornar todas as colunas de uma tabela , existe uma solução simples e poderosa usando um tipo polimórfico :
CREATE OR REPLACE FUNCTION data_of(_tbl_type anyelement, _id int)
  RETURNS SETOF anyelement
  LANGUAGE plpgsql 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$;

Ligue (importante!):
SELECT * FROM data_of(NULL::pcdmet, 17);

Substitua pcdmet na chamada com qualquer outro nome de tabela.

Como isso funciona?


anyelement é um tipo de dados pseudo, um tipo polimórfico, um espaço reservado para qualquer tipo de dados não array. Todas as ocorrências de anyelement na função avaliar para o mesmo tipo fornecido em tempo de execução. Ao fornecer um valor de um tipo definido como argumento para a função, definimos implicitamente o tipo de retorno.

O PostgreSQL define automaticamente um tipo de linha (um tipo de dado composto) para cada tabela criada, portanto, há um tipo bem definido para cada tabela. Isso inclui tabelas temporárias, o que é conveniente para uso ad hoc.

Qualquer tipo pode ser NULL . Entregue um NULL valor, convertido para o tipo de tabela:NULL::pcdmet .

Agora a função retorna um tipo de linha bem definido e podemos usar SELECT * FROM data_of() para decompor a linha e obter colunas individuais.

pg_typeof(_tbl_type) retorna o nome da tabela como identificador de objeto tipo regtype . Quando convertido automaticamente para text , os identificadores são automaticamente entre aspas duplas e qualificados pelo esquema se necessário, defendendo-se contra injeção de SQL automaticamente. Isso pode até mesmo lidar com nomes de tabela qualificados pelo esquema onde quote_ident() falharia. Ver:
  • Nome da tabela como parâmetro de função do PostgreSQL