Wie erstelle ich einen geschwenkten CROSS JOIN, bei dem die resultierende Tabellendefinition unbekannt ist?

17

Wie würde ich bei zwei Tabellen mit einer undefinierten Zeilenanzahl mit einem Namen und einem Wert eine Pivot- CROSS JOINFunktion über ihren Werten anzeigen?

CREATE TEMP TABLE foo AS
SELECT x::text AS name, x::int
FROM generate_series(1,10) AS t(x);

CREATE TEMP TABLE bar AS
SELECT x::text AS name, x::int
FROM generate_series(1,5) AS t(x);

Wenn diese Funktion beispielsweise Multiplikation wäre, wie würde ich eine (Multiplikations-) Tabelle wie die folgende erzeugen?

Gemeinsame Multiplikationstabelle vom 1..12

Alle diese (arg1,arg2,result)Zeilen können mit erzeugt werden

SELECT foo.name AS arg1, bar.name AS arg2, foo.x*bar.x AS result
FROM foo
CROSS JOIN bar; 

Dies ist also nur eine Frage der Präsentation. Ich möchte, dass dies auch mit einem benutzerdefinierten Namen funktioniert - einem Namen, der nicht nur als Textargument dient CAST, sondern in der Tabelle festgelegt ist.

CREATE TEMP TABLE foo AS
SELECT chr(x+64) AS name, x::int
FROM generate_series(1,10) AS t(x);

CREATE TEMP TABLE bar AS
SELECT chr(x+72) AS name, x::int
FROM generate_series(1,5) AS t(x);

Ich denke, dass dies mit einem CROSSTAB, der einen dynamischen Rückgabetyp aufweist, leicht möglich wäre.

SELECT * FROM crosstab(
  '
    SELECT foo.x AS arg1, bar.x AS arg2, foo.x*bar.x
    FROM foo
    CROSS JOIN bar
  ', 'SELECT DISTINCT name FROM bar'
) AS **MAGIC**

Aber ohne das **MAGIC**bekomme ich

