Query a campi incrociati PostgreSQL


196

Qualcuno sa come creare query a campi incrociati in PostgreSQL?
Ad esempio ho la seguente tabella:

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

Vorrei che la query restituisse la seguente tabella a campi incrociati:

Section    Active    Inactive
A          1         2
B          4         5

È possibile?


1
Avevo una struttura leggermente diversa e ho trovato questo esempio un po 'difficile da capire, quindi ho documentato il mio modo di pensare a questo stackoverflow.com/q/49051959/808723 . Forse è utile per chiunque.
GameScript

Risposte:


317

Installa il modulo aggiuntivo tablefunc una volta per database, che fornisce la funzione crosstab(). Da Postgres 9.1 puoi usare CREATE EXTENSIONper questo:

CREATE EXTENSION IF NOT EXISTS tablefunc;

Caso di prova migliorato

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

Modulo semplice: non adatto per gli attributi mancanti

crosstab(text)con 1 parametro di input:

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);

Ritorna:

Sezione | Attivo | Inattivo
--------- + -------- + ----------
 A | 1 | 2
 B | 4 | 5
 C |      7 | - !!
  • Non è necessario il cast e la ridenominazione.
  • Nota il risultato errato per C: il valore 7viene inserito per la prima colonna. A volte, questo comportamento è desiderabile, ma non per questo caso d'uso.
  • Il modulo semplice è inoltre limitato a esattamente tre colonne nella query di input fornita: row_name , categoria , valore . Non c'è spazio per colonne extra come nell'alternativa a 2 parametri di seguito.

Forma sicura

crosstab(text, text)con 2 parametri di input:

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);

Ritorna:

Sezione | Attivo | Inattivo
--------- + -------- + ----------
 A | 1 | 2
 B | 4 | 5
 C | |        7   - !!
  • Nota il risultato corretto per C.

  • Il secondo parametro può essere qualsiasi query che restituisce una riga per attributo corrispondente all'ordine della definizione di colonna alla fine. Spesso vorrai interrogare attributi distinti dalla tabella sottostante in questo modo:

    'SELECT DISTINCT attribute FROM tbl ORDER BY 1'

    Questo è nel manuale.

    Dato che devi comunque precisare tutte le colonne in un elenco di definizioni di colonne (tranne che per le varianti predefinite ), è in genere più efficiente fornire un breve elenco in un'espressione come dimostrato:crosstabN()VALUES

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

    Oppure (non nel manuale):

    $$SELECT unnest('{Active,Inactive}'::text[])$$  -- short syntax for long lists
  • Ho usato la quotazione in dollari per semplificare la quotazione.

  • Si può anche colonne di output con diversi tipi di dati con crosstab(text, text)- fino a quando la rappresentazione del testo della colonna di valore è di input valido per il tipo di destinazione. In questo modo si potrebbe avere gli attributi di diverso tipo e in uscita text, date, numericecc per rispettivi attributi. C'è un esempio di codice alla fine del capitolo crosstab(text, text)nel manuale .

db <> violino qui

Esempi avanzati


\crosstabview in psql

Postgres 9.6 ha aggiunto questo meta-comando al suo terminale interattivo psql predefinito . È possibile eseguire la query da utilizzare come primo crosstab()parametro e alimentarla \crosstabview(immediatamente o nel passaggio successivo). Piace:

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

Risultato simile a quello sopra, ma è esclusivamente una funzione di rappresentazione sul lato client . Le righe di input vengono trattate in modo leggermente diverso, quindi ORDER BYnon è necessario. Dettagli per \crosstabviewnel manuale. Ci sono altri esempi di codice in fondo a quella pagina.

