Consulta de tabela de referência cruzada do PostgreSQL

196

Alguém sabe como criar consultas de referência cruzada no PostgreSQL?
Por exemplo, tenho a seguinte tabela:

Section    Status    Count
A          Active    1
A          Inactive  2
B          Active    4
B          Inactive  5

Gostaria que a consulta retornasse a seguinte tabela de referência cruzada:

Section    Active    Inactive
A          1         2
B          4         5

Isso é possível?

schone
fonte
1
Como eu tinha uma estrutura um pouco diferente e achei esse exemplo um pouco difícil de entender, documentei minha maneira de pensar sobre esse stackoverflow.com/q/49051959/808723 . Talvez seja útil para qualquer pessoa.
GameScripting

Respostas:

317

Instale o módulo adicional tablefunc uma vez por banco de dados, que fornece a função crosstab(). Desde o Postgres 9.1, você pode usar CREATE EXTENSIONpara isso:

CREATE EXTENSION IF NOT EXISTS tablefunc;

Caso de teste aprimorado

CREATE TABLE tbl (
   section   text
 , status    text
 , ct        integer  -- "count" is a reserved word in standard SQL
);

INSERT INTO tbl VALUES 
  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                    , ('C', 'Inactive', 7);  -- ('C', 'Active') is missing

Formulário simples - não adequado para atributos ausentes

crosstab(text)com 1 parâmetro de entrada:

SELECT *
FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- needs to be "ORDER BY 1,2" here
   ) AS ct ("Section" text, "Active" int, "Inactive" int);

Devoluções:

Seção Ativo Inativo
--------- + -------- + ----------
 A 1 | 2
 B 4 | 5
 C      7 | - !!
  • Não há necessidade de transmitir e renomear.
  • Observe o resultado incorreto para C: o valor 7é preenchido para a primeira coluna. Às vezes, esse comportamento é desejável, mas não para esse caso de uso.
  • O formulário simples também é limitado a exatamente três colunas na consulta de entrada fornecida: row_name , categoria , valor . Não há espaço para colunas extras, como na alternativa de 2 parâmetros abaixo.

Formulário seguro

crosstab(text, text)com 2 parâmetros de entrada:

SELECT *
FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- could also just be "ORDER BY 1" here

  , $$VALUES ('Active'::text), ('Inactive')$$
   ) AS ct ("Section" text, "Active" int, "Inactive" int);

Devoluções:

Seção Ativo Inativo
--------- + -------- + ----------
 A 1 | 2
 B 4 | 5
 C |        7   - !!
  • Observe o resultado correto para C.

  • O segundo parâmetro pode ser qualquer consulta que retorne uma linha por atributo que corresponda à ordem da definição da coluna no final. Freqüentemente, você deseja consultar atributos distintos da tabela subjacente como esta:

    'SELECT DISTINCT attribute FROM tbl ORDER BY 1'

    Isso está no manual.

    Como você precisa digitar todas as colunas em uma lista de definições de coluna de qualquer maneira (exceto as variantes predefinidas ), geralmente é mais eficiente fornecer uma lista curta em uma expressão como demonstrada:crosstabN()VALUES

    $$VALUES ('Active'::text), ('Inactive')$$)

    Ou (não no manual):

    $$SELECT unnest('{Active,Inactive}'::text[])$$  -- short syntax for long lists
  • Eu usei cotação em dólar para facilitar a cotação.

  • Você pode até produzir colunas com diferentes tipos de dados com crosstab(text, text)- desde que a representação de texto da coluna de valor seja uma entrada válida para o tipo de destino. Desta forma, você pode ter os atributos de diferentes tipos e saída text, date, numericetc. para os respectivos atributos. Há um exemplo de código no final do capítulo crosstab(text, text)no manual .

db <> mexer aqui

Exemplos avançados


\crosstabview no psql

O Postgres 9.6 adicionou esse meta-comando ao seu terminal interativo padrão psql . Você pode executar a consulta que usaria como primeiro crosstab()parâmetro e alimentá-la \crosstabview(imediatamente ou na próxima etapa). Gostar:

db=> SELECT section, status, ct FROM tbl \crosstabview

