PostgreSQL-Kreuztabellenabfrage

196

Weiß jemand, wie man Kreuztabellenabfragen in PostgreSQL erstellt?
Zum Beispiel habe ich die folgende Tabelle:

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

Ich möchte, dass die Abfrage die folgende Kreuztabelle zurückgibt:

Section    Active    Inactive
A          1         2
B          4         5

Ist das möglich?

schone
quelle
1
Ich hatte eine etwas andere Struktur und fand dieses Beispiel etwas schwer zu verstehen, so dass ich meine Denkweise über diesen stackoverflow.com/q/49051959/808723 dokumentierte . Vielleicht ist es für jeden hilfreich.
GameScripting

Antworten:

317

Installieren Sie das zusätzliche Modul tablefunc einmal pro Datenbank, die die Funktion bereitstellt crosstab(). Seit Postgres 9.1 können Sie dafür verwenden CREATE EXTENSION:

CREATE EXTENSION IF NOT EXISTS tablefunc;

Verbesserter Testfall

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

Einfache Form - nicht für fehlende Attribute geeignet

crosstab(text)mit 1 Eingabeparameter:

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

Kehrt zurück:

Abschnitt | Aktiv | Inaktiv
--------- + -------- + ----------
 A | 1 | 2
 B | 4 | 5
 C |      7 | - !!
  • Kein Casting und Umbenennen erforderlich.
  • Beachten Sie das falsche Ergebnis für C: Der Wert 7wird für die erste Spalte ausgefüllt. Manchmal ist dieses Verhalten wünschenswert, aber nicht für diesen Anwendungsfall.
  • Das einfache Formular ist in der bereitgestellten Eingabeabfrage auch auf genau drei Spalten beschränkt : Zeilenname , Kategorie , Wert . Es ist kein Platz für zusätzliche Spalten wie in der folgenden 2-Parameter-Alternative.

Sichere Form

crosstab(text, text)mit 2 Eingabeparametern:

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

Kehrt zurück:

Abschnitt | Aktiv | Inaktiv
--------- + -------- + ----------
 A | 1 | 2
 B | 4 | 5
 C | |        7   - !!
  • Notieren Sie das richtige Ergebnis für C.

  • Der zweite Parameter kann eine beliebige Abfrage sein, die eine Zeile pro Attribut zurückgibt , die der Reihenfolge der Spaltendefinition am Ende entspricht. Oft möchten Sie unterschiedliche Attribute aus der zugrunde liegenden Tabelle wie folgt abfragen:

    'SELECT DISTINCT attribute FROM tbl ORDER BY 1'

    Das steht im Handbuch.

    Da Sie ohnehin alle Spalten in einer Spaltendefinitionsliste buchstabieren müssen (mit Ausnahme vordefinierter Varianten), ist es in der Regel effizienter, eine kurze Liste in einem Ausdruck wie dem folgenden bereitzustellen :crosstabN()VALUES

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

    Oder (nicht im Handbuch):

    $$SELECT unnest('{Active,Inactive}'::text[])$$  -- short syntax for long lists
  • Ich habe Dollarnotierungen verwendet , um die Notierung zu vereinfachen.

  • Sie können sogar Spalten mit unterschiedlichen Ausgaben ausgeben Datentypen mit crosstab(text, text)- solange die Textdarstellung der Wertespalte eine gültige Eingabe für den Zieltyp ist. Auf diese Weise haben Sie vielleicht Attribute verschiedener Art und Ausgang text, date, numericusw. für die jeweiligen Attribute. Am Ende des Kapitels crosstab(text, text)des Handbuchs befindet sich ein Codebeispiel .

db <> hier fummeln

Fortgeschrittene Beispiele


\crosstabview in psql

Postgres 9.6 hat diesen Meta-Befehl zu seinem interaktiven Standardterminal hinzugefügt psql . Sie können die Abfrage, die Sie als ersten crosstab()Parameter verwenden würden, ausführen und an sie weiterleiten \crosstabview(sofort oder im nächsten Schritt). Mögen:

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

Ähnliches Ergebnis wie oben, aber es ist ein ausschließlich auf der Client-Seite . Eingabezeilen werden geringfügig anders behandelt und sind daher ORDER BYnicht erforderlich. Details dazu \crosstabviewim Handbuch. Weitere Codebeispiele finden Sie am Ende dieser Seite.

Verwandte Antwort auf dba.SE von Daniel Vérité (dem Autor der psql-Funktion):



Das zuvor akzeptierte Antwort ist veraltet.

  • Die Variante der Funktion crosstab(text, integer)ist veraltet. Der zweite integerParameter wird ignoriert. Ich zitiere die aktuelle Handbuch :

    crosstab(text sql, int N) ...

    Veraltete Version von crosstab(text). Der Parameter Nwird jetzt ignoriert, da die Anzahl der Wertespalten immer von der aufrufenden Abfrage bestimmt wird

  • Unnötiges Casting und Umbenennen.

  • Es schlägt fehl, wenn eine Zeile nicht alle Attribute enthält. Siehe sichere Variante mit zwei Eingabeparametern oben, um fehlende Attribute richtig zu behandeln.

  • ORDER BYwird in der Ein-Parameter-Form von benötigt crosstab().Das Handbuch:

    In der Praxis sollte die SQL-Abfrage immer angeben, ORDER BY 1,2um sicherzustellen, dass die Eingabezeilen ordnungsgemäß sortiert sind