Risposta correlata su dba.SE di Daniel Vérité (l'autore della funzione psql):



La risposta precedentemente accettata è obsoleta.

  • La variante della funzione crosstab(text, integer)è obsoleta. Il secondo integerparametro viene ignorato. Cito il manuale attuale :

    crosstab(text sql, int N) ...

    Versione obsoleta di crosstab(text). Il parametro Nè ora ignorato, poiché il numero di colonne di valori è sempre determinato dalla query chiamante

  • Inutili casting e ridenominazioni.

  • Non riesce se una riga non ha tutti gli attributi. Vedere la variante sicura con due parametri di input sopra per gestire correttamente gli attributi mancanti.

  • ORDER BYè richiesto nella forma di un parametro di crosstab(). Il manuale:

    In pratica, la query SQL deve sempre specificare ORDER BY 1,2per garantire che le righe di input siano ordinate correttamente


3
+1, buon commento, grazie per averlo notatoIn practice the SQL query should always specify ORDER BY 1,2 to ensure that the input rows are properly ordered
ChristopheD

Ho dei problemi con l'utilizzo di $$ VALUES .. $$. Ho usato invece 'VALUES (' '<attr>' ':: <type>), ..'
Marco Fantasia

È possibile specificare l'associazione dei parametri nella query a campi incrociati? Ricevo questo errore => impossibile determinare il tipo di dati del parametro $ 2
Ashish,

1
È possibile impostare il valore predefinito per la colonna nella query a campi incrociati?
Ashish,

2
@Ashish: per favore, fai una nuova domanda. I commenti non sono il posto giusto. Puoi sempre collegarti a questo per il contesto.
Erwin Brandstetter,

30

È possibile utilizzare la crosstab()funzione del modulo aggiuntivo tablefunc , che è necessario installare una volta per database. Da PostgreSQL 9.1 è possibile utilizzare CREATE EXTENSIONper questo:

CREATE EXTENSION tablefunc;

Nel tuo caso, credo che sarebbe simile a questo:

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);

Nel caso in cui si utilizzi un parametro nella query a campi incrociati, è necessario evitarlo correttamente. Esempio: (dall'alto) dire che si desidera solo quelli attivi: SELEZIONARE ... DA tabella a campi incrociati ('selezionare la sezione :: testo, stato, conteggio :: testo da t dove status =' 'attivo' '', 2) AS. .. (notare le doppie virgolette). Nel caso in cui il parametro venga passato in fase di esecuzione dall'utente (come parametro di funzione per esempio) puoi dire: SELEZIONA ... DA tabella a campi incrociati ('seleziona sezione :: testo, stato, conteggio :: testo da t dove stato =' ' '|| par_active ||' '' ', 2) AS ... (virgolette triple qui!). In BIRT funziona anche con? segnaposto.
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

1
Qualcuno può spiegare ciò che la funzione della tabella a campi incrociati nel modulo tablefunc aggiunge a questa risposta, che fa sia il lavoro a portata di mano, che secondo me è più facile da capire?
John Powell,

4
@ JohnBarça: un caso semplice come questo può essere facilmente risolto con le dichiarazioni CASE. Tuttavia, questo diventa ingombrante molto rapidamente con più attributi e / o altri tipi di dati rispetto ai soli numeri interi. A parte questo: questo modulo usa la funzione aggregata sum(), sarebbe meglio usare min()o max()no per ELSEcui funziona textanche. Ma questo ha effetti leggermente diversi rispetto a corosstab(), che utilizza solo il "primo" valore per attributo. Non importa finché ce ne può essere solo uno. Infine, anche le prestazioni sono rilevanti. crosstab()è scritto in C e ottimizzato per l'attività.
Erwin Brandstetter,

Questo non funziona per me, per Postgresql. Ottengo l'erroreERROR: 42803: aggregate function calls may not be nested
Audrey,

1
@Audrey, allora non stai eseguendo lo stesso SQL?

2
Considera di aggiungere una spiegazione a un solo blocco di codice
Daniel L. VanDenBosch,

10

Soluzione con aggregazione 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

Grazie, questo mi ha aiutato con un problema correlato.
JeffCharter,

1

Mi dispiace che non sia completo perché non posso provarlo qui, ma potrebbe portarti nella giusta direzione. Sto traducendo da qualcosa che uso che fa una query simile:

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;

Il codice da cui sto lavorando è:

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;

che restituirà un typeID, l'offerta con il prezzo più alto e il prezzo più basso richiesto e la differenza tra i due (una differenza positiva significherebbe che qualcosa potrebbe essere acquistato per meno di quanto possa essere venduto).


1
Ti manca una clausola from, altrimenti questo è corretto. I piani di spiegazione sono molto diversi sul mio sistema: la funzione a campi incrociati ha un costo di 22,5 mentre l'approccio LEFT JOIN è circa 4 volte più costoso con un costo di 91.38. Produce anche circa il doppio delle letture fisiche ed esegue hash join, il che può essere piuttosto costoso rispetto ad altri tipi di join.
Jeremiah Peschka,

Grazie Geremia, è buono a sapersi. Ho votato a fondo l'altra risposta, ma vale la pena conservare il tuo commento, quindi non lo eliminerò.
LanceH,

-1

Crosstabla funzione è disponibile sotto l' tablefuncestensione. Dovrai creare questa estensione una volta per il database.

CREA ESTENSIONE tablefunc;

È possibile utilizzare il codice seguente per creare una tabella pivot utilizzando la scheda a croce:

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)

1
Questa risposta non aggiunge nulla rispetto alle risposte preesistenti.
Erwin Brandstetter,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.