MySQL - Righe su colonne


188

Ho provato a cercare post, ma ho trovato solo soluzioni per SQL Server / Access. Ho bisogno di una soluzione in MySQL (5.X).

Ho una tabella (chiamata cronologia) con 3 colonne: hostid, nome oggetto, valore oggetto.
Se faccio un select ( select * from history), tornerà

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

Come posso interrogare il database per restituire qualcosa del genere

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

@Rob, puoi modificare la domanda per includere la query esatta?
— Johan,


NOTA: il link di @ako è rilevante solo per MariaDB.
— ToolmakerSteve

Generazione automatica e gestione di un perno: mysql.rjweb.org/doc.php/pivot
— Rick James,

Risposte:


276

Ho intenzione di aggiungere una spiegazione un po 'più lunga e dettagliata dei passi da fare per risolvere questo problema. Mi scuso se è troppo lungo.


Inizierò con la base che hai fornito e lo userò per definire un paio di termini che userò per il resto di questo post. Questa sarà la tabella di base :

select * from history;

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

Questo sarà il nostro obiettivo, la bella tabella pivot :

select * from history_itemvalue_pivot;

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

I valori nella history.hostidcolonna diventeranno valori y nella tabella pivot. I valori nella history.itemnamecolonna diventeranno valori x (per ovvi motivi).


Quando devo risolvere il problema della creazione di una tabella pivot, la affronto utilizzando un processo in tre passaggi (con un quarto passaggio facoltativo):

  1. selezionare le colonne di interesse, vale a dire valori y e valori x
  2. estendi la tabella di base con colonne aggiuntive, una per ogni valore x
  3. raggruppa e aggrega la tabella estesa - un gruppo per ciascun valore y
  4. (facoltativo) preimpostare la tabella aggregata

Applichiamo questi passaggi al tuo problema e vediamo cosa otteniamo:

Passaggio 1: selezionare le colonne di interesse . Nel risultato desiderato, hostidfornisce i valori y e itemnamefornisce i valori x .

Passaggio 2: estendere la tabella di base con colonne aggiuntive . In genere è necessaria una colonna per valore x. Ricordiamo che la nostra colonna di valore x è 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 |
+--------+----------+-----------+------+------+------+

Nota che non abbiamo modificato il numero di righe, abbiamo solo aggiunto colonne extra. Nota anche il modello di NULLs: una riga con itemname = "A"ha un valore non nullo per la nuova colonna Ae valori nulli per le altre nuove colonne.

Passaggio 3: raggruppare e aggregare la tabella estesa . Dobbiamo group by hostid, poiché fornisce i valori 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 |
+--------+------+------+------+

(Nota che ora abbiamo una riga per valore y.) Okay, ci siamo quasi! Dobbiamo solo sbarazzarci di quei brutti NULL.

Passaggio 4: prettify . Sostituiremo semplicemente i valori null con zero in modo che il set di risultati sia più bello da guardare:

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 |
+--------+------+------+------+

E abbiamo finito: abbiamo creato una tabella pivot bella e carina usando MySQL.


Considerazioni sull'applicazione di questa procedura:

  • quale valore usare nelle colonne extra. Ho usato itemvaluein questo esempio
  • quale valore "neutro" usare nelle colonne extra. Ho usato NULL, ma potrebbe anche essere 0o "", a seconda della tua situazione esatta
  • quale funzione aggregata usare durante il raggruppamento. Ho usato sum, ma counte maxsono anche spesso usati ( maxè spesso usato quando si costruiscono "oggetti" su una riga che erano stati distribuiti su molte righe)
  • utilizzando più colonne per i valori y. Questa soluzione non si limita all'utilizzo di una singola colonna per i valori y: basta inserire le colonne extra nella group byclausola (e non dimenticarle select)

Limitazioni note:

  • questa soluzione non consente n colonne nella tabella pivot: ogni colonna pivot deve essere aggiunta manualmente quando si estende la tabella di base. Quindi, per 5 o 10 valori x, questa soluzione è bella. Per 100, non così bello. Esistono alcune soluzioni con procedure memorizzate che generano una query, ma sono brutte e difficili da ottenere. Al momento non conosco un buon modo per risolvere questo problema quando la tabella pivot deve contenere molte colonne.

25
+1 Questa è di gran lunga la spiegazione migliore / più chiara delle tabelle pivot / tabulazioni incrociate in MySQL che ho visto
— cameron.bracken

6
Ottima spiegazione, grazie. Il passaggio 4 può essere unito al passaggio 3 utilizzando IFNULL (somma (A), 0) AS A, ottenendo lo stesso risultato ma senza la necessità di creare un'altra tabella
— nealio82

1
È stata la soluzione più incredibile per pivot, ma sono solo curioso di sapere se nella colonna nome oggetto che forma l'asse x ha più valori, come qui abbiamo solo tre valori, ovvero A, B, C.Se questi valori si estendono ad A, B, C, D, E, AB, BC, AC, AD, H ..... n. quindi in questo caso quale sarebbe la soluzione.
— Deepesh,

1
questa dovrebbe davvero essere la risposta accettata qui. È molto più dettagliato, utile e spiega come comprenderlo piuttosto che collegarsi ad un articolo come quello attualmente accettato
— EdgeCaseBerg

