265

I tried to search posts, but I only found solutions for SQL Server/Access. I need a solution in MySQL (5.X).

I have a table (called history) with 3 columns: hostid, itemname, itemvalue.
If I do a select (select * from history), it will return

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

How do I query the database to return something like

   +--------+------+-----+-----+
   | hostid |   A  |  B  |  C  |
   +--------+------+-----+-----+
   |   1    |  10  |  3  |  0  |
   +--------+------+-----+-----+
   |   2    |   9  |  0  |  40 |
   +--------+------+-----+-----+
xnakos
  • 9,870
  • 6
  • 27
  • 33
Bob Rivers
  • 5,261
  • 6
  • 47
  • 59

13 Answers13

349

I'm going to add a somewhat longer and more detailed explanation of the steps to take to solve this problem. I apologize if it's too long.


I'll start out with the base you've given and use it to define a couple of terms that I'll use for the rest of this post. This will be the base table:

select * from history;

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

This will be our goal, the pretty pivot table:

select * from history_itemvalue_pivot;

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

Values in the history.hostid column will become y-values in the pivot table. Values in the history.itemname column will become x-values (for obvious reasons).


When I have to solve the problem of creating a pivot table, I tackle it using a three-step process (with an optional fourth step):

  1. select the columns of interest, i.e. y-values and x-values
  2. extend the base table with extra columns -- one for each x-value
  3. group and aggregate the extended table -- one group for each y-value
  4. (optional) prettify the aggregated table

Let's apply these steps to your problem and see what we get:

Step 1: select columns of interest. In the desired result, hostid provides the y-values and itemname provides the x-values.

Step 2: extend the base table with extra columns. We typically need one column per x-value. Recall that our x-value column is 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 |
+--------+----------+-----------+------+------+------+

Note that we didn't change the number of rows -- we just added extra columns. Also note the pattern of NULLs -- a row with itemname = "A" has a non-null value for new column A, and null values for the other new columns.

Step 3: group and aggregate the extended table. We need to group by hostid, since it provides the y-values:

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

(Note that we now have one row per y-value.) Okay, we're almost there! We just need to get rid of those ugly NULLs.

Step 4: prettify. We're just going to replace any null values with zeroes so the result set is nicer to look at:

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

And we're done -- we've built a nice, pretty pivot table using MySQL.


Considerations when applying this procedure:

  • what value to use in the extra columns. I used itemvalue in this example
  • what "neutral" value to use in the extra columns. I used NULL, but it could also be 0 or "", depending on your exact situation
  • what aggregate function to use when grouping. I used sum, but count and max are also often used (max is often used when building one-row "objects" that had been spread across many rows)
  • using multiple columns for y-values. This solution isn't limited to using a single column for the y-values -- just plug the extra columns into the group by clause (and don't forget to select them)

Known limitations:

  • this solution doesn't allow n columns in the pivot table -- each pivot column needs to be manually added when extending the base table. So for 5 or 10 x-values, this solution is nice. For 100, not so nice. There are some solutions with stored procedures generating a query, but they're ugly and difficult to get right. I currently don't know of a good way to solve this problem when the pivot table needs to have lots of columns.