Resultado semelhante ao descrito acima, mas é um recurso de representação exclusivamente do lado do cliente . As linhas de entrada são tratadas de maneira ligeiramente diferente, portanto, ORDER BYnão é necessário. Detalhes para \crosstabviewno manual. Existem mais exemplos de código na parte inferior dessa página.

Resposta relacionada no dba.SE de Daniel Vérité (o autor do recurso psql):



A resposta aceita anteriormente está desatualizada.

  • A variante da função crosstab(text, integer)está desatualizada. O segundo integerparâmetro é ignorado. Cito o manual atual :

    crosstab(text sql, int N) ...

    Versão obsoleta de crosstab(text). O parâmetro Nagora é ignorado, pois o número de colunas de valor é sempre determinado pela consulta de chamada

  • Fundição e renomeação desnecessárias.

  • Falha se uma linha não tiver todos os atributos. Consulte a variante segura com dois parâmetros de entrada acima para lidar adequadamente com os atributos ausentes.

  • ORDER BYé necessário na forma de um parâmetro de crosstab(). O manual:

    Na prática, a consulta SQL sempre deve especificar ORDER BY 1,2para garantir que as linhas de entrada sejam ordenadas corretamente

Erwin Brandstetter
fonte
3
+1, bom writeup, obrigado por perceberIn practice the SQL query should always specify ORDER BY 1,2 to ensure that the input rows are properly ordered
ChristopheD
Estou com alguns problemas ao usar $$ VALUES .. $$. Em vez disso, usei 'VALUES (' '<attr>' ':: <type>), ..'
Marco Fantasia
Podemos especificar a ligação de parâmetro na consulta de tabela de referência cruzada? Estou recebendo este erro => não foi possível determinar o tipo de dados do parâmetro $ 2
Ashish
1
É possível definir o valor padrão para a coluna na consulta de tabela de referência cruzada?
Ashish
2
@ Ashish: Por favor, inicie uma nova pergunta. Comentários não são o lugar. Você sempre pode vincular a este para contextualizar.
Erwin Brandstetter
30

Você pode usar a crosstab()função do módulo adicional tablefunc - que você deve instalar uma vez por banco de dados. Desde o PostgreSQL 9.1, você pode usar CREATE EXTENSIONpara isso:

CREATE EXTENSION tablefunc;

No seu caso, acredito que seria algo como isto:

CREATE TABLE t (Section CHAR(1), Status VARCHAR(10), Count integer);

INSERT INTO t VALUES ('A', 'Active',   1);
INSERT INTO t VALUES ('A', 'Inactive', 2);
INSERT INTO t VALUES ('B', 'Active',   4);
INSERT INTO t VALUES ('B', 'Inactive', 5);

SELECT row_name AS Section,
       category_1::integer AS Active,
       category_2::integer AS Inactive
FROM crosstab('select section::text, status, count::text from t',2)
            AS ct (row_name text, category_1 text, category_2 text);
Jeremiah Peschka
fonte
Caso você use um parâmetro na consulta de tabela de referência cruzada, será necessário escapá-lo corretamente. Exemplo: (de cima) diga que você deseja apenas os ativos: SELECT ... FROM tabela de referência cruzada ('selecione a seção :: texto, status, count :: texto de t onde status =' 'ativo' '', 2) AS. .. (observe as aspas duplas). Caso o parâmetro seja passado em tempo de execução pelo usuário (como um parâmetro de função, por exemplo), você pode dizer: SELECT ... FROM tabela de referência cruzada ('select section :: text, status, count :: text from t where status =' ' '|| par_active ||' '' ', 2) AS ... (aspas triplas aqui!). No BIRT, isso também funciona com o? espaço reservado.
Wim Verhavert
26
SELECT section,
       SUM(CASE status WHEN 'Active' THEN count ELSE 0 END) AS active, --here you pivot each status value as a separate column explicitly
       SUM(CASE status WHEN 'Inactive' THEN count ELSE 0 END) AS inactive --here you pivot each status  value as a separate column explicitly

