mysql pivot/crosstab query - mysql

Question 1: I have a table with the below structure and data:
app_id transaction_id mobile_no node_id customer_attribute entered_value
100 111 9999999999 1 Q1 2
100 111 9999999999 2 Q2 1
100 111 9999999999 3 Q3 4
100 111 9999999999 4 Q4 3
100 111 9999999999 5 Q5 2
100 222 8888888888 4 Q4 1
100 222 8888888888 3 Q3 2
100 222 8888888888 2 Q2 1
100 222 8888888888 1 Q1 3
100 222 8888888888 5 Q5 4
I want to display these records in the below format:
app_id | transaction_id | mobile | Q1 | Q2 | Q3 | Q4 | Q5 |
100 | 111 | 9999999999 | 2 | 1 | 4 | 3 | 2 |
100 | 222 | 8888888888 | 3 | 1 | 2 | 1 | 4 |
I know I need to use crosstab/pivot query to get this display. For this I tried it based on the limited knowledge that I have about it. Following is my query:
SELECT app_id, transaction_id, mobile_no,
(CASE node_id WHEN 1 THEN entered_value ELSE '' END) AS user_input1,
(CASE node_id WHEN 2 THEN entered_value ELSE '' END) AS user_input2,
(CASE node_id WHEN 3 THEN entered_value ELSE '' END) AS user_input3,
(CASE node_id WHEN 4 THEN entered_value ELSE '' END) AS user_input4,
(CASE node_id WHEN 5 THEN entered_value ELSE '' END) AS user_input5
FROM trn_user_log
GROUP BY app_id, transaction_id, mobile_no, node_id
And based on this query I got the below display:
app_id transaction_id mobile_no user_input1 user_input2 user_input3 user_input4 user_input5
100 111 9999999999 2
100 111 9999999999 1
100 111 9999999999 4
100 111 9999999999 3
100 111 9999999999 2
100 222 8888888888 3
100 222 8888888888 1
100 222 8888888888 2
100 222 8888888888 1
100 222 8888888888 4
Can anyone help me with the proper changes that I need to make to my query to get the records in one single row and not multiple rows as above.
Question 2: Also is there a way to get the value of a particular field as the NAME of the column. As you can see above I have user_input1, user_input2,... as the header. Instead of that I want to have the values in customer_attribute as the header of the columns.
For this I checked NAME_CONST(name,value) as below:
SELECT app_id, transaction_id, mobile_no,
NAME_CONST(customer_attribute, (CASE node_id WHEN 1 THEN entered_value ELSE '' END))
FROM trn_user_log
But it gives an error
Error Code : 1210 Incorrect arguments to NAME_CONST
Help required.

While #John's static answer works great, if you have an unknown number of columns that you want to transform, I would consider using prepared statements to get the results:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'GROUP_CONCAT((CASE node_id when ',
node_id,
' then entered_value else NULL END)) AS user_input',
node_id
)
) INTO #sql
FROM trn_user_log;
SET #sql = CONCAT('SELECT app_id, transaction_id, mobile_no, ', #sql, '
FROM trn_user_log
GROUP BY app_id, transaction_id, mobile_no');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
see SQL Fiddle with Demo
As far as your second, please clarify what you are trying to do it is not clear.

Add GROUP_CONCAT in your CASE clause
SELECT app_id, transaction_id, mobile_no,
GROUP_CONCAT((CASE node_id WHEN 1 THEN entered_value ELSE NULL END)) AS user_input1,
GROUP_CONCAT((CASE node_id WHEN 2 THEN entered_value ELSE NULL END)) AS user_input2,
GROUP_CONCAT((CASE node_id WHEN 3 THEN entered_value ELSE NULL END)) AS user_input3,
GROUP_CONCAT((CASE node_id WHEN 4 THEN entered_value ELSE NULL END)) AS user_input4,
GROUP_CONCAT((CASE node_id WHEN 5 THEN entered_value ELSE NULL END)) AS user_input5
FROM trn_user_log
GROUP BY app_id, transaction_id, mobile_no
SQLFiddle Demo

