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.