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

Alternativa dinâmica para pivotar com CASE e GROUP BY


Se você não instalou o módulo adicional tablefunc , execute este comando uma vez por banco de dados:
CREATE EXTENSION tablefunc;

Resposta à pergunta


Uma solução de crosstab muito básica para o seu caso:
SELECT * FROM crosstab(
  'SELECT bar, 1 AS cat, feh
   FROM   tbl_org
   ORDER  BY bar, feh')
 AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?

A dificuldade especial aqui está, que não há categoria (cat ) na tabela base. Para o formulário de 1 parâmetro básico podemos apenas fornecer uma coluna fictícia com um valor fictício servindo como categoria. O valor é ignorado de qualquer maneira.

Este é um dos casos raros onde o segundo parâmetro para o crosstab() função não é necessária , porque todos os NULL os valores só aparecem em colunas pendentes à direita por definição deste problema. E o pedido pode ser determinado pelo valor .

Se tivéssemos uma categoria real coluna com nomes determinando a ordem dos valores no resultado, precisaríamos do formulário de 2 parâmetros de crosstab() . Aqui eu sintetizo uma coluna de categoria com a ajuda da função de janela row_number() , para basear crosstab() em:
SELECT * FROM crosstab(
   $$
   SELECT bar, val, feh
   FROM  (
      SELECT *, 'val' || row_number() OVER (PARTITION BY bar ORDER BY feh) AS val
      FROM tbl_org
      ) x
   ORDER BY 1, 2
   $$
 , $$VALUES ('val1'), ('val2'), ('val3')$$         -- more columns?
) AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?

O resto é muito comum. Encontre mais explicações e links nestas respostas intimamente relacionadas.

Noções básicas:
Leia isto primeiro se você não estiver familiarizado com o crosstab() função!
  • Consulta de tabela cruzada PostgreSQL

Avançado:
  • Pivotar em várias colunas usando Tablefunc
  • Mesclar uma tabela e um log de alterações em uma visualização no PostgreSQL

Configuração de teste adequada


É assim que você deve fornecer um caso de teste para começar:
CREATE TEMP TABLE tbl_org (id int, feh int, bar text);
INSERT INTO tbl_org (id, feh, bar) VALUES
   (1, 10, 'A')
 , (2, 20, 'A')
 , (3,  3, 'B')
 , (4,  4, 'B')
 , (5,  5, 'C')
 , (6,  6, 'D')
 , (7,  7, 'D')
 , (8,  8, 'D');

Reta cruzada dinâmica?


Não muito dinâmico , ainda, como comentou @Clodoaldo. Tipos de retorno dinâmico são difíceis de alcançar com plpgsql. Mas existem maneiras de contornar isso - com algumas limitações .

Então, para não complicar ainda mais o resto, demonstro com um método mais simples caso de teste:
CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);

Ligar:
SELECT * FROM crosstab('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2')
AS ct (row_name text, val1 int, val2 int, val3 int);

Devoluções:
 row_name | val1 | val2 | val3
----------+------+------+------
 A        | 10   | 20   |
 B        |  3   |  4   |
 C        |  5   |      |
 D        |  6   |  7   |  8

Recurso integrado de tablefunc módulo


O módulo tablefunc fornece uma infraestrutura simples para crosstab() genérico chamadas sem fornecer uma lista de definição de coluna. Várias funções escritas em C (normalmente muito rápido):
crosstabN()


