Come faccio a generare un CROSS JOIN pivotato in cui la definizione della tabella risultante è sconosciuta?


17

Dato due tabelle con un conteggio di righe indefinito con un nome e un valore, come visualizzerei un pivot CROSS JOINdi una funzione sui loro valori.

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

Ad esempio, se quella funzione fosse la moltiplicazione, come avrei generato una tabella (moltiplicazione) come quella qui sotto,

Tabella di moltiplicazione comune di 1..12

Tutte queste (arg1,arg2,result)righe possono essere generate con

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

Quindi questa è solo una questione di presentazione, mi piacerebbe che funzionasse anche con un nome personalizzato , un nome che non è semplicemente l'argomento del CASTtesto ma impostato nella tabella,

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

Penso che questo sarebbe facilmente fattibile con un CROSSTAB capace di un tipo di ritorno dinamico.

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**

Ma, senza il **MAGIC**, capisco

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

Per riferimento, usando gli esempi precedenti con nomi questo è qualcosa di più simile a ciò tablefuncche crosstab()vuole.

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

Ma ora siamo tornati a fare ipotesi sul contenuto e le dimensioni della bartabella nel nostro esempio. Quindi se,

  1. Le tabelle sono di lunghezza indefinita,
  2. Quindi il cross-join rappresenta un cubo di dimensione indefinita (a causa di sopra),
  3. I nomi delle categorie (linguaggio incrociato) sono nella tabella

Qual è il meglio che possiamo fare in PostgreSQL senza un "elenco di definizioni di colonne" per generare quel tipo di presentazione?


1
I risultati di JSON sarebbero un buon approccio? Un ARRAY sarebbe un buon aprpoach? In questo modo, la definizione della "tabella di output" sarebbe già nota (e corretta). Metti la flessibilità all'interno di JSON o ARRAY. Immagino che dipenderà molti degli strumenti utilizzati successivamente per elaborare le informazioni.
joanolo,

Preferirei che fosse proprio come sopra, se possibile.
Evan Carroll,

Risposte:


11

Caso semplice, SQL statico

La soluzione non dinamica con crosstab()per il semplice caso:

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

Ordino le colonne risultanti per foo.nameno foo.x. Entrambi sono ordinati in parallelo, ma questa è solo la semplice configurazione. Scegli il giusto ordinamento per il tuo caso. Il valore effettivo della seconda colonna è irrilevante in questa query (forma a 1 parametro di crosstab()).

Non abbiamo nemmeno bisogno crosstab()di 2 parametri perché non ci sono valori mancanti per definizione. Vedere:

(Hai risolto la query a campi incrociati nella domanda sostituendola foocon barin una modifica successiva. Ciò risolve anche la query, ma continua a lavorare con i nomi da foo.)

Tipo di ritorno sconosciuto, SQL dinamico

I nomi e i tipi di colonna non possono essere dinamici. SQL richiede di conoscere numero, nomi e tipi di colonne risultanti al momento della chiamata. O mediante dichiarazione esplicita o dalle informazioni nei cataloghi di sistema (Ecco cosa succede SELECT * FROM tbl: Postgres cerca la definizione della tabella registrata.)

Volete che Postgres derivi le colonne risultanti dai dati in una tabella utente. Non succederà.

In un modo o nell'altro, sono necessari due round trip al server. O crei un cursore e poi lo percorri. Oppure si crea una tabella temporanea e quindi si seleziona da essa. Oppure si registra un tipo e lo si utilizza nella chiamata.

In alternativa, puoi semplicemente generare la query in un passaggio ed eseguirla nel successivo:

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;

Questo genera la query sopra, in modo dinamico. Eseguilo nel passaggio successivo.

Sto usando le virgolette ( $$) per semplificare la gestione delle citazioni nidificate. Vedere:

quote_ident() è essenziale per evitare nomi di colonne altrimenti illegali (e possibilmente difendersi dall'iniezione SQL).

Relazionato:


Ho notato che l'esecuzione della query che hai chiamato "Tipo di ritorno sconosciuto, SQL dinamico" in realtà restituisce solo una stringa che rappresenta un'altra query, quindi dici "eseguila nel passaggio successivo". Questo significa che sarebbe difficile, ad esempio, creare una visione materializzata di questo?
Colin D,

@ColinD: non difficile, ma impossibile. È possibile creare un MV dall'SQL generato con il tipo restituito noto. Ma non puoi avere un MV con tipo di ritorno sconosciuto.
Erwin Brandstetter,

10

Qual è il meglio che possiamo fare in PostgreSQL senza un "elenco di definizioni di colonne" per generare quel tipo di presentazione?

Se lo inquadra come un problema di presentazione, potresti considerare una funzione di presentazione post-query.

psqlVengono fornite le versioni più recenti di (9.6) \crosstabview, che mostrano un risultato nella rappresentazione a campi incrociati senza supporto SQL (poiché SQL non può produrlo direttamente, come indicato nella risposta di @ Erwin: SQL richiede di conoscere numero, nomi e tipi di colonne risultanti al momento della chiamata )

Ad esempio, la tua prima query fornisce:

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)

Il secondo esempio con i nomi delle colonne ASCII fornisce:

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)

Vedi il manuale di psql e https://wiki.postgresql.org/wiki/Crosstabview per ulteriori informazioni.


1
Questo è davvero dannatamente bello.
Evan Carroll,

1
La soluzione più elegante.
Erwin Brandstetter,

1

Questa non è una soluzione definitiva

Questo è il mio approccio migliore fino ad ora. Devo ancora convertire l'array finale in colonne.

Per prima cosa ho il prodotto cartesiano di entrambe le tabelle:

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

Ma ho aggiunto un numero di riga solo per identificare ogni riga della prima tabella.

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

Quindi ho acquistato il risultato in questo formato:

[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 |
+---+---------------------+ 

Convertendolo in stringa delimitata da virgole:

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 |
+---+------------------------------+

(Solo per provarlo più tardi: http://rextester.com/NBCYXA2183 )


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.