FROM t
GROUP BY section
araqnid
fonte
1
Alguém pode explicar o que a função de tabela de referência cruzada no módulo tablefunc adiciona a essa resposta, que faz o trabalho em mãos e, na minha opinião, é mais fácil de entender?
John John Powell
4
@ JohnBarça: Um caso simples como esse pode ser facilmente resolvido com instruções CASE. No entanto, isso torna-se pesado rapidamente, com mais atributos e / ou outros tipos de dados do que apenas números inteiros. Como um aparte: este formulário usa a função agregada sum(), seria melhor usar min()or max()e no ELSEque funciona texttambém. Mas isso tem efeitos sutilmente diferentes de corosstab(), que usa apenas o "primeiro" valor por atributo. Não importa, desde que só possa haver um. Finalmente, o desempenho também é relevante. crosstab()é escrito em C e otimizado para a tarefa.
Erwin Brandstetter
Isso não funciona para mim, para o postgresql. Eu recebo o erroERROR: 42803: aggregate function calls may not be nested
Audrey
1
@ Audrey você não está executando o mesmo SQL então?
2
Considere adicionar explicação vs apenas um bloco de código
Daniel L. Vandenbosch
10

Solução com agregação JSON:

CREATE TEMP TABLE t (
  section   text
, status    text
, ct        integer  -- don't use "count" as column name.
);

INSERT INTO t VALUES 
  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                   , ('C', 'Inactive', 7); 


SELECT section,
       (obj ->> 'Active')::int AS active,
       (obj ->> 'Inactive')::int AS inactive
FROM (SELECT section, json_object_agg(status,ct) AS obj
      FROM t
      GROUP BY section
     )X
Milos
fonte
Obrigado, isso me ajudou com um problema relacionado.
JeffCharter
1

Desculpe, isso não está completo porque não posso testá-lo aqui, mas pode levá-lo na direção certa. Estou traduzindo de algo que uso que faz uma consulta semelhante:

select mt.section, mt1.count as Active, mt2.count as Inactive
from mytable mt
left join (select section, count from mytable where status='Active')mt1
on mt.section = mt1.section
left join (select section, count from mytable where status='Inactive')mt2
on mt.section = mt2.section
group by mt.section,
         mt1.count,
         mt2.count
order by mt.section asc;

O código do qual estou trabalhando é:

select m.typeID, m1.highBid, m2.lowAsk, m1.highBid - m2.lowAsk as diff, 100*(m1.highBid - m2.lowAsk)/m2.lowAsk as diffPercent
from mktTrades m
   left join (select typeID,MAX(price) as highBid from mktTrades where bid=1 group by typeID)m1
   on m.typeID = m1.typeID
   left join (select typeID,MIN(price) as lowAsk  from mktTrades where bid=0 group by typeID)m2
   on m1.typeID = m2.typeID
group by m.typeID, 
         m1.highBid, 
         m2.lowAsk
order by diffPercent desc;

que retornará um typeID, o lance de preço mais alto e o preço mais baixo solicitado e a diferença entre os dois (uma diferença positiva significaria que algo poderia ser comprado por menos do que pode ser vendido).

LanceH
fonte
1
Está faltando uma cláusula from, caso contrário, isso está correto. Os planos de explicação são muito diferentes no meu sistema - a função de tabela de referência cruzada tem um custo de 22,5 enquanto a abordagem LEFT JOIN é cerca de 4 vezes mais cara, com um custo de 91,38. Também produz cerca de duas vezes mais leituras físicas e realiza junções de hash - o que pode ser bastante caro comparado a outros tipos de junção.
Jeremiah Peschka
Obrigado Jeremiah, é bom saber. Votou a outra resposta, mas vale a pena manter seu comentário, para não excluir esta.
LanceH
-1

Crosstab A função está disponível sob o tablefunc extensão Você precisará criar essa extensão uma vez para o banco de dados.

CRIAR EXTENSÃO tablefunc;

Você pode usar o código abaixo para criar tabela dinâmica usando a tabulação cruzada:

create table test_Crosstab( section text,
<br/>status text,
<br/>count numeric)

<br/>insert into test_Crosstab values ( 'A','Active',1)
                <br/>,( 'A','Inactive',2)
                <br/>,( 'B','Active',4)
                <br/>,( 'B','Inactive',5)

select * from crosstab(
<br/>'select section
    <br/>,status
    <br/>,count
    <br/>from test_crosstab'
    <br/>)as ctab ("Section" text,"Active" numeric,"Inactive" numeric)
Lekshmi Kurup
fonte
1
Esta resposta não acrescenta nada sobre as respostas pré-existentes.
Erwin Brandstetter