How to make a query to pivot/aggregate like this? - mysql

How would I query this data set:
SessionId | GameMode | Score
----------|----------|-------
1 | 1 | 100
1 | 1 | 90
1 | 2 | 20
1 | 2 | 15
1 | 3 | 5
1 | 3 | 5
2 | 1 | 90
2 | 1 | 80
2 | 2 | 15
2 | 2 | 15
2 | 3 | 2
2 | 3 | 4
to turn it into:
SessionId | GameMode1AvgScore | GameMode2AvgScore | GameMode3AvgScore
----------|--------------------|-------------------|-------------------
1 | 95 | 17.5 | 5
2 | 85 | 15 | 3
To clarify, I'm not asking how to group or aggregate rows, the core of my question is rather how do I pivot the GameMode rows into a column?

With MySQL, to return the specified resultset, we would need a query (SELECT statement) that returns four columns.
SELECT expr1 AS SessionId
, expr2 AS GameMode1AvgScore
, expr3 AS GameMode2AvgScore
, expr4 AS GameMode3AvgScore
FROM ...
GROUP BY SessionId
MySQL does not yet provide "PIVOT" (ala SQL Server and PostgreSQL) or "MODEL" (ala Oracle) features.
So in terms of achieving this result from a SQL SELECT statement, we are left with old-school conditional aggregation,
AVG(CASE WHEN t.GameMode = 1 THEN t.Score ELSE NULL END) AS GameMode1AvgScore
which seems to fall into the "how to group or aggregate rows" bucket you are not asking about. We can use a second SQL statement to help us build the SQL statement we need to run, but seems like that would still fall into the "how to group or aggregate rows". In fact, any SQL statement that produces the specified result is going to need to aggregate rows.
So with that tool excluded from our MySQL toolbelts, that leaves us with one answer: it is not possible to "pivot" GamerMode values into columns.
A statement to return the specified result might be like this:
SELECT t.sessionid AS SessionId
, AVG(CASE WHEN t.GameMode = 1 THEN t.Score ELSE NULL END) AS GameMode1AvgScore
, AVG(CASE WHEN t.GameMode = 2 THEN t.Score ELSE NULL END) AS GameMode2AvgScore
, AVG(CASE WHEN t.GameMode = 3 THEN t.Score ELSE NULL END) AS GameMode3AvgScore
-- ^ ^
FROM t
GROUP BY t.sessionid
ORDER BY t.sessionid
Note how expressions to return the average scores are the same pattern, we're just substutiting in a value of GameMode.
To help us write the query, we could run an initial query to get a distinct list of GameMode values, either from the same table
SELECT q.GameMode FROM t q GROUP BY q.GameMode ORDER BY q.GameMode
or we could query a "dimension" table of GameMode if that exists.
We cycle through the resultset (GameMode values) and generate the expression required to return the average score for each GameMode. And we can use that to construct the SQL statement.

Related

Getting Corresponding Row Values to a GROUP BY with MIN