Erwin Brandstetter
quelle
3
+1, gute Berichterstattung, danke, dass Sie es bemerkt habenIn practice the SQL query should always specify ORDER BY 1,2 to ensure that the input rows are properly ordered
ChristopheD
Ich habe einige Probleme mit $$ VALUES .. $$. Ich habe stattdessen 'VALUES (' '<attr>' ':: <type>) verwendet, ..'
Marco Fantasia
Können wir die Parameterbindung in der Kreuztabellenabfrage angeben? Ich erhalte diesen Fehler => konnte den Datentyp des Parameters $ 2
Ashish
1
Ist es möglich, den Standardwert für die Spalte in der Kreuztabellenabfrage festzulegen?
Ashish
2
@ Ashish: Bitte starte eine neue Frage. Kommentare sind nicht der richtige Ort. Sie können jederzeit einen Link zu diesem für den Kontext erstellen.
Erwin Brandstetter
30

Sie können die crosstab()Funktion des Zusatzmoduls tablefunc verwenden, das Sie einmal pro Datenbank installieren müssen . Seit PostgreSQL 9.1 können Sie dafür Folgendes verwenden CREATE EXTENSION:

CREATE EXTENSION tablefunc;

In Ihrem Fall würde es ungefähr so ​​aussehen:

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
quelle
Wenn Sie einen Parameter in der Kreuztabellenabfrage verwenden, müssen Sie ihn ordnungsgemäß maskieren. Beispiel: (von oben) Angenommen, Sie möchten nur die aktiven: SELECT ... FROM Kreuztabelle ('Abschnitt :: Text, Status, Anzahl :: Text aus t auswählen, wobei Status =' 'Aktiv' '', 2) AS. .. (beachten Sie die doppelten Anführungszeichen). Wenn der Parameter zur Laufzeit vom Benutzer übergeben wird (z. B. als Funktionsparameter), können Sie sagen: SELECT ... FROM Kreuztabelle ('Abschnitt :: Text, Status, Anzahl :: Text aus t auswählen, wobei Status =' ' '|| par_active ||' '' ', 2) AS ... (dreifache Anführungszeichen hier!). In BIRT funktioniert dies auch mit dem? Platzhalter.
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
quelle
1
Kann jemand erklären, was die Kreuztabellenfunktion im tablefunc-Modul zu dieser Antwort hinzufügt, die sowohl die vorliegende Aufgabe erledigt als auch meiner Meinung nach leichter zu verstehen ist?
John Powell
4
@ JohnBarça: Ein einfacher Fall wie dieser kann leicht mit CASE-Anweisungen gelöst werden. Dies wird jedoch sehr schnell unhandlich mit mehr Attributen und / oder anderen Datentypen als nur ganzen Zahlen. Nebenbei: Dieses Formular verwendet die Aggregatfunktionsum() , es wäre besser, min()oder max()und nein zu verwenden, ELSEwas auch funktioniert text. Dies hat jedoch subtil andere Auswirkungen als die corosstab(), bei der nur der "erste" Wert pro Attribut verwendet wird. Es spielt keine Rolle, solange es nur einen geben kann. Schließlich ist auch die Leistung relevant. crosstab()ist in C geschrieben und für die Aufgabe optimiert.
Erwin Brandstetter
Das funktioniert bei mir nicht, bei postgresql. Ich bekomme den FehlerERROR: 42803: aggregate function calls may not be nested
Audrey
1
@Audrey Sie führen dann nicht das gleiche SQL?
2
Erwägen
10

Lösung mit JSON-Aggregation:

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
quelle
Vielen Dank, dies hat mir bei einem verwandten Problem geholfen.
JeffCharter
1

Es tut mir leid, dass dies nicht vollständig ist, da ich es hier nicht testen kann, aber es kann Sie in die richtige Richtung bringen. Ich übersetze von etwas, das ich verwende und das eine ähnliche Abfrage macht:

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;

Der Code, mit dem ich arbeite, lautet:

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;

Dies gibt eine typeID, das höchste Preisgebot und den niedrigsten geforderten Preis sowie die Differenz zwischen den beiden zurück (eine positive Differenz würde bedeuten, dass etwas für weniger gekauft werden könnte, als es verkauft werden kann).

LanceH
quelle
1
Sie vermissen eine from-Klausel, sonst ist dies richtig. Die EXPLAIN-Pläne sind auf meinem System sehr unterschiedlich - die Kreuztabellenfunktion kostet 22,5, während der LEFT JOIN-Ansatz mit 91,38 etwa viermal so teuer ist. Es erzeugt außerdem etwa doppelt so viele physische Lesevorgänge und führt Hash-Joins durch - was im Vergleich zu anderen Join-Typen recht teuer sein kann.
Jeremiah Peschka
Danke Jeremiah, das ist gut zu wissen. Ich habe die andere Antwort positiv bewertet, aber Ihr Kommentar ist es wert, beibehalten zu werden, damit ich diese nicht lösche.
LanceH
-1

CrosstabFunktion ist unter der tablefuncErweiterung verfügbar . Sie müssen diese Erweiterung einmal für die Datenbank erstellen.

ERWEITERUNG ERSTELLEN tablefunc;

Mit dem folgenden Code können Sie eine Pivot-Tabelle mithilfe der Kreuztabelle erstellen:

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
quelle
1
Diese Antwort fügt nichts zu bereits vorhandenen Antworten hinzu.
Erwin Brandstetter