Requête de tableau croisé PostgreSQL

196

Quelqu'un sait-il comment créer des requêtes de tableau croisé dans PostgreSQL?
Par exemple, j'ai le tableau suivant:

Section    Status    Count
A          Active    1
A          Inactive  2
B          Active    4
B          Inactive  5

Je souhaite que la requête renvoie le tableau croisé suivant:

Section    Active    Inactive
A          1         2
B          4         5

Est-ce possible?

schone
la source
1
J'avais une structure légèrement différente et j'ai trouvé cet exemple un peu difficile à comprendre, j'ai donc documenté ma façon de penser ce stackoverflow.com/q/49051959/808723 . Peut-être que c'est utile pour n'importe qui.
GameScripting

Réponses:

317

Installez le module supplémentaire tablefunc une fois par base de données, qui fournit la fonction crosstab(). Depuis Postgres 9.1, vous pouvez utiliser CREATE EXTENSIONpour cela:

CREATE EXTENSION IF NOT EXISTS tablefunc;

Cas de test amélioré

CREATE TABLE tbl (
   section   text
 , status    text
 , ct        integer  -- "count" is a reserved word in standard SQL
);

INSERT INTO tbl VALUES 
  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                    , ('C', 'Inactive', 7);  -- ('C', 'Active') is missing

Formulaire simple - ne convient pas aux attributs manquants

crosstab(text)avec 1 paramètre d'entrée:

SELECT *
FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- needs to be "ORDER BY 1,2" here
   ) AS ct ("Section" text, "Active" int, "Inactive" int);

Retour:

Section | Actif | Inactif
--------- + -------- + ----------
 A | 1 | 2
 B | 4 | 5
 C |      7 | - !!
  • Pas besoin de couler et de renommer.
  • Notez le résultat incorrect pour C: la valeur 7est remplie pour la première colonne. Parfois, ce comportement est souhaitable, mais pas pour ce cas d'utilisation.
  • Le formulaire simple est également limité à exactement trois colonnes dans la requête d'entrée fournie: row_name , category , value . Il n'y a pas de place pour des colonnes supplémentaires comme dans l'alternative à 2 paramètres ci-dessous.

Forme sûre

crosstab(text, text)avec 2 paramètres d'entrée:

SELECT *
FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- could also just be "ORDER BY 1" here

  , $$VALUES ('Active'::text), ('Inactive')$$
   ) AS ct ("Section" text, "Active" int, "Inactive" int);

Retour:

Section | Actif | Inactif
--------- + -------- + ----------
 A | 1 | 2
 B | 4 | 5
 C | |        7   - !!
  • Notez le résultat correct pour C.

  • Le deuxième paramètre peut être n'importe quelle requête qui renvoie une ligne par attribut correspondant à l'ordre de la définition de colonne à la fin. Souvent, vous souhaiterez interroger des attributs distincts de la table sous-jacente comme ceci:

    'SELECT DISTINCT attribute FROM tbl ORDER BY 1'

    C'est dans le manuel.

    Comme vous devez de toute façon épeler toutes les colonnes d'une liste de définitions de colonnes (à l'exception des variantes prédéfinies ), il est généralement plus efficace de fournir une courte liste dans une expression comme illustré:crosstabN()VALUES

    $$VALUES ('Active'::text), ('Inactive')$$)

    Ou (pas dans le manuel):

    $$SELECT unnest('{Active,Inactive}'::text[])$$  -- short syntax for long lists
  • J'ai utilisé la cotation en dollars pour faciliter la cotation.

  • Vous pouvez même générer des colonnes avec différents types de données avec crosstab(text, text)- tant que la représentation textuelle de la colonne de valeur est une entrée valide pour le type cible. De cette façon , vous pourriez avoir des attributs de différents types et de sortie text, date, numericetc. pour les attributs respectifs. Il y a un exemple de code à la fin du chapitre crosstab(text, text)du manuel .

db <> violon ici

Exemples avancés


\crosstabview en psql