Matt Fenwick
  • 48,199
  • 22
  • 128
  • 192
  • 12
    Excellent explanation, thanks. Step 4 could be merged into step 3 by using IFNULL(sum(A), 0) AS A, giving you the same result but without the need for creating yet another table – nealio82 Mar 20 '13 at 10:33
  • 1
    It was most amazing solution for pivot, but I'm just curious if in column itemname which forms the x-axis has multiple values ,like here we only have three values i.e A, B, C .If these values gets extends to A, B, C, D, E, AB, BC, AC, AD, H.....n. then in this case what would be the solution. – Deepesh Feb 24 '16 at 11:45
  • well amazing explanation indeed. It would be very nice if some guru would elaborate further, to solve the problem with the need of manual addition of columns – taiko Jul 07 '17 at 13:43
  • As far as I know, this is a blog written by Stratos Provatopoulos, I don't know if you are him. O/W, please cite the source and give credit to him as a courtesy. http://stratosprovatopoulos.com/web-development/mysql/pivot-a-table-in-mysql/ – Demo Dec 07 '17 at 01:59
  • 5
    @WhiteBig please take a look at the dates -- this StackOverflow answer was written 1.5 years before that blog post. Perhaps you should instead be asking the blog to credit me. – Matt Fenwick Dec 07 '17 at 11:23
  • Sorry, it was my fault, just came across the topic of pivotal table and found two exact same answers. Thanks for correcting me. – Demo Dec 07 '17 at 12:43
  • @Matt Fenwick Maybe you can help me. Look at this : https://stackoverflow.com/questions/51832979/how-to-create-select-dynamic-fields-from-a-table-in-mysql – moses toh Aug 14 '18 at 03:21
  • Step 4 could be done in step 2 with (case .. else 0 end). – Eperbab Oct 13 '18 at 20:17
  • 2
    Great Answer! If you column type is string, you have to use MAX() instead of SUM() – Tanner Clark Jan 15 '20 at 16:53
  • [Mihai's more recent answer](https://stackoverflow.com/a/28284999/199364) shows how to avoid the manual work of step 2 - therefore handling arbitrary values (less effort, more general, avoids the "known limitation" mentioned in this answer). Complete with fiddle. – ToolmakerSteve Jul 07 '20 at 23:20
  • thanks @MattFenwick it is the most excellenct explanation i found for this topic, – Sudhanshu Jun 03 '21 at 01:32
  • 1
    And to add something on this wonderful answer: you could avoid creating the view by using a nested select. Not the best option, but sometimes it is needed. – Erenor Paz Jun 21 '23 at 14:30
70
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;
shA.t
  • 16,580
  • 5
  • 54
  • 111
shantanuo
  • 31,689
  • 78
  • 245
  • 403
54

Another option,especially useful if you have many items you need to pivot is to let mysql build the query for you:

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 Added some extra values to see it working

GROUP_CONCAT has a default value of 1000 so if you have a really big query change this parameter before running it

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
  • 26,325
  • 7
  • 66
  • 81
  • @Mihai Maybe you can help me. Look at this : https://stackoverflow.com/questions/51832979/how-to-create-select-dynamic-fields-from-a-table-in-mysql – moses toh Aug 14 '18 at 03:22
  • Can simplify `'ifnull(SUM(case when itemname = ''',` with `''' then itemvalue end),0) AS `',` to `'SUM(case when itemname = '''` with `''' then itemvalue else 0 end) AS ',`. This outputs terms like `SUM(case when itemname = 'A' then itemvalue else 0 end) AS 'A'`. – ToolmakerSteve Jul 08 '20 at 01:14
  • can we build this as view? – Joe Apr 02 '21 at 09:09
  • https://thispointer.com/mysql-convert-rows-to-columns/ – Nee Jul 13 '22 at 19:33
29

Taking advantage of Matt Fenwick's idea that helped me to solve the problem (a lot of thanks), let's reduce it to only one 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
Nik Reiman
  • 39,067
  • 29
  • 104
  • 160
jalber
  • 299
  • 3
  • 3
17

I edit Agung Sagita's answer from subquery to join. I'm not sure about how much difference between this 2 way, but just for another reference.

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'
Community
  • 1
  • 1
haudoing
  • 603
  • 7
  • 18
  • Thanks. That's good, so simple but nearly works. It returns duplicate into 3 rows for each hostid with the same column and value, so I must delete two others. – Mey Lin Aug 30 '21 at 16:59
13

use 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

but it will be a problem if sub query resulting more than a row, use further aggregate function in the subquery

Agung Sagita
  • 157
  • 1
  • 8
7

If you could use MariaDB there is a very very easy solution.

Since MariaDB-10.02 there has been added a new storage engine called CONNECT that can help us to convert the results of another query or table into a pivot table, just like what you want: You can have a look at the docs.

First of all install the connect storage engine.

Now the pivot column of our table is itemname and the data for each item is located in itemvalue column, so we can have the result pivot table using this query:

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

Now we can select what we want from the pivot_table:

select * from pivot_table

More details here

ako
  • 2,000
  • 2
  • 28
  • 34
6

My 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

It produces the expected results in the submitted case.

André Wéber
  • 74
  • 1
  • 1
4

I figure out one way to make my reports converting rows to columns almost dynamic using simple querys. You can see and test it online here.

The number of columns of query is fixed but the values are dynamic and based on values of rows. You can build it So, I use one query to build the table header and another one to see the values:

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;

You can summarize it, too:

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

Results of RexTester:

Results of RexTester

http://rextester.com/ZSWKS28923

For one real example of use, this report bellow show in columns the hours of departures arrivals of boat/bus with a visual schedule. You will see one additional column not used at the last col without confuse the visualization: sistema venda de passagens online e consumidor final e controle de frota - xsl tecnologia - xsl.com.br ** ticketing system to of sell ticket online and presential

lynx_74
  • 1,633
  • 18
  • 12
4

I make that into Group By hostId then it will show only first row with values,
like:

A   B  C
1  10
2      3
Johan
  • 74,508
  • 24
  • 191
  • 319
arpit
  • 41
  • 1
1

This isn't the exact answer you are looking for but it was a solution that i needed on my project and hope this helps someone. This will list 1 to n row items separated by commas. Group_Concat makes this possible 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

This cemetery has two common names so the names are listed in different rows connected by a single id but two name ids and the query produces something like this

    CemeteryID     Cemetery_Name             Latitude
    1                    Appleton,Sulpher Springs   35.4276242832293

1

You can use a couple of LEFT JOINs. Kindly use this code

SELECT t.hostid,
       COALESCE(t1.itemvalue, 0) A,
       COALESCE(t2.itemvalue, 0) B,
       COALESCE(t3.itemvalue, 0) C 
FROM history t 
LEFT JOIN history t1 
    ON t1.hostid = t.hostid 
    AND t1.itemname = 'A' 
LEFT JOIN history t2 
    ON t2.hostid = t.hostid 
    AND t2.itemname = 'B' 
LEFT JOIN history t3 
    ON t3.hostid = t.hostid 
    AND t3.itemname = 'C' 
GROUP BY t.hostid
ruleboy21
  • 5,510
  • 4
  • 17
  • 34
-5

I'm sorry to say this and maybe I'm not solving your problem exactly but PostgreSQL is 10 years older than MySQL and is extremely advanced compared to MySQL and there's many ways to achieve this easily. Install PostgreSQL and execute this query

CREATE EXTENSION tablefunc;

then voila! And here's extensive documentation: PostgreSQL: Documentation: 9.1: tablefunc or this query

CREATE EXTENSION hstore;

then again voila! PostgreSQL: Documentation: 9.0: hstore

gdarcan
  • 595
  • 5
  • 8