#DarkKnightFan, this was a very helpful question for a task I was working. I went ahead and modified the solution from #bluefin to solve your second question. The following code produces your originally requested format with the value of customer_attribute as the resulting column headings in the cross-tab.
The relevant change was to change:
' then entered_value else NULL END)) AS user_input',
node_id
To this:
' then entered_value else NULL END)) AS ''',
customer_attribute,''''
The full code:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'GROUP_CONCAT((CASE node_id when ',
node_id,
' then entered_value else NULL END)) AS ''',
customer_attribute,''''
)
) INTO #sql
FROM trn_user_log;
SET #sql = CONCAT('SELECT app_id, transaction_id, mobile_no, ', #sql, '
FROM trn_user_log
GROUP BY app_id, transaction_id, mobile_no');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Also, for other users browsing this issue, if you have a lot of values that you are trying to cross-tab, you may run into error because GROUP_CONCAT() has a default max length of 1024 characters. To increase put this at the start of your prepared statement:
SET SESSION group_concat_max_len = value; -- replace value with an int

Related

LEFT JOIN 11 times in the same table [duplicate]

Question 1: I have a table with the below structure and data:
app_id transaction_id mobile_no node_id customer_attribute entered_value
100 111 9999999999 1 Q1 2
100 111 9999999999 2 Q2 1
100 111 9999999999 3 Q3 4
100 111 9999999999 4 Q4 3
100 111 9999999999 5 Q5 2
100 222 8888888888 4 Q4 1
100 222 8888888888 3 Q3 2
100 222 8888888888 2 Q2 1
100 222 8888888888 1 Q1 3
100 222 8888888888 5 Q5 4
I want to display these records in the below format:
app_id | transaction_id | mobile | Q1 | Q2 | Q3 | Q4 | Q5 |
100 | 111 | 9999999999 | 2 | 1 | 4 | 3 | 2 |
100 | 222 | 8888888888 | 3 | 1 | 2 | 1 | 4 |
I know I need to use crosstab/pivot query to get this display. For this I tried it based on the limited knowledge that I have about it. Following is my query:
SELECT app_id, transaction_id, mobile_no,
(CASE node_id WHEN 1 THEN entered_value ELSE '' END) AS user_input1,
(CASE node_id WHEN 2 THEN entered_value ELSE '' END) AS user_input2,
(CASE node_id WHEN 3 THEN entered_value ELSE '' END) AS user_input3,
(CASE node_id WHEN 4 THEN entered_value ELSE '' END) AS user_input4,
(CASE node_id WHEN 5 THEN entered_value ELSE '' END) AS user_input5
FROM trn_user_log
GROUP BY app_id, transaction_id, mobile_no, node_id
And based on this query I got the below display:
app_id transaction_id mobile_no user_input1 user_input2 user_input3 user_input4 user_input5
100 111 9999999999 2
100 111 9999999999 1
100 111 9999999999 4
100 111 9999999999 3
100 111 9999999999 2
100 222 8888888888 3
100 222 8888888888 1
100 222 8888888888 2
100 222 8888888888 1
100 222 8888888888 4
Can anyone help me with the proper changes that I need to make to my query to get the records in one single row and not multiple rows as above.
Question 2: Also is there a way to get the value of a particular field as the NAME of the column. As you can see above I have user_input1, user_input2,... as the header. Instead of that I want to have the values in customer_attribute as the header of the columns.
For this I checked NAME_CONST(name,value) as below:
SELECT app_id, transaction_id, mobile_no,
NAME_CONST(customer_attribute, (CASE node_id WHEN 1 THEN entered_value ELSE '' END))
FROM trn_user_log
But it gives an error
Error Code : 1210 Incorrect arguments to NAME_CONST
Help required.
While #John's static answer works great, if you have an unknown number of columns that you want to transform, I would consider using prepared statements to get the results:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'GROUP_CONCAT((CASE node_id when ',
node_id,
' then entered_value else NULL END)) AS user_input',
node_id
)
) INTO #sql
FROM trn_user_log;
SET #sql = CONCAT('SELECT app_id, transaction_id, mobile_no, ', #sql, '
FROM trn_user_log
GROUP BY app_id, transaction_id, mobile_no');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
see SQL Fiddle with Demo
As far as your second, please clarify what you are trying to do it is not clear.
Add GROUP_CONCAT in your CASE clause
SELECT app_id, transaction_id, mobile_no,
GROUP_CONCAT((CASE node_id WHEN 1 THEN entered_value ELSE NULL END)) AS user_input1,
GROUP_CONCAT((CASE node_id WHEN 2 THEN entered_value ELSE NULL END)) AS user_input2,
GROUP_CONCAT((CASE node_id WHEN 3 THEN entered_value ELSE NULL END)) AS user_input3,
GROUP_CONCAT((CASE node_id WHEN 4 THEN entered_value ELSE NULL END)) AS user_input4,
GROUP_CONCAT((CASE node_id WHEN 5 THEN entered_value ELSE NULL END)) AS user_input5
FROM trn_user_log
GROUP BY app_id, transaction_id, mobile_no
SQLFiddle Demo
#DarkKnightFan, this was a very helpful question for a task I was working. I went ahead and modified the solution from #bluefin to solve your second question. The following code produces your originally requested format with the value of customer_attribute as the resulting column headings in the cross-tab.
The relevant change was to change:
' then entered_value else NULL END)) AS user_input',
node_id
To this:
' then entered_value else NULL END)) AS ''',
customer_attribute,''''
The full code:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'GROUP_CONCAT((CASE node_id when ',
node_id,
' then entered_value else NULL END)) AS ''',
customer_attribute,''''
)
) INTO #sql
FROM trn_user_log;
SET #sql = CONCAT('SELECT app_id, transaction_id, mobile_no, ', #sql, '
FROM trn_user_log
GROUP BY app_id, transaction_id, mobile_no');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Also, for other users browsing this issue, if you have a lot of values that you are trying to cross-tab, you may run into error because GROUP_CONCAT() has a default max length of 1024 characters. To increase put this at the start of your prepared statement:
SET SESSION group_concat_max_len = value; -- replace value with an int

Transpose the results of a MySQL query that outputs ranges

My source table (wplott_wpkl_winner) contains the field "lottery_number" that carries 1 to 6 digit numbers and the corresponding "draw_date".
lottery_number | draw_date
==================================
0024 | 2018-11-10
4456 | 2018-11-10
3895 | 2018-11-10
4557 | 2018-11-10
4225 | 2018-11-10
2896 | 2018-11-10
3354 | 2018-11-10
1895 | 2018-11-10
78466 | 2018-11-10
998556 | 2018-11-10
My current MYSQL query is as below (I am trying to group the data into ranges)
select
count(case when wplott_wpkl_winner.lottery_number between 0 and 999 then 1 end) `0-999`,
count(case when wplott_wpkl_winner.lottery_number between 1000 and 1999 then 1 end) `1000-1999`,
count(case when wplott_wpkl_winner.lottery_number between 2000 and 2999 then 1 end) `2000-2999`,
count(case when wplott_wpkl_winner.lottery_number between 3000 and 3999 then 1 end) `3000-3999`,
count(case when wplott_wpkl_winner.lottery_number between 4000 and 4999 then 1 end) `4000-4999`,
count(case when wplott_wpkl_winner.lottery_number between 5000 and 5999 then 1 end) `5000-5999`,
count(case when wplott_wpkl_winner.lottery_number between 6000 and 6999 then 1 end) `6000-6999`,
count(case when wplott_wpkl_winner.lottery_number between 7000 and 7999 then 1 end) `7000-7999`,
count(case when wplott_wpkl_winner.lottery_number between 8000 and 8999 then 1 end) `8000-8999`,
count(case when wplott_wpkl_winner.lottery_number between 9000 and 9999 then 1 end) `9000-9999`
from wplott_wpkl_winner
where CHAR_LENGTH(wplott_wpkl_winner.lottery_number) = 4 AND wplott_wpkl_winner.draw_date > '2013-06-30'
It provides the below output
0-999 | 1000-1999 | 2000-2999 | 3000-3999 | 4000- 4999 .... etc
=====================================================================
1 | 1 | 1 | 2 | 3
However, I would like to get the output in the below format.
Range | Count
=======================
0-999 | 1
1000-1999 | 1
2000-2999 | 1
3000-3999 | 2
4000-4999 | 3
.
.
.
Any help is highly appreciated. I did search in SO for a similar answer but none of the answers helped my particular case.
Thanks in advance!
One approach uses a series of unions:
SELECT
`range`,
count
FROM
(
SELECT 1 AS pos, '0-999' AS `range`, COUNT(*) AS count
FROM wplott_wpkl_winner
WHERE draw_date > '2013-06-30' AND lottery_number BETWEEN 0 AND 999
UNION ALL
SELECT 2, '1000-1999', COUNT(*)
FROM wplott_wpkl_winner
WHERE draw_date > '2013-06-30' AND lottery_number BETWEEN 1000 AND 1999
UNION ALL
... -- fill in remaining ranges here
) t
ORDER BY pos;
Note that I introduce a computed column pos so that we may maintain the desired ordering of the ranges in the final output. Also, I removed the check on the CHAR_LENGTH of the lottery_number, since the conditional sums already handle this logic.

SQL convert columns in rows

I want convert this table (Reading):
ID TimeTable_ID reading_Value Sensor_ID
1 1 482 1
2 1 153 2
3 1 152 3
4 1 781 4
5 2 156 1
6 2 842 2
7 2 157 3
8 2 453 4
into this:
TimeTable_ID Sensor_1 Sensor_2 Sensor_3 Sensor_4
1 482 153 152 781
2 156 842 157 453
My try:
SELECT *
FROM (SELECT TimeTable_ID, reading_Value
FROM Reading
) AS BaseData PIVOT
(COUNT(reading_Value) FOR TimeTable_ID IN ([Sensor_1], [Sensor_2], [Sensor_3], [Sensor_4])
) AS PivotTable;
but it does not work.
MySQL does not support the PIVOT operator. But, you may use a standard pivot query instead:
SELECT
TimeTable_ID,
MAX(CASE WHEN Sensor_ID = 1 THEN reading_Value END) AS Sensor_1,
MAX(CASE WHEN Sensor_ID = 2 THEN reading_Value END) AS Sensor_2,
MAX(CASE WHEN Sensor_ID = 3 THEN reading_Value END) AS Sensor_3,
MAX(CASE WHEN Sensor_ID = 4 THEN reading_Value END) AS Sensor_4
FROM Reading
GROUP BY TimeTable_ID;
If you have limited Sensors then you can do :
select TimeTable_ID,
sum(case when Sensor_ID = 1 then reading_Value else 0 end) as Sensor_1,
. . .
sum(case when Sensor_ID = 4 then reading_Value else 0 end) as Sensor_4
from reading r
group by TimeTable_ID;

SQL consolidate data and turn them into columns

So here's what my data table looks like:
TeamNum Round Points1 Points2
1 1 5 21
2 1 10 20
3 1 9 29
1 2 6 22
2 2 11 21
3 2 10 30
1 3 80 50
I also have a second table with this:
TeamNum TeamName
1 goteam1
2 goteam2
3 goteam4-1
I want SQL to take it and turn it into this:
Team Round1 Round2 Round3 TeamName
1 (points1+points2 of round1) (same but for r2) (same but for r3) goteam1
2 (points1+points2 of round1) (same but for r2) (same but for r3) goteam2
3 (points1+points2 of round1) (same but for r2) (same but for r3) goteam4-1
And a sample output from the tables above would be:
Team Round1 Round2 Round3 TeamName
1 26 28 130 goteam1
2 30 32 0 goteam2
3 38 40 0 goteam4-1
The actual data has a bunch of "points1" and "points2" columns, but there are only 3 rounds.
I am very new to SQL and this is all I have right now:
select
`data`.`round`,
`data`.`teamNumber`,
sum(`Points1`) + sum(`Points2`) as score
from `data` join `teams` ON `teams`.`teamNumber` = `data`.`teamNumber`
group by `data`.`teamNumber` , `round`
order by `data`.`teamNumber`, `data`.`round`
But it doesn't return anything at all. If I remove the join statement, it shows everything like I want, but doesn't consolidate Round1, 2, and 3 as columns, they are each separate rows. Can you guys help me out? Thanks!
Use conditional aggregation
SELECT t.teamnumber, t.teamname,
SUM(CASE WHEN d.round = 1 THEN d.points1 + d.points2 ELSE 0 END) round1,
SUM(CASE WHEN d.round = 2 THEN d.points1 + d.points2 ELSE 0 END) round2,
SUM(CASE WHEN d.round = 3 THEN d.points1 + d.points2 ELSE 0 END) round3
FROM data d JOIN teams t
ON d.teamnumber = t.teamnumber
GROUP BY t.teamnumber, t.teamname
Output:
| TEAMNUMBER | TEAMNAME | ROUND1 | ROUND2 | ROUND3 |
|------------|-----------|--------|--------|--------|
| 1 | goteam1 | 26 | 28 | 130 |
| 2 | goteam2 | 30 | 32 | 0 |
| 3 | goteam4-1 | 38 | 40 | 0 |
Here is SQLFiddle demo
No need to aggregate:
SELECT
t.teamnumber,
COALESCE(r1.points1 + r1.points2, 0) AS round1,
COALESCE(r2.points1 + r2.points2, 0) AS round2,
COALESCE(r3.points1 + r3.points2, 0) AS round3,
t.teamname
FROM teams t
LEFT JOIN data r1 ON r1.teamnumber = t.teamnumber AND r1.round = 1
LEFT JOIN data r2 ON r2.teamnumber = t.teamnumber AND r2.round = 2
LEFT JOIN data r3 ON r3.teamnumber = t.teamnumber AND r3.round = 3
Something like this:
select teams.teamNumber,
SUM(CASE WHEN Round=1 THEN `Points1`+`Points2` ELSE 0 END)as Round1,
SUM(CASE WHEN Round=2 THEN `Points1`+`Points2` ELSE 0 END)as Round2,
SUM(CASE WHEN Round=3 THEN `Points1`+`Points2` ELSE 0 END)as Round3,
teams.teamName
from `data` join `teams` ON `teams`.`teamNumber` = `data`.`teamNumber`
group by teamnumber , teamname
order by `data`.`teamNumber`, `data`.`round`

MySQL - Combining multiple selects from same table into one result table with a group by

I have a table of meter readings which holds a meterNo, readingValue and readingDate.
I want to show the total sum of readingValue per meterNo per year.
For example:
SELECT meterNo, SUM(readingValue) '2009' FROM readings
WHERE readingDate >= '2009-01-01' AND readingDate <= '2009-12-31'
GROUP BY meterNo;
Will display the table:
meterNo 2009
--------------
4 50
5 5
12 30
13 63
18 18
26 484
27 21
28 510
29 28
Which is what I want, it shows the total reading value for each meter in 2009. However, I need the result table to display a column for each year. So it would be something like this:
meterNo 2009 2010 2011 2012
--------------------------------
1 20 35 50
2 45 35
3 50 60
4 50 35 20 30
5 5 10 10 15
6 30
And so on...
I can reuse the where part of the query:
WHERE readingDate >= '2009-01-01' AND readingDate <= '2009-12-31'
And just change the date for each year, butt how do I display each WHERE result as its own column in the one result table?
MySQL does not have a PIVOT function but you can convert the rows of data into columns using an aggregate function with a CASE expression.
If you have a limited number of years, then you can hard-code the query:
select meterNo,
sum(case when year(readingDate) = 2009 then readingValue else 0 end) `2009`,
sum(case when year(readingDate) = 2010 then readingValue else 0 end) `2010`,
sum(case when year(readingDate) = 2011 then readingValue else 0 end) `2011`,
sum(case when year(readingDate) = 2012 then readingValue else 0 end) `2012`,
sum(case when year(readingDate) = 2013 then readingValue else 0 end) `2013`
from readings
group by meterno;
See SQL Fiddle with Demo
But if you are going to have an unknown number of values or what the query to adjust as new years are added to the database, then you can use a prepared statement to generate dynamic SQL:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'sum(CASE WHEN year(readingDate) = ',
year(readingDate),
' THEN readingValue else 0 END) AS `',
year(readingDate), '`'
)
) INTO #sql
FROM readings;
SET #sql
= CONCAT('SELECT meterno, ', #sql, '
from readings
group by meterno');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
See SQL Fiddle with Demo. Both give the result:
| METERNO | 2009 | 2010 | 2012 | 2013 | 2011 |
----------------------------------------------
| 1 | 90 | 180 | 0 | 90 | 90 |
| 2 | 50 | 0 | 90 | 0 | 0 |
| 3 | 80 | 40 | 90 | 90 | 0 |
As a side note, if you want null to display in the rows without values instead of the zeros, then you can remove the else 0 (see Demo)
You can use CASE WHEN to do this. It allows you to sum multiple values based on multiple conditions. In your example, you could do something like this (I've simplified my example because I don't have access to your actual data):
SELECT meterNo,
sum(CASE WHEN readingDate = 2009 THEN readingValue END) as '2009',
sum(CASE WHEN readingDate = 2010 THEN readingValue END) as '2010',
sum(CASE WHEN readingDate = 2011 THEN readingValue END) as '2011',
sum(CASE WHEN readingDate = 2012 THEN readingValue END) as '2012'
FROM readings
GROUP BY meterNo
You need to switch up the conditions with your own--sum(CASE WHEN readingDate >= '2009-01-01' AND readingDate <= '2009-12-31' THEN readingValue END), etc--but it should work. Here's a working example that I did on sqlFiddle: http://sqlfiddle.com/#!2/6990e/29. Output is five columns with sums of each meterNo.
You could do something like this:
SELECT
meterNo, SUM(readingValue) AS `total`, YEAR(readingDate)
FROM
readings
GROUP BY
meterNo, YEAR(readingDate);
result:
METERNO TOTAL YEAR(READINGDATE)
1 90 2009
1 90 2010
2 50 2009
3 80 2009
3 40 2010
Also note how you can use YEAR function instead of the where clause you have.
SEE FIDDLE