ERROR:  a column definition list is required for functions returning "record"
LINE 1: SELECT * FROM crosstab(

Als Referenz der obigen Beispiele mit Namen verwenden , ist dies etwas mehr wie das, was tablefunc‚s crosstab()will.

SELECT * FROM crosstab(
  '
    SELECT foo.x AS arg1, bar.x AS arg2, foo.x*bar.x
    FROM foo
    CROSS JOIN bar
  '
) AS t(row int, i int, j int, k int, l int, m int);

Aber jetzt kehren wir zu den Annahmen über den Inhalt und die Größe der barTabelle in unserem Beispiel zurück. Also wenn,

  1. Die Tabellen sind undefiniert lang,
  2. Dann repräsentiert der Cross-Join einen Würfel mit undefinierter Dimension (wegen oben),
  3. Die Kategorienamen (Kreuzworträtsel) sind in der Tabelle aufgeführt

Was ist das Beste, was wir in PostgreSQL ohne eine "Spaltendefinitionsliste" tun können, um eine solche Präsentation zu erstellen?

Evan Carroll
quelle
1
Wären JSON-Ergebnisse ein guter Ansatz? Wäre ein ARRAY ein guter Anlaufpunkt? Auf diese Weise wäre die Definition der "Ausgabetabelle" bereits bekannt (und festgelegt). Sie legen die Flexibilität innerhalb des JSON oder des ARRAY fest. Ich vermute, dass es von vielen Tools abhängt, die später zur Verarbeitung der Informationen verwendet werden.
Joanolo
Ich würde es vorziehen, wenn es möglich wäre.
Evan Carroll

Antworten:

11

Einfacher Fall, statisches SQL

Die nicht dynamische Lösung crosstab()für den einfachen Fall:

SELECT * FROM crosstab(
  'SELECT b.x, f.name, f.x * b.x AS prod
   FROM   foo f, bar b
   ORDER  BY 1, 2'
   ) AS ct (x int, "A" int, "B" int, "C" int, "D" int, "E" int
                 , "F" int, "G" int, "H" int, "I" int, "J" int);

Ich ordne die resultierenden Spalten nach foo.name, nicht nach foo.x. Beide werden zufällig parallel sortiert, aber das ist nur die einfache Einrichtung. Wählen Sie die richtige Sortierreihenfolge für Ihren Fall. Der tatsächliche Wert der zweiten Spalte ist in dieser Abfrage irrelevant (1-Parameter-Form von crosstab()).

Wir brauchen nicht einmal crosstab()2 Parameter, da per Definition keine Werte fehlen. Sehen:

(Sie haben die Kreuztabellenabfrage in der Frage durch Ersetzen foodurch barin einer späteren Bearbeitung behoben . Dadurch wird auch die Abfrage behoben , es wird jedoch weiterhin mit Namen von gearbeitet foo.)

Unbekannter Rückgabetyp, dynamisches SQL

Spaltennamen und -typen können nicht dynamisch sein. SQL fordert zum Zeitpunkt des Aufrufs die Kenntnis von Nummer, Namen und Typen der resultierenden Spalten. Entweder durch explizite Deklaration oder anhand von Informationen in den Systemkatalogen (Das passiert mit SELECT * FROM tbl: Postgres schlägt die registrierte Tabellendefinition nach.)

Sie möchten, dass Postgres resultierende Spalten aus Daten in einer Benutzertabelle ableitet. Das wird nicht passieren.

Auf die eine oder andere Weise benötigen Sie zwei Roundtrips zum Server. Entweder Sie erstellen einen Cursor und gehen dann durch. Oder Sie erstellen eine temporäre Tabelle und wählen sie aus. Oder Sie registrieren einen Typ und verwenden ihn im Anruf.

Oder Sie generieren die Abfrage einfach in einem Schritt und führen sie im nächsten aus:

SELECT $$SELECT * FROM crosstab(
  'SELECT b.x, f.name, f.x * b.x AS prod
   FROM   foo f, bar b
   ORDER  BY 1, 2'
   ) AS ct (x int, $$
 || string_agg(quote_ident(name), ' int, ' ORDER BY name) || ' int)'
FROM   foo;

Dadurch wird die obige Abfrage dynamisch generiert. Führen Sie es im nächsten Schritt aus.

Ich verwende Dollar-Anführungszeichen ( $$), um die Handhabung verschachtelter Anführungszeichen einfach zu halten. Sehen:

quote_ident() ist wichtig, um ansonsten unzulässigen Spaltennamen zu entgehen (und möglicherweise gegen SQL-Injection zu verteidigen).

Verbunden:

Erwin Brandstetter
quelle
Ich habe festgestellt, dass beim Ausführen der Abfrage "Unbekannter Rückgabetyp, dynamisches SQL" nur eine Zeichenfolge zurückgegeben wird, die eine andere Abfrage darstellt, und dann "im nächsten Schritt ausführen". Bedeutet dies, dass es zum Beispiel schwierig wäre, eine materialisierte Sicht davon zu schaffen?
Colin D
@ColinD: Nicht schwer, aber unmöglich. Sie können eine MV aus der generierten SQL mit bekanntem Rückgabetyp erstellen. Sie können jedoch kein MV mit unbekanntem Rückgabetyp verwenden.
Erwin Brandstetter
10

Was ist das Beste, was wir in PostgreSQL ohne eine "Spaltendefinitionsliste" tun können, um eine solche Präsentation zu erstellen?

Wenn Sie dies als Präsentationsproblem einrahmen, können Sie eine Präsentationsfunktion nach der Abfrage in Betracht ziehen.

Neuere Versionen von psql(9.6) werden mit \crosstabvieweinem Ergebnis in Kreuztabellendarstellung ohne SQL-Unterstützung geliefert (da SQL dies nicht direkt erzeugen kann, wie in @ Erwins Antwort erwähnt: SQL verlangt, dass Anzahl, Namen und Typen der resultierenden Spalten zum Aufrufzeitpunkt bekannt sind )

Ihre erste Abfrage lautet beispielsweise:

SELECT foo.name AS arg1, bar.name AS arg2, foo.x*bar.x AS result
FROM foo
CROSS JOIN bar
\crosstabview

 arg1 | 1  | 2  | 3  | 4  | 5  
------+----+----+----+----+----
 1    |  1 |  2 |  3 |  4 |  5
 2    |  2 |  4 |  6 |  8 | 10
 3    |  3 |  6 |  9 | 12 | 15
 4    |  4 |  8 | 12 | 16 | 20
 5    |  5 | 10 | 15 | 20 | 25
 6    |  6 | 12 | 18 | 24 | 30
 7    |  7 | 14 | 21 | 28 | 35
 8    |  8 | 16 | 24 | 32 | 40
 9    |  9 | 18 | 27 | 36 | 45
 10   | 10 | 20 | 30 | 40 | 50
(10 rows)

Das zweite Beispiel mit ASCII-Spaltennamen lautet:

SELECT foo.name AS arg1, bar.name AS arg2, foo.x*bar.x
    FROM foo
    CROSS JOIN bar
  \crosstabview

 arg1 | I  | J  | K  | L  | M  
------+----+----+----+----+----
 A    |  1 |  2 |  3 |  4 |  5
 B    |  2 |  4 |  6 |  8 | 10
 C    |  3 |  6 |  9 | 12 | 15
 D    |  4 |  8 | 12 | 16 | 20
 E    |  5 | 10 | 15 | 20 | 25
 F    |  6 | 12 | 18 | 24 | 30
 G    |  7 | 14 | 21 | 28 | 35
 H    |  8 | 16 | 24 | 32 | 40
 I    |  9 | 18 | 27 | 36 | 45
 J    | 10 | 20 | 30 | 40 | 50
(10 rows)

Weitere Informationen finden Sie im psql-Handbuch und unter https://wiki.postgresql.org/wiki/Crosstabview .

Daniel Vérité
quelle
1
Das ist wirklich verdammt cool.
Evan Carroll
1
Der eleganteste Workaround.
Erwin Brandstetter
1

Dies ist keine endgültige Lösung

Das ist mein bisher bester Ansatz. Müssen noch das endgültige Array in Spalten konvertieren.

Zuerst habe ich das kartesische Produkt beider Tabellen:

select foo.name xname, bar.name yname, (foo.x * bar.x)::text as val,
       ((row_number() over ()) - 1) / (select count(*)::integer from foo) as row
 from bar
     cross join foo
 order by bar.name, foo.name

Ich habe jedoch eine Zeilennummer hinzugefügt, um jede Zeile der ersten Tabelle zu identifizieren.

((row_number() over ()) - 1) / (select count(*)::integer from foo)

Dann habe ich das Ergebnis in diesem Format erstellt:

[Row name] [Array of values]


select col_name, values
from
(
select '' as col_name, array_agg(name) as values from foo
UNION
select fy.name as col_name,
    (select array_agg(t.val) as values
    from  
        (select foo.name xname, bar.name yname, (foo.x * bar.x)::text as val,
              ((row_number() over ()) - 1) / (select count(*)::integer from foo) as row
        from bar
           cross join foo
        order by bar.name, foo.name) t
    where t.row = fy.row)
from
    (select name, (row_number() over(order by name)) - 1 as row from bar) fy
) a
order by col_name;

+---+---------------------+
|   |      ABCDEFGHIJ     |
+---+---------------------+
| I |     12345678910     |
+---+---------------------+
| J |   2468101214161820  |
+---+---------------------+
| K |  36912151821242730  |
+---+---------------------+
| L |  481216202428323640 |
+---+---------------------+
| M | 5101520253035404550 |
+---+---------------------+ 

Konvertieren in einen durch Kommas getrennten String:

select col_name, values
from
(
select '' as col_name, array_to_string(array_agg(name),',') as values from foo
UNION
select fy.name as col_name,
    (select array_to_string(array_agg(t.val),',') as values
    from  
        (select foo.name xname, bar.name yname, (foo.x * bar.x)::text as val,
              ((row_number() over ()) - 1) / (select count(*)::integer from foo) as row
        from bar
           cross join foo
        order by bar.name, foo.name) t
    where t.row = fy.row)
from
    (select name, (row_number() over(order by name)) - 1 as row from bar) fy
) a
order by col_name;


+---+------------------------------+
|   | A,B,C,D,E,F,G,H,I,J          |
+---+------------------------------+
| I | 1,2,3,4,5,6,7,8,9,10         |
+---+------------------------------+
| J | 2,4,6,8,10,12,14,16,18,20    |
+---+------------------------------+
| K | 3,6,9,12,15,18,21,24,27,30   |
+---+------------------------------+
| L | 4,8,12,16,20,24,28,32,36,40  |
+---+------------------------------+
| M | 5,10,15,20,25,30,35,40,45,50 |
+---+------------------------------+

(Nur um es später zu versuchen: http://rextester.com/NBCYXA2183 )

McNets
quelle