crosstab1() - crosstab4() são pré-definidos. Um ponto menor:eles exigem e retornam todo o text . Então, precisamos converter nosso integer valores. Mas simplifica a chamada:
SELECT * FROM crosstab4('SELECT row_name, attrib, val::text  -- cast!
                         FROM tbl ORDER BY 1,2')

Resultado:
 row_name | category_1 | category_2 | category_3 | category_4
----------+------------+------------+------------+------------
 A        | 10         | 20         |            |
 B        | 3          | 4          |            |
 C        | 5          |            |            |
 D        | 6          | 7          | 8          |

crosstab() personalizado função


Para mais colunas ou outros tipos de dados , criamos nosso próprio tipo composto e função (uma vez).
Tipo:
CREATE TYPE tablefunc_crosstab_int_5 AS (
  row_name text, val1 int, val2 int, val3 int, val4 int, val5 int);

Função:
CREATE OR REPLACE FUNCTION crosstab_int_5(text)
  RETURNS SETOF tablefunc_crosstab_int_5
AS '$libdir/tablefunc', 'crosstab' LANGUAGE c STABLE STRICT;

Ligar:
SELECT * FROM crosstab_int_5('SELECT row_name, attrib, val   -- no cast!
                              FROM tbl ORDER BY 1,2');

Resultado:
 row_name | val1 | val2 | val3 | val4 | val5
----------+------+------+------+------+------
 A        |   10 |   20 |      |      |
 B        |    3 |    4 |      |      |
 C        |    5 |      |      |      |
 D        |    6 |    7 |    8 |      |

Um função polimórfica e dinâmica para todos


Isso vai além do que é coberto pelo tablefunc módulo.
Para tornar o tipo de retorno dinâmico, uso um tipo polimórfico com uma técnica detalhada nesta resposta relacionada:
  • Refatorar uma função PL/pgSQL para retornar a saída de várias consultas SELECT



Formulário de 1 parâmetro:
CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L) t(%s)'
                , _qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;

Sobrecarregue com esta variante para o formulário de 2 parâmetros:
CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _cat_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L, %L) t(%s)'
                , _qry, _cat_qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;

pg_typeof(_rowtype)::text::regclass :Existe um tipo de linha definido para cada tipo composto definido pelo usuário, para que os atributos (colunas) sejam listados no catálogo do sistema pg_attribute . O caminho mais rápido para obtê-lo:converta o tipo registrado (regtype ) para text e converta este text para regclass .

Crie tipos compostos uma vez:


Você precisa definir uma vez cada tipo de retorno que você vai usar:
CREATE TYPE tablefunc_crosstab_int_3 AS (
    row_name text, val1 int, val2 int, val3 int);

CREATE TYPE tablefunc_crosstab_int_4 AS (
    row_name text, val1 int, val2 int, val3 int, val4 int);

...

Para chamadas ad hoc, você também pode criar uma tabela temporária para o mesmo efeito (temporário):
CREATE TEMP TABLE temp_xtype7 AS (
    row_name text, x1 int, x2 int, x3 int, x4 int, x5 int, x6 int, x7 int);

Ou use o tipo de tabela, visualização ou visualização materializada existente, se disponível.

Ligar


Usando os tipos de linha acima:

Formulário de 1 parâmetro (sem valores ausentes):
SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1,2'
 , NULL::tablefunc_crosstab_int_3);

Formulário de 2 parâmetros (alguns valores podem estar faltando):
SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1'
 , $$VALUES ('val1'), ('val2'), ('val3')$$
 , NULL::tablefunc_crosstab_int_3);

Esta uma função funciona para todos os tipos de retorno, enquanto o crosstabN() framework fornecido pelo tablefunc O módulo precisa de uma função separada para cada um.
Se você nomeou seus tipos em sequência como demonstrado acima, você só precisa substituir o número em negrito. Para encontrar o número máximo de categorias na tabela base:
SELECT max(count(*)) OVER () FROM tbl  -- returns 3
GROUP  BY row_name
LIMIT  1;

Isso é o mais dinâmico possível se você quiser colunas individuais . Arrays como demonstrado por @Clocoaldo ou uma representação de texto simples ou o resultado envolvido em um tipo de documento como json ou hstore pode trabalhar para qualquer número de categorias dinamicamente.

Isenção de responsabilidade:
É sempre potencialmente perigoso quando a entrada do usuário é convertida em código. Certifique-se de que isso não pode ser usado para injeção de SQL. Não aceite entrada de usuários não confiáveis ​​(diretamente).

Chamada para pergunta original:

SELECT * FROM crosstab_n('SELECT bar, 1, feh FROM tbl_org ORDER BY 1,2'
                       , NULL::tablefunc_crosstab_int_3);