web-dev-qa-db-fra.com

MySQL - Des lignes aux colonnes

J'ai essayé de rechercher des publications, 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 history) avec 3 colonnes: hostid, itemname, itemvalue.
Si je fais un select (select * from history), il retournera

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

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

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

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


Je vais commencer par la base que vous avez fournie et l'utiliser pour définir quelques termes que je vais utiliser pour le reste de cet article. Ce sera le 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 colonne history.hostid deviendront valeurs y dans le tableau croisé dynamique. Les valeurs de la colonne history.itemname deviendront 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 qui vous intéressent, c'est-à-dire valeurs y et 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. (optionnel) embellir le tableau agrégé

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

Étape 1: sélectionner les colonnes d'intérêt. Dans le résultat souhaité, hostid fournit les valeurs y et itemname fournit les valeurs x.

Étape 2: étendre la table de base avec des colonnes supplémentaires. Nous avons généralement besoin d'une colonne par valeur x. Rappelons que notre colonne x-value 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 que le modèle NULLs - une ligne contenant itemname = "A" a une valeur non nulle pour la nouvelle colonne A et des valeurs nulles pour les autres nouvelles colonnes.

Étape 3: regrouper et agréger 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 avons juste besoin de nous débarrasser de ces laids NULLs.

étape 4: faire plaisir. Nous allons simplement remplacer les valeurs nulles par des zéros afin que l'ensemble 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é itemvalue dans cet exemple
  • quelle valeur "neutre" à utiliser dans les colonnes supplémentaires. J'ai utilisé NULL, mais il pourrait aussi s'agir de 0 ou "", selon votre situation exacte.
  • quelle fonction d'agrégat à utiliser lors du regroupement. J'ai utilisé sum, mais count et max sont également souvent utilisés (max est souvent utilisé lors de la création "d'objets" à une ligne 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 - il suffit de brancher les colonnes supplémentaires dans la clause group by (et de ne pas oublier de select les)

Limites 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 du tableau de base. Donc, pour 5 ou 10 valeurs x, cette solution est Nice. 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 à résoudre. Je ne connais pas actuellement un bon moyen de résoudre ce problème lorsque le tableau croisé dynamique doit comporter de nombreuses colonnes.
238
Matt Fenwick
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;
42
shantanuo

Une autre option, particulièrement utile si vous avez plusieurs éléments à pivoter, est de laisser mysql créer 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 Ajout de quelques valeurs supplémentaires pour le voir fonctionner

GROUP_CONCAT a une valeur par défaut de 1000. Si vous avez une très grosse requête, changez 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
25
Mihai

Profitant de l'idée de Matt Fenwick qui m'a aidé à résoudre le problème (merci beaucoup), réduisons-la à 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
21
jalber

Je modifie la réponse de Agung Sagita de sous-requête à rejoindre. Je ne suis pas sûr de la différence entre cette façon, 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'
11
haudoing

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 une sous-requête résultant plus d'une ligne, utilisez une fonction d'agrégation supplémentaire dans la sous-requête

8
Agung Sagita

Je fais cela dans Group By hostId alors il ne montrera que la première ligne avec des valeurs,
comme:

A   B  C
1  10
2      3
3
arpit

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.

3
André Wéber

Je suis désolé de le dire et je ne résous peut-être 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 de le résoudre 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;

là encore voila! PostgreSQL: Documentation: 9.0: hstore

2
gdarcan

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

Le nombre de colonnes de requête est fixé mais les valeurs sont dynamiques et basé sur les valeurs des lignes. Vous pouvez le construire Donc, j'utilise une requête pour construire 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 aussi 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 :

Results of RexTester

http://rextester.com/ZSWKS2892

Pour un exemple réel d'utilisation, le présent rapport ci-dessous montre sous forme de colonnes les heures de départs des bateaux/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

2
lynx_74

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 peut nous aider à convertir les résultats de une autre requête ou table dans un tableau croisé dynamique, comme ce que vous voulez: Vous pouvez jeter un oeil à la documentation .

Tout d'abord installez le moteur de stockage connect .

Maintenant, la colonne pivot de notre table est itemname et les données pour chaque élément sont situées dans la colonne itemvalue. Vous pouvez ainsi obtenir le tableau croisé dynamique résultant à l'aide de 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 le pivot_table:

select * from pivot_table

Plus de détails ici

1
ako

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. Ceci 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. Les noms sont donc répertoriés dans des rangées différentes connectées par un identifiant unique mais deux identifiants de noms. La requête produit un résultat similaire.

CemeteryID Cemetery_Name Latitude
1 Appleton, Sulpher Springs 35.4276242832293

1
James Humphrey