web-dev-qa-db-fra.com

Requête d'analyse croisée PostgreSQL

Est-ce que quelqu'un sait comment créer des requêtes d'analyse croisée 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?

172
schone

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 EXTENSION pour 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 :

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

Résultats:

 Section | Actif | Inactif 
 --------- + -------- + ---------- 
 A | 1 | 2 
 B | 4 | 5 
 C | sept | - !! 
  • Pas besoin de casting et de renommer.
  • Notez que le résultat incorrect pour C: la valeur _7_ est renseigné 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: nom_ligne , catégorie , valeur . 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);_

Résultats:

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

  • Le deuxième paramètre peut être toute requête renvoyant une ligne par attribut correspondant à l'ordre de la définition de la colonne à la fin. Vous voudrez souvent interroger des attributs distincts de la table sous-jacente comme ceci:

    _'SELECT DISTINCT attribute FROM tbl ORDER BY 1'
    _

    C'est dans le manuel.

    Étant donné que vous devez malgré tout épeler toutes les colonnes de la liste de définition de colonne (à l'exception des variantes crosstabN() prédéfinies), il est généralement plus efficace de fournir une liste courte dans une expression VALUES comme illustré:

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

    Ou (pas dans le manuel):

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

  • Vous pouvez même éditer des colonnes avec différents types de données avec crosstab(text, text) - tant que la représentation textuelle du La colonne value est une entrée valide pour le type de cible. De cette façon, vous pourriez avoir des attributs de type et de sortie différents text, date, numeric etc. pour les attributs respectifs. Il existe un exemple de code à la fin de chapitre crosstab(text, text) dans le 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 paramètre crosstab() et le transmettre à _\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 d’une fonction de représentation côté client exclusivement. Les lignes en entrée sont traitées légèrement différemment. Par conséquent, _ORDER BY_ n'est pas requis. Détails pour _\crosstabview_ dans le manuel. Il y a plus d'exemples de code au bas de cette page.

Réponse associée sur dba.SE de Daniel Vérité (l'auteur de la fonction psql):



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

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

    crosstab(text sql, int N) ...

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

  • Inutile casting et renommer.

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

  • _ORDER BY_ est requis sous la forme à un paramètre de crosstab(). Le manuel:

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

288
Erwin Brandstetter

Vous pouvez utiliser la fonction crosstab() du module supplémentaire tablefunc - que vous devez installer une fois par base de données. Depuis PostgreSQL 9.1, vous pouvez utiliser CREATE 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);
_
27
Jeremiah Peschka
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
23
araqnid

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
4
Milos

Désolé, ce n'est pas complet parce que je ne peux pas le tester ici, mais cela pourrait vous faire avancer dans la bonne direction. Je traduis 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 sur lequel 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 la plus élevée et le prix le plus bas demandé, ainsi que la différence entre les deux (une différence positive signifierait que quelque chose pourrait être acheté à un prix inférieur à ce qu'il peut être vendu).

1
LanceH