Comment générer un CROSS JOIN pivoté dont la définition de table résultante est inconnue?

18

Étant donné deux tables avec un nombre de lignes non défini avec un nom et une valeur, comment afficher un pivot CROSS JOINd'une fonction sur leurs valeurs.

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

Par exemple, si cette fonction était une multiplication, comment générer une table (de multiplication) comme celle ci-dessous,

Table de multiplication commune de 1 à 12

Toutes ces (arg1,arg2,result)lignes peuvent être générées avec

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

Donc, ce n'est qu'une question de présentation, je voudrais que cela fonctionne également avec un nom personnalisé - un nom qui n'est pas simplement l'argument CASTédité dans le texte mais défini dans le tableau,

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

Je pense que ce serait facilement faisable avec un CROSSTAB capable d'un type de retour dynamique.

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

Mais, sans le **MAGIC**, je reçois

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

Pour référence, en utilisant les exemples ci-dessus avec des noms, cela ressemble plus à ce que tablefuncl'on crosstab()veut.

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

Mais, maintenant nous sommes de retour à faire des hypothèses sur le contenu et la taille de la bartable dans notre exemple. Donc si,

  1. Les tables sont de longueur indéfinie,
  2. Ensuite, la jointure croisée représente un cube de dimension indéfinie (à cause de ci-dessus),
  3. Les noms de catégories (langage croisé) sont dans le tableau

Que pouvons-nous faire de mieux dans PostgreSQL sans une "liste de définitions de colonnes" pour générer ce type de présentation?

Evan Carroll
la source
1
Les résultats JSON seraient-ils une bonne approche? Un tableau serait-il une bonne approche? De cette façon, la définition de la "table de sortie" serait déjà connue (et fixée). Vous mettez la flexibilité dans le JSON ou l'ARRAY. Je suppose que cela dépendra de nombreux outils utilisés par la suite pour traiter les informations.
joanolo
Je préférerais que ce soit exactement comme ci-dessus, si possible.
Evan Carroll

Réponses:

12

Cas simple, SQL statique

La solution non dynamique avec crosstab()pour le cas simple:

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

Je commande les colonnes résultantes par foo.name, non foo.x. Les deux sont triés en parallèle, mais ce n'est que la configuration simple. Choisissez le bon ordre de tri pour votre cas. La valeur réelle de la deuxième colonne n'est pas pertinente dans cette requête (forme à 1 paramètre de crosstab()).

Nous n'avons même pas besoin crosstab()de 2 paramètres car il n'y a pas de valeurs manquantes par définition. Voir:

(Vous avez corrigé la requête de tableau croisé dans la question en la remplaçant foopar bardans une modification ultérieure. Cela corrige également la requête, mais continue de travailler avec les noms de foo.)

Type de retour inconnu, SQL dynamique

Les noms et types de colonnes ne peuvent pas être dynamiques. SQL exige de connaître le nombre, les noms et les types de colonnes résultantes au moment de l'appel. Soit par déclaration explicite, soit à partir d'informations dans les catalogues système (c'est ce qui se passe avec SELECT * FROM tbl: Postgres recherche la définition de table enregistrée.)

Vous souhaitez que Postgres dérive les colonnes résultantes des données d'une table utilisateur. Ça n'arrivera pas.

D'une manière ou d'une autre, vous avez besoin de deux aller-retour vers le serveur. Soit vous créez un curseur, puis vous le parcourez. Ou vous créez une table temporaire, puis sélectionnez-la. Ou vous enregistrez un type et l'utilisez dans l'appel.

Ou vous générez simplement la requête en une étape et l'exécutez à la suivante:

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;

Cela génère la requête ci-dessus, dynamiquement. Exécutez-le à l'étape suivante.

J'utilise dollar-quotes ( $$) pour simplifier la gestion des citations imbriquées. Voir:

quote_ident() est essentiel pour échapper aux noms de colonnes autrement illégaux (et éventuellement se défendre contre l'injection SQL).

En relation:

Erwin Brandstetter
la source
J'ai remarqué que l'exécution de la requête que vous appelez "Type de retour inconnu, SQL dynamique" renvoie en fait juste une chaîne qui représente une autre requête, puis vous dites "exécutez-la à l'étape suivante". Est-ce à dire qu'il serait par exemple difficile de créer une vision matérialisée à partir de cela?
Colin D
@ColinD: Pas difficile, mais tout simplement impossible. Vous pouvez créer un MV à partir du SQL généré avec un type de retour connu. Mais vous ne pouvez pas avoir un MV avec un type de retour inconnu.
Erwin Brandstetter
11

Que pouvons-nous faire de mieux dans PostgreSQL sans une "liste de définitions de colonnes" pour générer ce type de présentation?

Si vous définissez cela comme un problème de présentation, vous pouvez envisager une fonctionnalité de présentation post-requête.

Les versions plus récentes de psql(9.6) sont livrées avec \crosstabview, montrant un résultat dans la représentation croisée sans prise en charge SQL (puisque SQL ne peut pas produire cela directement, comme mentionné dans la réponse de @ Erwin: SQL exige de connaître le nombre, les noms et les types de colonnes résultantes au moment de l'appel) )

Par exemple, votre première requête donne:

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)

Le deuxième exemple avec les noms de colonnes ASCII donne:

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)

Voir le manuel psql et https://wiki.postgresql.org/wiki/Crosstabview pour en savoir plus.

Daniel Vérité
la source
1
C'est vraiment sacrément cool.
Evan Carroll
1
La solution de contournement la plus élégante.
Erwin Brandstetter
1

Ce n'est pas une solution définitive

C'est ma meilleure approche jusqu'à présent. Encore faut-il convertir le tableau final en colonnes.

J'ai d'abord le produit cartésien des deux tables:

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

Mais, j'ai ajouté un numéro de ligne juste pour identifier chaque ligne de la première table.

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

Ensuite, j'ai construit le résultat dans ce format:

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

Le convertir en chaîne délimitée par des comas:

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

(Juste pour l'essayer plus tard: http://rextester.com/NBCYXA2183 )

McNets
la source