Postgres 9.6 a ajouté cette méta-commande à son terminal interactif par défaut psql . Vous pouvez exécuter la requête que vous utiliseriez comme premier crosstab()paramètre et la nourrir \crosstabview(immédiatement ou à l'étape suivante). Comme:

db=> SELECT section, status, ct FROM tbl \crosstabview

Résultat similaire à celui ci-dessus, mais il s'agit uniquement d'une fonction de représentation côté client . Les lignes en entrée sont traitées légèrement différemment, elles ORDER BYne sont donc pas nécessaires. Détails pour \crosstabviewdans le manuel. Il y a plus d'exemples de code au bas de cette page.

Réponse connexe sur dba.SE par Daniel Vérité (l'auteur de la fonctionnalité psql):



La réponse précédemment acceptée est obsolète.

  • La variante de la fonction crosstab(text, integer)est obsolète. Le deuxième integerparamètre est ignoré. Je cite le manuel actuel :

    crosstab(text sql, int N) ...

    Version obsolète de crosstab(text). Le paramètre Nest désormais ignoré, car le nombre de colonnes de valeur est toujours déterminé par la requête appelante

  • Casting et renommage inutiles.

  • Il échoue si une ligne n'a pas tous les attributs. Voir la variante sûre avec deux paramètres d'entrée ci-dessus pour gérer correctement les attributs manquants.

  • ORDER BYest requis sous la forme à un paramètre de crosstab(). Le manuel:

    En pratique, la requête SQL doit toujours spécifier ORDER BY 1,2pour garantir que les lignes d'entrée sont correctement ordonnées

Erwin Brandstetter
la source
3
+1, bonne rédaction, merci de l'avoir remarquéIn practice the SQL query should always specify ORDER BY 1,2 to ensure that the input rows are properly ordered
ChristopheD
J'ai quelques problèmes avec $$ VALUES .. $$. J'ai utilisé à la place 'VALUES (' '<attr>' ':: <type>), ..'
Marco Fantasia
Pouvons-nous spécifier la liaison de paramètres dans la requête croisée? Je reçois cette erreur => n'a pas pu déterminer le type de données du paramètre $ 2
Ashish
1
Est-il possible de définir la valeur par défaut de la colonne dans la requête d'analyse croisée?
Ashish
2
@Ashish: Veuillez commencer une nouvelle question. Les commentaires ne sont pas le lieu. Vous pouvez toujours créer un lien vers celui-ci pour le contexte.
Erwin Brandstetter
30

Vous pouvez utiliser la crosstab()fonction du module supplémentaire tablefunc - que vous devez installer une fois par base de données. Depuis PostgreSQL 9.1, vous pouvez utiliserCREATE EXTENSION pour cela:

CREATE EXTENSION tablefunc;

Dans votre cas, je pense que cela ressemblerait à ceci:

CREATE TABLE t (Section CHAR(1), Status VARCHAR(10), Count integer);

INSERT INTO t VALUES ('A', 'Active',   1);
INSERT INTO t VALUES ('A', 'Inactive', 2);
INSERT INTO t VALUES ('B', 'Active',   4);
INSERT INTO t VALUES ('B', 'Inactive', 5);

SELECT row_name AS Section,
       category_1::integer AS Active,
       category_2::integer AS Inactive
FROM crosstab('select section::text, status, count::text from t',2)
            AS ct (row_name text, category_1 text, category_2 text);
Jeremiah Peschka
la source
Si vous utilisez un paramètre dans la requête d'analyse croisée, vous devez l'échapper correctement. Exemple: (vu ci-dessus) dites que vous ne voulez que les actifs: SELECT ... FROM tableau croisé ('select section :: text, status, count :: text from t where status =' 'active' '', 2) AS. .. (remarquez les doubles guillemets). Dans le cas où le paramètre est passé à l'exécution par l'utilisateur (comme paramètre de fonction par exemple), vous pouvez dire: SELECT ... FROM tableau croisé ('select section :: text, status, count :: text from t where status =' ' '|| par_active ||' '' ', 2) AS ... (triple guillemets ici!). Dans BIRT, cela fonctionne également avec le? espace réservé.
Wim Verhavert
26
SELECT section,
       SUM(CASE status WHEN 'Active' THEN count ELSE 0 END) AS active, --here you pivot each status value as a separate column explicitly
       SUM(CASE status WHEN 'Inactive' THEN count ELSE 0 END) AS inactive --here you pivot each status  value as a separate column explicitly

FROM t
GROUP BY section
araqnid
la source
1
Quelqu'un peut-il expliquer ce que la fonction de tableau croisé dans le module tablefunc ajoute à cette réponse, qui fait le travail à la fois, et à mon avis est plus facile à comprendre?
John Powell
4
@ JohnBarça: Un cas simple comme celui-ci peut facilement être résolu avec des instructions CASE. Cependant, cela devient très rapidement difficile à gérer avec plus d'attributs et / ou d'autres types de données que de simples entiers. En aparté: ce formulaire utilise la fonction d'agrégation sum(), il serait préférable d'utiliser min()ou max()et non ELSEqui fonctionne textaussi. Mais cela a des effets subtilement différents de ceux corosstab()qui n'utilisent que la "première" valeur par attribut. Peu importe tant qu'il ne peut y en avoir qu'un. Enfin, les performances sont également pertinentes. crosstab()est écrit en C et optimisé pour la tâche.
Erwin Brandstetter
Cela ne fonctionne pas pour moi, pour postgresql. Je reçois l'erreurERROR: 42803: aggregate function calls may not be nested
Audrey
1
@Audrey, vous n'exécutez pas le même SQL alors?
2
Envisagez d'ajouter une explication par rapport à un simple bloc de code
Daniel L. VanDenBosch
10

Solution avec agrégation JSON:

CREATE TEMP TABLE t (
  section   text
, status    text
, ct        integer  -- don't use "count" as column name.
);

INSERT INTO t VALUES 
  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                   , ('C', 'Inactive', 7); 


SELECT section,
       (obj ->> 'Active')::int AS active,
       (obj ->> 'Inactive')::int AS inactive
FROM (SELECT section, json_object_agg(status,ct) AS obj
      FROM t
      GROUP BY section
     )X
Milos
la source
Merci, cela m'a aidé avec un problème connexe.
JeffCharter
1

Désolé, ce n'est pas complet car je ne peux pas le tester ici, mais cela peut vous faire avancer dans la bonne direction. Je traduis à partir de quelque chose que j'utilise qui fait une requête similaire:

select mt.section, mt1.count as Active, mt2.count as Inactive
from mytable mt
left join (select section, count from mytable where status='Active')mt1
on mt.section = mt1.section
left join (select section, count from mytable where status='Inactive')mt2
on mt.section = mt2.section
group by mt.section,
         mt1.count,
         mt2.count
order by mt.section asc;

Le code à partir duquel je travaille est:

select m.typeID, m1.highBid, m2.lowAsk, m1.highBid - m2.lowAsk as diff, 100*(m1.highBid - m2.lowAsk)/m2.lowAsk as diffPercent
from mktTrades m
   left join (select typeID,MAX(price) as highBid from mktTrades where bid=1 group by typeID)m1
   on m.typeID = m1.typeID
   left join (select typeID,MIN(price) as lowAsk  from mktTrades where bid=0 group by typeID)m2
   on m1.typeID = m2.typeID
group by m.typeID, 
         m1.highBid, 
         m2.lowAsk
order by diffPercent desc;

qui renverra un typeID, l'offre de prix la plus élevée et le prix le plus bas demandé et la différence entre les deux (une différence positive signifierait que quelque chose pourrait être acheté moins cher qu'il ne peut être vendu).

LanceH
la source
1
Vous manquez une clause from, sinon c'est correct. Les plans d'explication sont très différents sur mon système - la fonction de tableau croisé a un coût de 22,5 tandis que l'approche LEFT JOIN est environ 4 fois plus chère avec un coût de 91,38. Il produit également environ deux fois plus de lectures physiques et effectue des jointures de hachage - ce qui peut être assez coûteux par rapport aux autres types de jointures.
Jeremiah Peschka
Merci Jeremiah, c'est bon à savoir. J'ai voté pour l'autre réponse, mais votre commentaire mérite d'être conservé, je ne supprimerai donc pas celle-ci.
LanceH
-1

Crosstabest disponible sous l' tablefuncextension. Vous devrez créer cette extension une fois pour la base de données.

CRÉER UNE EXTENSION tablefunc;

Vous pouvez utiliser le code ci-dessous pour créer un tableau croisé dynamique à l'aide du tableau croisé:

create table test_Crosstab( section text,
<br/>status text,
<br/>count numeric)

<br/>insert into test_Crosstab values ( 'A','Active',1)
                <br/>,( 'A','Inactive',2)
                <br/>,( 'B','Active',4)
                <br/>,( 'B','Inactive',5)

select * from crosstab(
<br/>'select section
    <br/>,status
    <br/>,count
    <br/>from test_crosstab'
    <br/>)as ctab ("Section" text,"Active" numeric,"Inactive" numeric)
Lekshmi Kurup
la source
1
Cette réponse n'ajoute rien aux réponses préexistantes.
Erwin Brandstetter