Alternative dynamique au pivot avec boîtier et groupe par

j'ai une table qui ressemble à ceci:

id    feh    bar
1     10     A
2     20     A
3      3     B
4      4     B
5      5     C
6      6     D
7      7     D
8      8     D

et je veux qu'il ressemble à ceci:

bar  val1   val2   val3
A     10     20 
B      3      4 
C      5        
D      6      7     8

j'ai cette requête:

SELECT bar, 
   MAX(CASE WHEN abc."row" = 1 THEN feh ELSE NULL END) AS "val1",
   MAX(CASE WHEN abc."row" = 2 THEN feh ELSE NULL END) AS "val2",
   MAX(CASE WHEN abc."row" = 3 THEN feh ELSE NULL END) AS "val3"
FROM
(
  SELECT bar, feh, row_number() OVER (partition by bar) as row
  FROM "Foo"
 ) abc
GROUP BY bar

c'est une approche très make-shifty et devient lourd s'il y a beaucoup de nouvelles colonnes à créer. Je me demandais si les déclarations CASE pouvaient être faites mieux pour rendre cette requête plus dynamique? Aussi, j'aimerais voir d'autres approches à faire ce.

21
demandé sur Erwin Brandstetter 2013-03-19 21:16:48

5 réponses

si vous n'avez pas installé le module supplémentaire tablefunc , lancez cette commande une fois par base de données:

CREATE EXTENSION tablefunc;

réponse à la question

une solution crosstab très basique pour votre cas:

SELECT * FROM crosstab(
  'SELECT bar, 1 AS cat, feh
   FROM   tbl_org
   ORDER  BY bar, feh')
 AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?

Le difficultés particulières , ici, c'est qu'il n'y a pas de catégorie ( cat ) dans la table de base. Pour la forme de base à 1 paramètre nous pouvons simplement fournir une colonne factice avec une valeur factice servant de catégorie. La valeur est ignorée de toute façon.

C'est l'un des rares cas où le second paramètre pour la fonction crosstab() est pas nécessaire , parce que toutes les valeurs NULL apparaissent seulement dans les colonnes pendantes à droite par la définition de ce problème. Et l'ordre peut être déterminé par la valeur .

si nous avions une colonne réelle catégorie avec des noms déterminant l'ordre des valeurs dans le résultat, nous aurions besoin de la forme à 2 paramètres de crosstab() . Ici je synthétise une colonne de catégorie avec l'aide de la fonction de fenêtre row_number() , sur la base crosstab() on:

SELECT * FROM crosstab(
   $$
   SELECT bar, val, feh
   FROM  (
      SELECT *, 'val' || row_number() OVER (PARTITION BY bar ORDER BY feh) AS val
      FROM tbl_org
      ) x
   ORDER BY 1, 2
   $$
 , $$VALUES ('val1'), ('val2'), ('val3')$$         -- more columns?
) AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?

le reste est à peu près ordinaire. Vous trouverez plus d'explications et de liens dans ces réponses étroitement liées.

Basics:

lisez d'abord ceci si vous n'êtes pas familier avec la fonction crosstab() !

avancé:

installation D'essai appropriée

C'est comme ça que vous devez fournir un cas d'essai pour commencer:

CREATE TEMP TABLE tbl_org (id int, feh int, bar text);
INSERT INTO tbl_org (id, feh, bar) VALUES
   (1, 10, 'A')
 , (2, 20, 'A')
 , (3,  3, 'B')
 , (4,  4, 'B')
 , (5,  5, 'C')
 , (6,  6, 'D')
 , (7,  7, 'D')
 , (8,  8, 'D');

Tableau croisé dynamique?

pas très dynamique , pourtant, comme @Clodoaldo a commenté . Les types de retour dynamique sont difficiles à réaliser avec plpgsql. Mais il sont les moyens de l'contourner - avec certaines limites .

donc pour ne pas compliquer davantage le reste, je démontre avec un plus simple test case:

CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);

appel:

SELECT * FROM crosstab('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2')
AS ct (row_name text, val1 int, val2 int, val3 int);

Retourne:

 row_name | val1 | val2 | val3
----------+------+------+------
 A        | 10   | 20   |
 B        |  3   |  4   |
 C        |  5   |      |
 D        |  6   |  7   |  8

dispositif intégré de tablefunc module

le module tablefunc fournit une infrastructure simple pour les appels génériques crosstab() sans fournir de liste de définition de colonne. Un certain nombre de fonctions écrites dans C (typiquement très rapide):

crosstabN()

