MySQL - Des lignes aux colonnes

188

J'ai essayé de rechercher des articles, mais je n'ai trouvé que des solutions pour SQL Server / Access. J'ai besoin d'une solution dans MySQL (5.X).

J'ai une table (appelée historique) avec 3 colonnes: hostid, itemname, itemvalue.
Si je fais un select ( select * from history), il reviendra

   +--------+----------+-----------+
   | hostid | itemname | itemvalue |
   +--------+----------+-----------+
   |   1    |    A     |    10     |
   +--------+----------+-----------+
   |   1    |    B     |     3     |
   +--------+----------+-----------+
   |   2    |    A     |     9     |
   +--------+----------+-----------+
   |   2    |    c     |    40     |
   +--------+----------+-----------+

Comment interroger la base de données pour renvoyer quelque chose comme

   +--------+------+-----+-----+
   | hostid |   A  |  B  |  C  |
   +--------+------+-----+-----+
   |   1    |  10  |  3  |  0  |
   +--------+------+-----+-----+
   |   2    |   9  |  0  |  40 |
   +--------+------+-----+-----+
Bob Rivers
la source
@Rob, pouvez-vous modifier la question pour inclure la requête exacte?
Johan
REMARQUE: le lien de @ako ne concerne que MariaDB.
ToolmakerSteve
Génération automatique et exécution d'un pivot: mysql.rjweb.org/doc.php/pivot
Rick James

Réponses:

276

Je vais ajouter une explication un peu plus longue et plus détaillée des étapes à suivre pour résoudre ce problème. Je m'excuse si c'est trop long.


Je vais commencer par la base que vous avez donnée et l'utiliser pour définir quelques termes que j'utiliserai pour le reste de cet article. Ce sera la table de base :

select * from history;

+--------+----------+-----------+
| hostid | itemname | itemvalue |
+--------+----------+-----------+
|      1 | A        |        10 |
|      1 | B        |         3 |
|      2 | A        |         9 |
|      2 | C        |        40 |
+--------+----------+-----------+

Ce sera notre objectif, le joli tableau croisé dynamique :

select * from history_itemvalue_pivot;

+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 |    0 |
|      2 |    9 |    0 |   40 |
+--------+------+------+------+

Les valeurs de la history.hostidcolonne deviendront des valeurs y dans le tableau croisé dynamique. Les valeurs de la history.itemnamecolonne deviendront des valeurs x (pour des raisons évidentes).


Lorsque je dois résoudre le problème de la création d'un tableau croisé dynamique, je l'aborde en utilisant un processus en trois étapes (avec une quatrième étape facultative):

  1. sélectionnez les colonnes d'intérêt, c'est -à- dire les valeurs y et les valeurs x
  2. étendre la table de base avec des colonnes supplémentaires - une pour chaque valeur x
  3. grouper et agréger la table étendue - un groupe pour chaque valeur y
  4. (facultatif) joliment la table agrégée

Appliquons ces étapes à votre problème et voyons ce que nous obtenons:

Étape 1: sélectionnez les colonnes d'intérêt . Dans le résultat souhaité, hostidfournit les valeurs y et itemnamefournit les valeurs de x .

Étape 2: prolongez la table de base avec des colonnes supplémentaires . Nous avons généralement besoin d'une colonne par valeur x. Rappelez-vous que notre colonne de valeur x est itemname:

create view history_extended as (
  select
    history.*,
    case when itemname = "A" then itemvalue end as A,
    case when itemname = "B" then itemvalue end as B,
    case when itemname = "C" then itemvalue end as C
  from history
);

select * from history_extended;

+--------+----------+-----------+------+------+------+
| hostid | itemname | itemvalue | A    | B    | C    |
+--------+----------+-----------+------+------+------+
|      1 | A        |        10 |   10 | NULL | NULL |
|      1 | B        |         3 | NULL |    3 | NULL |
|      2 | A        |         9 |    9 | NULL | NULL |
|      2 | C        |        40 | NULL | NULL |   40 |
+--------+----------+-----------+------+------+------+

Notez que nous n'avons pas changé le nombre de lignes - nous avons simplement ajouté des colonnes supplémentaires. Notez également le modèle de NULLs - une ligne avec itemname = "A"a une valeur non nulle pour la nouvelle colonne Aet des valeurs nulles pour les autres nouvelles colonnes.

Étape 3: regroupez et agrégez la table étendue . Nous devons group by hostid, car il fournit les valeurs y:

create view history_itemvalue_pivot as (
  select
    hostid,
    sum(A) as A,
    sum(B) as B,
    sum(C) as C
  from history_extended
  group by hostid
);