I've got data that's something like this:
+------------+---------+-------+
| Name | Time | Flag |
+------------+---------+-------+
| Bob | 401 | 1 |
| Bob | 204 | 0 |
| Dan | 402 | 1 |
| Dan | 210 | 0 |
| Jeff | 204 | 0 |
| Fred | 407 | 1 |
| Mike | 415 | 1 |
| Mike | 238 | 0 |
+------------+---------+-------+
I want to get each person's best time, but if the "flag" is set, their time should be divided by 2.
For example, Bob's best time would be 200.5
Now I can do a relatively simple query to get this data like this:
SELECT userid,
MIN(CASE WHEN flag = 1 THEN time / 2 ELSE time END) AS convertedTime,
time,
flag
FROM times t
GROUP BY userid
ORDER BY convertedtime ASC
The problem here is that is doesn't return the proper corresponding data for the time and flag, getting this data:
Bob 200.5 204 0
instead of the correct data
Bob 200.5 401 1
Of course I see the issue with the previous query and I've fixed it with this:
SELECT userid,
MIN(convertedtime) AS convertedTime,
(SELECT time
FROM times
WHERE MIN(convertedtime) = CASE
WHEN flag = 1 THEN time / 2
ELSE time
end
LIMIT 1) AS time,
(SELECT flag
FROM times
WHERE MIN(convertedtime) = CASE
WHEN flag = 1 THEN time / 2
ELSE time
end
LIMIT 1) AS flag
FROM (SELECT userid,
CASE
WHEN flag = 1 THEN time / 2
ELSE time
end AS convertedTime,
time,
flag
FROM times t) AS t
GROUP BY userid
ORDER BY convertedtime ASC
SQLFiddle
This does work, but I feel like there has to be a better and more efficient way of doing this. In my actual query, the part where I'm dividing the time by 2 is a much longer formula and I've got thousands of rows so it's very slow.
So the question is, is there a better/more efficient query for this?
Use a common technique from SQL select only rows with max value on a column but add the flag check when comparing the values in the ON condition.
SELECT t1.username, t2.convertedtime, t1.time, t1.flag
FROM times AS t1
JOIN (SELECT username,
MIN(IF(flag = 1, time/2, time) AS convertedtime
FROM times
GROUP BY username) AS t2
ON t1.username = t2.username
AND t1.time = IF(t1.flag = 1, t2.convertedtime * 2, t2.convertedtime)
This probably won't be very efficient -- I don't think it will be able to use an index for the conditional comparison.
This is what I ended up using (couldn't multiply by 2 to revert the number in this case due to my more complicated actual query having rounding in it), but Barmar did pretty much the same thing as I'm doing so I marked his as the answer.
SELECT times.userID, times2.convertedTime, times.time, times.flag
FROM times JOIN
(
SELECT userid,
MIN(CASE WHEN flag = 1 THEN time / 2 ELSE time END) AS convertedTime,
time,
flag
FROM times t
GROUP BY userid
ORDER BY convertedtime ASC
) as times2 ON times.userid = times2.userid AND
(
(times.time / 2 = times2.convertedTime AND times.flag = 1) OR
(times.time = times2.convertedTime AND times.flag = 0)
)

How to get number of half centuries, centuries and double hundreds for each player in MySQL

I have table with this format
id| name | types |
_________________
1 sachin 100
2 virat 50
3 sachin 50
4 sachin 50
5 sachin 200
6 virat 100
7 virat 200
What I want to get as the result is now something like
name | num of 50 |num of 100 |num of 200
sachin | 2 | 1 | 1
virat | 1 | 1 | 1
What is the correct way to get here ??
I tried using group by. But I didn't get there
Any help?
Thanks
Also you can rely on MySQL boolean expression.
SELECT
name,
SUM(types = 50) as num_50,
SUM(types = 100) as num_100,
SUM(types = 200) as num_200
FROM yourtable
GROUP BY name
Note:
Since MySQL boolean expression resolves into 0/1 so that you can use it inside SUM()
SUM(a=b) returns 1 only if a is equal to b
Working demo
More:
And if you use COUNT as mentioned in #JPG's answer then keep in mind the following subtleties of COUNT
Some subtleties regarding COUNT:
SELECT COUNT(0); Result: 1
SELECT COUNT(-1); Result: 1
SELECT COUNT(NULL); Result: 0
SELECT COUNT(71); Result: 1
SQL FIDDLE
If you just have these types, you can try this:
select
name,
count(case when types = 50 then 1 else null end) as num_50,
count(case when types = 100 then 1 else null end) as num_100,
count(case when types = 200 then 1 else null end) as num_200
from yourtable
group by name
Demo Here

Mysql increment value on multiple cases to order by

I have a table with products, and by answering a set of questions (Could be 2 or could be six, it's not always the same) I want to get results. Each answer has rules to it like color = green or noise > 69.
Now I don't want to use these rules as a WHERE to refine my searchresults, but I want to increment a variable on CASE so I can ORDER on the amount of true conditions. I still get all values, but the ones that better suit my customers needs are on top.
I tried a lot already, something like:
SELECT a.*, b.*,COUNT(CASE WHEN (b.noise >= 1600) THEN 1 ELSE 0 END) as condition_true
But I cant get it to work with multiple CASES.
SAMPLE DATA:
Products table:
id | title
1 | washing machine X
2 | Washing Machine Y200
3 | Even cooler washing machine
Productinfo table
id | noise | color | locked | product_id
1 | 40 | white | 1 | 1
2 | 68 | green | 0 | 2
3 | 72 | green | 1 | 3
Possible rules I will use in the output table
b.noise > 42
b.color = "green"
b.locked = 1
I would love an output table like this
product_id | title | condition_true
3 | Even cooler washing machine | 3
2 | Washing Machine Y200 | 2
1 | washing machine X | 1
CASE only returns one value, and COUNT requires multiple rows. You could just add them.
SELECT a.*, b.*, (CASE WHEN b.noise >= 1600 THEN 1 ELSE 0 END) +
(CASE WHEN color = 'green' THEN 1 ELSE 0 END) AS relevance
FROM table1
ORDER BY relevance DESC
Or, abbreviated for MySQL:
SELECT a.*, b.*, (b.noise >= 1600) +
(color = 'green') AS relevance
FROM table1
ORDER BY relevance DESC
COUNT is not what you want, that works to aggregate results from multiple rows when using GROUP BY.
I would do this using a series of IF() statements, returning 1 or 0 based on the condition (you could also return a bigger number, for more important columns or something):
SELECT IF(b.noise >= 1600,1,0)+IF(b.ocl1 <= 3,1,0)+...;

Average time difference between rows in database

Using MySQL, I have a table that keep track of user visit:
USER_ID | TIMESTAMP
--------+----------------------
1 | 2014-08-11 14:37:36
2 | 2014-08-11 12:37:36
3 | 2014-08-07 16:37:36
1 | 2014-07-14 15:34:36
1 | 2014-07-09 14:37:36
2 | 2014-07-03 14:37:36
3 | 2014-05-23 15:37:36
3 | 2014-05-13 12:37:36
Time is not important, more concern about answer to "how many days between entries"
How do I go about figuring how the average number of days between entries through SQL queries?
For example, the output should look like something like:
(output is just a sample, not reflection of the data table above)
USER_ID | AVG TIME (days)
--------+----------------------
1 | 2
2 | 3
3 | 1
MySQL has no direct "get something from a previous row" capabilities. Easiest workaround is to use a variable to store that "previous" value:
SET last = null;
SELECT user_id, AVG(diff)
FROM (
SELECT user_id, IF(last IS NULL, 0, timestamp - last) AS diff, #last := timestamp
FROM yourtable
ORDER BY user_id, timestamp ASC
) AS foo
GROUP BY user_id
The inner query does your "difference from previous row" calculations, and the outer query does the averaging.

mysql 3 queries on 2 tables in one

I have 2 tables that I need to query
**tbl_jobs**
jobid | description | someinfo
1 foo bar
2 fuu buu
**tbl_invlog**
idinv | jobid | type | ammount
1 1 add 100
2 1 rem 50
3 1 rem 15
4 1 add 8
5 2 add 42
the result should be to make a sum of the inventory "add" and "rem" and give a total of sum(add)-sum(rem) for each jobid, including the rest of the job information.
jobid | description | someinfo | amountadd | amountrem | totaladdrem
1 | foo | bar | 108 | 65 | 43
2 | fuu | buu | 42 | 0 | 42
i have made a quadruple select statement with select * from (select .... ) without using joins or other cool stuff. which is terribly slow. I am quite new to mysql.
I would be glad to an idea on how to solve this.
thanks in advance
This is a query that requires a join and conditional aggregation:
select j.jobid, j.description, j.someinfo,
sum(case when il."type" = 'add' then amount else 0 end) as AmountAdd,
sum(case when il."type" = 'rem' then amount else 0 end) as AmountRem,
(sum(case when il."type" = 'add' then amount else 0 end) -
sum(case when il."type" = 'rem' then amount else 0 end)
) as totaladdrem
from tbl_jobs j left outer join
tbl_invlog il
on j.jobid = il.jobid
group by j.jobid, j.description, j.someinfo;
Note some things. First, the tables have table aliases, defined in the from clause. This allows you to say which table the columns come from. Second, the table aliases are always used for all columns in the query.
MySQL would allow you to just do group by j.jobid, using a feature called "hidden columns". I think this is a bad habit (except in a few cases), so this aggregates by all the columns in the jobs table.
The conditional aggregation is done by putting a condition in the sum() statement.