crosstab1() - crosstab4() sont prédéfinies. Un point mineur: ils exigent et renvoient tout text . Nous devons donc exprimer nos valeurs integer . Mais il simplifie l'appel:

SELECT * FROM crosstab4('SELECT row_name, attrib, val::text  -- cast!
                         FROM tbl ORDER BY 1,2')

résultat:

 row_name | category_1 | category_2 | category_3 | category_4
----------+------------+------------+------------+------------
 A        | 10         | 20         |            |
 B        | 3          | 4          |            |
 C        | 5          |            |            |
 D        | 6          | 7          | 8          |

personnalisé crosstab() fonction

Pour colonnes ou autres "types de données 1519540920" , nous créons notre propre composites de type et fonction (une fois).

Type:

CREATE TYPE tablefunc_crosstab_int_5 AS (
  row_name text, val1 int, val2 int, val3 int, val4 int, val5 int);

fonction:

CREATE OR REPLACE FUNCTION crosstab_int_5(text)
  RETURNS SETOF tablefunc_crosstab_int_5
AS '$libdir/tablefunc', 'crosstab' LANGUAGE c STABLE STRICT;

appel:

SELECT * FROM crosstab_int_5('SELECT row_name, attrib, val   -- no cast!
                              FROM tbl ORDER BY 1,2');

résultat:

 row_name | val1 | val2 | val3 | val4 | val5
----------+------+------+------+------+------
 A        |   10 |   20 |      |      |
 B        |    3 |    4 |      |      |
 C        |    5 |      |      |      |
 D        |    6 |    7 |    8 |      |

Un polymorphes, fonction dynamique pour tous

cela va au-delà de ce qui est couvert par le tablefunc module.

Pour rendre le type de retour dynamique j'utilise un type polymorphe avec une technique détaillée dans cette réponse connexe:

1-paramètre de la forme:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L) t(%s)'
                , _qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;

surcharge avec cette variante pour la forme à 2 paramètres:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _cat_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L, %L) t(%s)'
                , _qry, _cat_qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;

pg_typeof(_rowtype)::text::regclass : il y a un type de ligne défini pour chaque type composite défini par l'utilisateur, de sorte que les attributs (colonnes) sont répertoriés dans le catalogue système pg_attribute . La voie rapide pour l'obtenir: cast le type enregistré ( regtype ) à text et cast ce text à regclass .

créer des types composites une fois:

vous devez définir une fois chaque type de retour vous allez utiliser:

CREATE TYPE tablefunc_crosstab_int_3 AS (
    row_name text, val1 int, val2 int, val3 int);

CREATE TYPE tablefunc_crosstab_int_4 AS (
    row_name text, val1 int, val2 int, val3 int, val4 int);

...

pour les appels ad-hoc, vous pouvez également créer un table temporaire pour le même effet (temporaire):

CREATE TEMP TABLE temp_xtype7 AS (
    row_name text, x1 int, x2 int, x3 int, x4 int, x5 int, x6 int, x7 int);

ou utiliser le type d'une table, d'une vue ou d'une vue matérialisée existante si disponible.

Appel

utilisant les types de ligne ci-dessus:

1-paramètre de forme (pas de valeurs manquantes):

SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1,2'
 , NULL::tablefunc_crosstab_int_3);

forme à 2 paramètres (certaines valeurs peuvent être manquantes):

SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1'
 , $$VALUES ('val1'), ('val2'), ('val3')$$
 , NULL::tablefunc_crosstab_int_3);

cette une fonction fonctionne pour tous les types de retour, tandis que le cadre crosstabN() fourni par le module tablefunc nécessite une fonction séparée pour chacun.

Si vous avez nommé votre types de séquence comme démontré ci-dessus, vous n'avez qu'à remplacer le numéro en gras. Pour trouver le nombre maximum de catégories dans le tableau de base:

SELECT max(count(*)) OVER () FROM tbl  -- returns 3
GROUP  BY row_name
LIMIT  1;

c'est à peu près aussi dynamique que cela devient si vous voulez colonnes individuelles . Des tableaux comme démontré par @Clocoaldo ou une simple représentation de texte ou le résultat enveloppé dans un type de document comme json ou hstore peuvent fonctionner pour un nombre de catégories dynamiquement.

Avertissement:

C'est toujours potentiellement dangereux quand les entrées de l'utilisateur sont converties en code. Assurez-vous que cela ne peut pas être utilisé pour L'injection SQL. N'acceptez pas les données provenant d'utilisateurs non fiables (directement).

Appel à la question de départ:

SELECT * FROM crosstab_n('SELECT bar, 1, feh FROM tbl_org ORDER BY 1,2'
                       , NULL::tablefunc_crosstab_int_3);
47
répondu Erwin Brandstetter 2017-09-03 00:25:50

bien qu'il s'agisse d'une vieille question, je voudrais ajouter une autre solution rendue possible par les récentes améliorations de PostgreSQL. Cette solution atteint le même objectif de retourner un résultat structuré à partir d'un ensemble de données dynamiques sans utiliser la fonction crosstab du tout. en d'autres termes, il s'agit d'un bon exemple de réexamen des hypothèses non intentionnelles et implicites qui nous empêchent de trouver de nouvelles solutions à des problèmes anciens. ;)

à illustrez, vous avez demandé une méthode pour transposer les données avec la structure suivante:

id    feh    bar
1     10     A
2     20     A
3      3     B
4      4     B
5      5     C
6      6     D
7      7     D
8      8     D

dans ce format:

bar  val1   val2   val3
A     10     20 
B      3      4 
C      5        
D      6      7     8

la solution conventionnelle est une approche intelligente (et incroyablement bien informée) pour créer des requêtes dynamiques croisées qui est expliquée en détail exquis dans la réponse D'Erwin Brandstetter.

cependant, si votre cas d'utilisation est suffisamment flexible pour accepter un résultat légèrement différent format, puis une autre solution est possible qui gère les pivots dynamiques magnifiquement. Cette technique, que j'ai appris ici

utilise la nouvelle fonction jsonb_object_agg de PostgreSQL pour construire des données pivotées sur la mouche sous la forme d'un objet JSON.

j'utiliserai le test plus simple de M. Brandstetter cas" pour illustrer:

CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);

en utilisant la fonction jsonb_object_agg , nous pouvons créer le jeu de résultats pivotés requis avec cette beauté de pithy:

SELECT
  row_name AS bar,
  json_object_agg(attrib, val) AS data
FROM tbl
GROUP BY row_name
ORDER BY row_name;

qui produit:

 bar |                  data                  
-----+----------------------------------------
 A   | { "val1" : 10, "val2" : 20 }
 B   | { "val1" : 3, "val2" : 4 }
 C   | { "val1" : 5 }
 D   | { "val3" : 8, "val1" : 6, "val2" : 7 }

comme vous pouvez le voir, cette fonction fonctionne en créant des paires clé/valeur dans l'objet JSON à partir des colonnes attrib et value dans les données de l'échantillon, toutes regroupées par row_name .

bien que ce l'ensemble des résultats semble évidemment différent, je crois qu'il satisfera en fait de nombreux (sinon la plupart) cas d'utilisation dans le monde réel, en particulier ceux où les données nécessitent un pivot généré dynamiquement, ou lorsque les données résultantes sont consommées par une application mère (par exemple, doit être reformaté pour la transmission dans une réponse http).

avantages de cette approche:

  • syntaxe plus claire. je pense que tout le monde d'accord que la syntaxe pour cette approche est beaucoup plus propre et plus facile à comprendre que même les exemples les plus basiques crosstab.

  • Complètement dynamique. aucune information sur les données sous-jacentes ne doit être spécifiée au préalable. Ni les noms des colonnes ni leurs types de données ne doivent être connus à l'avance.

  • manipule un grand nombre de colonnes. Puisque les données pivotées sont sauvegardées sous forme d'une seule colonne jsonb, vous ne pourrez pas courir contre la limite de colonne de PostgreSQL (≤1600 colonnes, je crois). Il y a encore une limite, mais je crois que c'est la même que pour les champs de texte: 1 Go par objet JSON créé (corrigez-moi si je me trompe). C'est beaucoup de paires clé/valeur!

  • traitement simplifié des données. je crois que la création de données JSON dans le DB simplifiera (et accélérer probablement) le processus de conversion des données dans les applications parent. (Vous noterez que les données entières dans notre exemple de cas de test ont été correctement stockées comme telles dans les objets JSON résultants. PostgreSQL gère cela en convertissant automatiquement ses types de données intrinsèques en JSON conformément à la spécification JSON. Cela éliminera la nécessité de lancer manuellement les données transmises aux applications mères: tout cela peut être délégué à l'analyseur JSON natif de l'application.

différences (et inconvénients possibles):

  • ça a l'air différent. il est indéniable que les résultats de cette approche semblent différents. L'objet JSON n'est pas aussi joli que l'ensemble de résultats crosstab; cependant, les différences sont purement cosmétiques. La même information est produite--et dans un format qui est probablement plus convivial pour la consommation par les parents application.

  • clés manquantes. les valeurs manquantes dans l'approche crosstab sont remplies avec des NULL, alors que les objets JSON manquent simplement les clés applicables. Vous aurez à décider pour vous même si c'est un compromis acceptable pour votre cas d'utilisation. Il me semble que toute tentative d'aborder ce problème dans PostgreSQL compliquera grandement le processus et impliquera probablement une certaine introspection sous la forme de questions supplémentaires.

  • commande de Clés est pas conservé. Je ne sais pas si cela peut être abordé dans PostgreSQL, mais cette question est surtout esthétique aussi, puisque toute application parent sont peu susceptibles de compter sur l'ordre de clé, ou ont la capacité de déterminer l'ordre de clé approprié par d'autres moyens. Le cas le plus défavorable exigera probablement seulement une requête supplémentaire de la base de données.

Conclusion

je suis très curieux d'entendre les opinions des autres (en particulier @Erwinbrandstetter's) sur cette approche, surtout en ce qui concerne la performance. Quand J'ai découvert cette approche sur le blog D'Andrew Bender, c'était comme se faire frapper sur le côté de la tête. Quelle belle façon d'aborder un problème difficile à PostrgeSQL. Il a résolu mon cas d'utilisation parfaitement, et je crois qu'il va également servir à de nombreuses autres.

