PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Alternative dynamique au pivot avec CASE et GROUP BY

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

CREATE EXTENSION tablefunc;

Réponse à la question

Une solution de tableau croisé 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?

La difficulté spéciale voilà, qu'il n'y a pas de catégorie (cat ) dans la table de base. Pour le formulaire de base à 1 paramètre nous pouvons simplement fournir une colonne fictive avec une valeur fictive servant de catégorie. La valeur est ignorée de toute façon.

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

Si nous avions une catégorie réelle colonne 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 à l'aide de la fonction de fenêtre row_number() , pour baser crosstab() le :

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 plutôt banal. Trouvez plus d'explications et de liens dans ces réponses étroitement liées.

Bases :
Lisez ceci en premier si vous n'êtes pas familier avec le crosstab() fonction !

  • Requête croisée PostgreSQL

Avancé :

  • Pivoter sur plusieurs colonnes à l'aide de Tablefunc
  • Fusionner une table et un journal des modifications dans une vue dans PostgreSQL

Configuration de test appropriée

C'est ainsi que vous devez fournir un scénario de test 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 l'a commenté @Clodoaldo. Les types de retour dynamiques sont difficiles à obtenir avec plpgsql. Mais il existe façons de le contourner - avec quelques limitations .

Alors pour ne pas compliquer davantage la suite, je démontre avec un plus simple cas de test :

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

Renvoie :

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

Fonctionnalité intégrée de tablefunc modules

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

crosstabN()

crosstab1() - crosstab4() sont prédéfinis. Un point mineur :ils nécessitent et renvoient tous les text . Nous devons donc caster notre integer valeurs. Mais cela 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 plus de colonnes ou autres types de données , nous créons notre propre type composite et fonction (une fois).
Tapez :

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 fonction polymorphe et 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 associée :

  • Refactoriser une fonction PL/pgSQL pour renvoyer la sortie de diverses requêtes SELECT

Formulaire à 1 paramètre :

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  :Un type de ligne est 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 :lancez le type enregistré (regtype ) en text et lancez ce text à regclass .

Créez des types composites une seule fois :

Vous devez définir une fois chaque type de retour que 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 ponctuels, vous pouvez également simplement créer un tableau temporaire au 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 utilisez le type d'une table, d'une vue ou d'une vue matérialisée existante si disponible.

Appeler

Utilisation des types de lignes ci-dessus :

Formulaire à 1 paramètre (aucune valeur manquante) :

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

Formulaire à 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 fonction unique fonctionne pour tous les types de retour, tandis que le crosstabN() framework fourni par le tablefunc module a besoin d'une fonction distincte pour chacun.
Si vous avez nommé vos types dans l'ordre comme démontré ci-dessus, vous n'avez qu'à remplacer le nombre en gras. Pour trouver le nombre maximum de catégories dans la table 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 si vous voulez des colonnes individuelles . Des tableaux comme démontré par @Clocoaldo ou une simple représentation textuelle ou le résultat enveloppé dans un type de document comme json ou hstore peut fonctionner pour n'importe quel nombre de catégories de manière dynamique.

Avis de non-responsabilité :
Il est toujours potentiellement dangereux lorsque l'entrée de l'utilisateur est convertie en code. Assurez-vous que cela ne peut pas être utilisé pour l'injection SQL. N'acceptez pas les entrées d'utilisateurs non fiables (directement).

Appel pour la question initiale :

SELECT * FROM crosstab_n('SELECT bar, 1, feh FROM tbl_org ORDER BY 1,2'
                       , NULL::tablefunc_crosstab_int_3);