2
@Bianco, per favore, date un'occhiata alle date: questa risposta StackOverflow è stata scritta 1,5 anni prima di quel post sul blog. Forse dovresti invece chiedere al blog di riconoscermi.
— 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;

Crea tre diverse righe, per "A", "B", "C"
— Palani

1
@Palani: No, non lo è. Vedere group by.
— Ruakh,

Grazie, ha funzionato per me! Tuttavia, solo un FYI con un paio di anni di ritardo, ho dovuto usare MAXinvece SUMperché le mie itemValuesono stringhe, non valori numerici.
— Merricat,

33

Un'altra opzione, particolarmente utile se hai molti elementi che devi ruotare, è quella di consentire a mysql di creare la query per te:

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 Aggiunti alcuni valori extra per vederlo funzionare

GROUP_CONCAT ha un valore predefinito di 1000, quindi se hai una query molto grande modifica questo parametro prima di eseguirlo

SET SESSION group_concat_max_len = 1000000;

Test:

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 Forse mi puoi aiutare. Guarda questo: stackoverflow.com/questions/51832979/…
— Success Man

Può semplificare 'ifnull(SUM(case when itemname = ''',con ''' then itemvalue end),0) AS "," 'SUM(case when itemname = '''con ''' then itemvalue else 0 end) AS ',. Questo produce termini come SUM(case when itemname = 'A' then itemvalue else 0 end) AS 'A'.
— ToolmakerSteve

24

Sfruttando l'idea di Matt Fenwick che mi ha aiutato a risolvere il problema (molte grazie), riduciamolo a una sola query:

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

14

Modifica la risposta di Agung Sagita dalla subquery per unirmi. Non sono sicuro di quanta differenza tra questo 2 modo, ma solo per un altro riferimento.

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'

2
Forse, questa potrebbe essere una soluzione più veloce.
— jave.web,

Non la penso così. perché left join ha una sua latenza!
— Abadis,

10

usa subquery

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

ma sarà un problema se la sub query risulta più di una riga, usa un'ulteriore funzione aggregata nella subquery


4

La mia soluzione:

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

Produce i risultati previsti nel caso presentato.


3

Lo trasformo in Group By hostIdpoi mostrerà solo la prima riga con valori,
come:

A   B  C
1  10
2      3

3

Ho escogitato un modo per rendere i miei rapporti convertendo le righe in colonne quasi dinamiche usando semplici query. Puoi vederlo e testarlo online qui .

Il numero di colonne della query è fisso ma i valori sono dinamici e basati sui valori delle righe. Puoi costruirlo Quindi, uso una query per creare l'intestazione della tabella e un'altra per vedere i valori:

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;

Puoi anche riassumerlo:

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 |
+--------+------+------+------+

Risultati di RexTester :

Risultati di RexTester

http://rextester.com/ZSWKS28923

Per un vero esempio di utilizzo, questo rapporto mostra in colonne le ore di partenza degli arrivi di barca / autobus con un programma visivo. Vedrai una colonna aggiuntiva non utilizzata all'ultimo col senza confondere la visualizzazione: sistema venda de passagens online e consumer final e controle de frota - xsl tecnologia - xsl.com.br ** sistema di ticketing per vendere biglietti online e presentali


3

Se potessi usare MariaDB c'è una soluzione molto semplice.

Da MariaDB-10.02 è stato aggiunto un nuovo motore di archiviazione chiamato CONNECT che può aiutarci a convertire i risultati di un'altra query o tabella in una tabella pivot, proprio come quello che vuoi: puoi dare un'occhiata ai documenti .

Prima di tutto installa il motore di archiviazione connect .

Ora la colonna pivot della nostra tabella è itemnamee i dati per ogni articolo si trovano nella itemvaluecolonna, quindi possiamo avere la tabella pivot del risultato usando questa query:

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

Ora possiamo selezionare ciò che vogliamo da pivot_table:

select * from pivot_table

Maggiori dettagli qui


1

Questa non è la risposta esatta che stai cercando, ma era una soluzione di cui avevo bisogno nel mio progetto e spero che questo aiuti qualcuno. Verranno elencati gli elementi da 1 a n righe separati da virgole. Group_Concat lo rende possibile in 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

Questo cimitero ha due nomi comuni, quindi i nomi sono elencati in diverse righe collegate da un singolo ID ma due ID nome e la query produce qualcosa come questo

    CemeteryID Cemetery_Name Latitude
    1 Appleton, Sulpher Springs 35.4276242832293


-2

Mi dispiace dirlo e forse non sto risolvendo esattamente il tuo problema, ma PostgreSQL ha 10 anni più di MySQL ed è estremamente avanzato rispetto a MySQL e ci sono molti modi per farlo facilmente. Installa PostgreSQL ed esegui questa query

CREATE EXTENSION tablefunc;

quindi voilà! Ed ecco un'ampia documentazione: PostgreSQL: Documentazione: 9.1: tablefunc o questa query

CREATE EXTENSION hstore;

poi di nuovo voilà! PostgreSQL: Documentazione: 9.0: hstore

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.