11
répondu Damian C. Rossney 2016-10-18 21:24:45

c'est pour compléter @Damian bonne réponse. J'ai déjà suggéré l'approche JSON dans d'autres réponses avant la fonction json_object_agg . Cela demande juste plus de travail avec l'ensemble d'outils précédent.

deux des inconvénients possibles cités ne sont vraiment pas. L'ordre aléatoire des touches est trivialement corrigé si nécessaire. Les touches manquantes, le cas échéant, prennent une quantité presque insignifiante de code à adresser:

select
    row_name as bar,
    json_object_agg(attrib, val order by attrib) as data
from
    tbl
    right join
    (
        (select distinct row_name from tbl) a
        cross join
        (select distinct attrib from tbl) b
    ) c using (row_name, attrib)
group by row_name
order by row_name
;
 bar |                     data                     
-----+----------------------------------------------
 a   | { "val1" : 10, "val2" : 20, "val3" : null }
 b   | { "val1" : 3, "val2" : 4, "val3" : null }
 c   | { "val1" : 5, "val2" : null, "val3" : null }
 d   | { "val1" : 6, "val2" : 7, "val3" : 8 }

pour un consommateur de requête finale qui comprend JSON il n'y a aucun inconvénient. Le seul est qu'il ne peut pas être consommé comme une source de table.

5
répondu Clodoaldo Neto 2017-05-23 12:18:01

dans votre cas, je suppose qu'un tableau est bon. violon SQL

select
    bar,
    feh || array_fill(null::int, array[c - array_length(feh, 1)]) feh
from
    (
        select bar, array_agg(feh) feh
        from foo
        group by bar
    ) s
    cross join (
        select count(*)::int c
        from foo
        group by bar
        order by c desc limit 1
    ) c(c)
;
 bar |      feh      
-----+---------------
 A   | {10,20,NULL}
 B   | {3,4,NULL}
 C   | {5,NULL,NULL}
 D   | {6,7,8}
4
répondu Clodoaldo Neto 2013-03-20 00:51:25

je suis désolé de revenir dans le passé, mais la solution" Dynamic Crosstab " renvoie une table de résultat erronée. Ainsi, les valeurs valN sont par erreur "alignées à gauche" et ne correspondent pas aux noms des colonnes. Lorsque la table d'entrée a des "trous" dans les valeurs, par exemple "C" A val1 et val3 mais pas val2. Cela produit une erreur: la valeur de val3 sera portée dans la colonne val2 (c.-à-d. la colonne libre suivante) dans le tableau final.

CREATE TEMP TABLE tbl (row_name text, attrib text, val int); 
INSERT INTO tbl (row_name, attrib, val) VALUES ('C', 'val1', 5) ('C', 'val3', 7);

SELECT * FROM crosstab('SELECT row_name, attrib, val FROM tbl 
ORDER BY 1,2') AS ct (row_name text, val1 int, val2 int, val3 int);

row_name|val1|val2|val3
 C      |   5|  7 |

pour retourner corriger les cellules avec "trous" dans la colonne de droite, la requête crosstab nécessite un 2nd SELECT dans la crosstab, quelque chose comme ce "crosstab('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2', 'select distinct row_name from tbl order by 1')"

1
répondu vsinceac 2018-05-14 10:27:37