select * from history_itemvalue_pivot;

+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 | NULL |
|      2 |    9 | NULL |   40 |
+--------+------+------+------+

(Notez que nous avons maintenant une ligne par valeur y.) D'accord, nous y sommes presque! Nous devons juste nous débarrasser de ces horribles NULLs.

Étape 4: joliment . Nous allons simplement remplacer toutes les valeurs nulles par des zéros afin que le jeu de résultats soit plus agréable à regarder:

create view history_itemvalue_pivot_pretty as (
  select 
    hostid, 
    coalesce(A, 0) as A, 
    coalesce(B, 0) as B, 
    coalesce(C, 0) as C 
  from history_itemvalue_pivot 
);

select * from history_itemvalue_pivot_pretty;

+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 |    0 |
|      2 |    9 |    0 |   40 |
+--------+------+------+------+

Et nous avons terminé - nous avons construit un joli tableau croisé dynamique en utilisant MySQL.


Considérations lors de l'application de cette procédure:

  • quelle valeur utiliser dans les colonnes supplémentaires. J'ai utilisé itemvaluedans cet exemple
  • quelle valeur «neutre» utiliser dans les colonnes supplémentaires. J'ai utilisé NULL, mais cela pourrait aussi être 0ou "", selon votre situation exacte
  • quelle fonction d'agrégat utiliser lors du regroupement. J'ai utilisé sum, mais countet maxsont aussi souvent utilisés ( maxest souvent utilisé lors de la création d '"objets" à une ligne qui avaient été répartis sur plusieurs lignes)
  • en utilisant plusieurs colonnes pour les valeurs y. Cette solution ne se limite pas à utiliser une seule colonne pour les valeurs y - branchez simplement les colonnes supplémentaires dans la group byclause (et ne les oubliez selectpas)

Limitations connues:

  • cette solution n'autorise pas n colonnes dans le tableau croisé dynamique - chaque colonne pivot doit être ajoutée manuellement lors de l'extension de la table de base. Donc pour 5 ou 10 valeurs x, cette solution est sympa. Pour 100, pas si gentil. Il existe certaines solutions avec des procédures stockées générant une requête, mais elles sont laides et difficiles à obtenir. Je ne connais actuellement pas de bon moyen de résoudre ce problème lorsque le tableau croisé dynamique a besoin de beaucoup de colonnes.
Matt Fenwick
la source
25
+1 C'est de loin l'explication la meilleure / la plus claire des tableaux croisés
dynamiques
6
Excellente explication, merci. L'étape 4 pourrait être fusionnée à l'étape 3 en utilisant IFNULL (sum (A), 0) AS A, vous donnant le même résultat mais sans avoir besoin de créer encore une autre table
nealio82
1
C'était la solution la plus étonnante pour le pivot, mais je suis juste curieux de savoir si dans la colonne itemname qui forme l'axe des x a plusieurs valeurs, comme ici nous n'avons que trois valeurs à savoir A, B, C.Si ces valeurs s'étendent à A, B, C, D, E, AB, BC, AC, AD, H ..... n. alors dans ce cas quelle serait la solution.
Deepesh
1
cela devrait vraiment être la réponse acceptée ici. C'est beaucoup plus détaillé, utile et explique comment le comprendre plutôt que de simplement
créer
2
@WhiteBig s'il vous plaît jeter un oeil aux dates - cette réponse StackOverflow a été écrite 1,5 ans avant ce billet de blog. Peut-être devriez-vous plutôt demander au blog de me créditer.
Matt Fenwick
55
SELECT 
    hostid, 
    sum( if( itemname = 'A', itemvalue, 0 ) ) AS A,  
    sum( if( itemname = 'B', itemvalue, 0 ) ) AS B, 
    sum( if( itemname = 'C', itemvalue, 0 ) ) AS C 
FROM 
    bob 
GROUP BY 
    hostid;
shantanuo
la source
Crée trois lignes différentes, pour «A», «B», «C»
Palani
1
@Palani: Non, ce n'est pas le cas. Voir group by.
ruakh
Merci, cela a fonctionné pour moi! Cependant, juste un FYI quelques années de retard, j'ai dû utiliser à la MAXplace de SUMparce que mes itemValuechaînes sont des chaînes, pas des valeurs numériques.
Merricat le
33

Une autre option, particulièrement utile si vous avez de nombreux éléments à faire pivoter, est de laisser mysql construire la requête pour vous:

SELECT
  GROUP_CONCAT(DISTINCT
    CONCAT(
      'ifnull(SUM(case when itemname = ''',
      itemname,
      ''' then itemvalue end),0) AS `',
      itemname, '`'
    )
  ) INTO @sql
FROM
  history;
SET @sql = CONCAT('SELECT hostid, ', @sql, ' 
                  FROM history 
                   GROUP BY hostid');

PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

FIDDLE a ajouté quelques valeurs supplémentaires pour le voir fonctionner

GROUP_CONCAT a une valeur par défaut de 1000 donc si vous avez une très grosse requête, modifiez ce paramètre avant de l'exécuter

SET SESSION group_concat_max_len = 1000000;

Tester:

DROP TABLE IF EXISTS history;
CREATE TABLE history
(hostid INT,
itemname VARCHAR(5),
itemvalue INT);

INSERT INTO history VALUES(1,'A',10),(1,'B',3),(2,'A',9),
(2,'C',40),(2,'D',5),
(3,'A',14),(3,'B',67),(3,'D',8);

  hostid    A     B     C      D
    1     10      3     0      0
    2     9       0    40      5
    3     14     67     0      8
Mihai
la source
@Mihai Vous pouvez peut-être m'aider. Regardez ceci: stackoverflow.com/questions/51832979/…
Success Man
Peut simplifier 'ifnull(SUM(case when itemname = ''',avec ''' then itemvalue end),0) AS «,» 'SUM(case when itemname = '''avec ''' then itemvalue else 0 end) AS ',. Cela génère des termes comme SUM(case when itemname = 'A' then itemvalue else 0 end) AS 'A'.
ToolmakerSteve
24

Profitant de l'idée de Matt Fenwick qui m'a aidé à résoudre le problème (merci beaucoup), réduisons-le à une seule requête:

select
    history.*,
    coalesce(sum(case when itemname = "A" then itemvalue end), 0) as A,
    coalesce(sum(case when itemname = "B" then itemvalue end), 0) as B,
    coalesce(sum(case when itemname = "C" then itemvalue end), 0) as C
from history
group by hostid
jalber
la source
14

Je modifier Agung Sagita de » réponse de sous - requête à se joindre. Je ne suis pas sûr de la différence entre ces deux méthodes, mais juste pour une autre référence.

SELECT  hostid, T2.VALUE AS A, T3.VALUE AS B, T4.VALUE AS C
FROM TableTest AS T1
LEFT JOIN TableTest T2 ON T2.hostid=T1.hostid AND T2.ITEMNAME='A'
LEFT JOIN TableTest T3 ON T3.hostid=T1.hostid AND T3.ITEMNAME='B'
LEFT JOIN TableTest T4 ON T4.hostid=T1.hostid AND T4.ITEMNAME='C'
haudoing
la source
2
Peut-être que cela pourrait être une solution plus rapide.
jave.web
Je ne pense pas. car la jointure gauche a sa propre latence!
Abadis
10

utiliser une sous-requête

SELECT  hostid, 
    (SELECT VALUE FROM TableTest WHERE ITEMNAME='A' AND hostid = t1.hostid) AS A,
    (SELECT VALUE FROM TableTest WHERE ITEMNAME='B' AND hostid = t1.hostid) AS B,
    (SELECT VALUE FROM TableTest WHERE ITEMNAME='C' AND hostid = t1.hostid) AS C
FROM TableTest AS T1
GROUP BY hostid

mais ce sera un problème si la sous-requête résultant plus d'une ligne, utilisez une fonction d'agrégation supplémentaire dans la sous-requête

Agung Sagita
la source
4

Ma solution:

select h.hostid, sum(ifnull(h.A,0)) as A, sum(ifnull(h.B,0)) as B, sum(ifnull(h.C,0)) as  C from (
select
hostid,
case when itemName = 'A' then itemvalue end as A,
case when itemName = 'B' then itemvalue end as B,
case when itemName = 'C' then itemvalue end as C
  from history 
) h group by hostid

Il produit les résultats attendus dans le cas soumis.

André Wéber
la source
3

Je fais cela en Group By hostIdalors il affichera seulement la première ligne avec des valeurs,
comme:

A   B  C
1  10
2      3
arpit
la source
3

Je trouve un moyen de rendre mes rapports convertissant des lignes en colonnes presque dynamiques à l'aide de requêtes simples. Vous pouvez le voir et le tester en ligne ici .

Le nombre de colonnes de requête est fixe mais les valeurs sont dynamiques et basées sur des valeurs de lignes. Vous pouvez le créer.J'utilise donc une requête pour créer l'en-tête de la table et une autre pour voir les valeurs:

SELECT distinct concat('<th>',itemname,'</th>') as column_name_table_header FROM history order by 1;

SELECT
     hostid
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 0,1) then itemvalue else '' end) as col1
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 1,1) then itemvalue else '' end) as col2
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 2,1) then itemvalue else '' end) as col3
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 3,1) then itemvalue else '' end) as col4
FROM history order by 1;

Vous pouvez également le résumer:

SELECT
     hostid
    ,sum(case when itemname = (select distinct itemname from history a order by 1 limit 0,1) then itemvalue end) as A
    ,sum(case when itemname = (select distinct itemname from history a order by 1 limit 1,1) then itemvalue end) as B
    ,sum(case when itemname = (select distinct itemname from history a order by 1 limit 2,1) then itemvalue end) as C
FROM history group by hostid order by 1;
+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 | NULL |
|      2 |    9 | NULL |   40 |
+--------+------+------+------+

Résultats de RexTester :

Résultats de RexTester

http://rextester.com/ZSWKS28923

Pour un vrai exemple d'utilisation, ce reportage ci-dessous affiche en colonnes les heures de départs arrivées de bateau / bus avec un horaire visuel. Vous verrez une colonne supplémentaire non utilisée au dernier col sans confondre la visualisation: sistema venda de passagens online e consumidor final e controle de frota - xsl tecnologia - xsl.com.br ** système de billetterie pour vendre des billets en ligne et présentiel

lynx_74
la source
3

Si vous pouviez utiliser MariaDB, il existe une solution très très simple.

Depuis MariaDB-10.02 , un nouveau moteur de stockage appelé CONNECT a été ajouté qui peut nous aider à convertir les résultats d'une autre requête ou table en un tableau croisé dynamique, comme vous le souhaitez: vous pouvez consulter la documentation .

Tout d'abord, installez le moteur de stockage connect .

Maintenant, la colonne pivot de notre tableau est itemnameet les données de chaque élément sont situées dans la itemvaluecolonne, nous pouvons donc avoir le tableau croisé dynamique des résultats en utilisant cette requête:

create table pivot_table
engine=connect table_type=pivot tabname=history
option_list='PivotCol=itemname,FncCol=itemvalue';

Maintenant, nous pouvons sélectionner ce que nous voulons dans pivot_table:

select * from pivot_table

Plus de détails ici

ako
la source
1

Ce n'est pas la réponse exacte que vous recherchez, mais c'était une solution dont j'avais besoin pour mon projet et j'espère que cela aidera quelqu'un. Cela listera 1 à n éléments de ligne séparés par des virgules. Group_Concat rend cela possible dans MySQL.

select
cemetery.cemetery_id as "Cemetery_ID",
GROUP_CONCAT(distinct(names.name)) as "Cemetery_Name",
cemetery.latitude as Latitude,
cemetery.longitude as Longitude,
c.Contact_Info,
d.Direction_Type,
d.Directions

    from cemetery
    left join cemetery_names on cemetery.cemetery_id = cemetery_names.cemetery_id 
    left join names on cemetery_names.name_id = names.name_id 
    left join cemetery_contact on cemetery.cemetery_id = cemetery_contact.cemetery_id 

    left join 
    (
        select 
            cemetery_contact.cemetery_id as cID,
            group_concat(contacts.name, char(32), phone.number) as Contact_Info

                from cemetery_contact
                left join contacts on cemetery_contact.contact_id = contacts.contact_id 
                left join phone on cemetery_contact.contact_id = phone.contact_id 

            group by cID
    )
    as c on c.cID = cemetery.cemetery_id


    left join
    (
        select 
            cemetery_id as dID, 
            group_concat(direction_type.direction_type) as Direction_Type,
            group_concat(directions.value , char(13), char(9)) as Directions

                from directions
                left join direction_type on directions.type = direction_type.direction_type_id

            group by dID


    )
    as d on d.dID  = cemetery.cemetery_id

group by Cemetery_ID

Ce cimetière a deux noms communs, donc les noms sont répertoriés dans différentes lignes reliées par un seul identifiant mais deux identifiants de nom et la requête produit quelque chose comme ce

    CemeteryID Cemetery_Name Latitude
    1 Appleton, Sulpher Springs 35.4276242832293

James Humphrey
la source
-2

Je suis désolé de le dire et peut-être que je ne résous pas votre problème exactement, mais PostgreSQL a 10 ans de plus que MySQL et est extrêmement avancé par rapport à MySQL et il existe de nombreuses façons d'y parvenir facilement. Installez PostgreSQL et exécutez cette requête

CREATE EXTENSION tablefunc;

alors voila! Et voici une documentation complète: PostgreSQL: Documentation: 9.1: tablefunc ou cette requête

CREATE EXTENSION hstore;

puis encore voila! PostgreSQL: Documentation: 9.0: hstore

gdarcan
la source