SQL: transform rows into columns in MySQL (SELECT statement) - mysql

I got table orders and order_comments. Each order can have from 0 to n comments. I would like to get list of all orders with their comments in a sepcific order.
Table orders:
order_id | order_nr
1 | 5252
4 | 6783
5 | 6785
Table order_comments
id_order_comments | order_fk | created_at | email | content
1 | 4 | 2015-01-12 | jack | some text here
2 | 5 | 2015-01-13 | marta | some text here
3 | 5 | 2015-01-14 | beata | some text here
4 | 4 | 2015-01-16 | julia | some text here
As a result, I would like to get 1 row for each order. Comments should be shown in separate columns, starting from the oldest comment. So desired output in this case is:
order_id | 1_comment_created_at | 1_comment_author | 1_comment_content | 2_comment_created_at | 2_comment_author | 2_comment_content
1 | NULL | NULL | NULL | NULL | NULL | NULL
4 | 2015-01-12 | jack | some text here | 2015-01-16 | Julia | some text here
5 | 2015-01-13 | marta | some text here | 2015-01-14 | beata | some text here
I found this: MySQL - Rows to Columns - but I cannot use 'create view'.
I found this: http://dev.mysql.com/doc/refman/5.5/en/while.html - but I cannot create procedure in this db.
What I got:
SELECT #c := (SELECT count(*) FROM order_comments GROUP BY order_fk ORDER BY count(*) DESC LIMIT 1);
SET #rank=0;
SET #test=0;
SELECT
CASE WHEN #test < #c AND temp.comment_id = #test THEN temp.created_at END AS created,
CASE WHEN #test < #c AND temp.comment_id = #test THEN temp.author END AS author,
CASE WHEN #test < #c AND temp.comment_id = #test THEN temp.content END AS content
/*But I cannot set #test as +1. And I cannot name column with variable - like CONCAT(#test, '_created')*/
FROM (
SELECT #rank := #rank +1 AS comment_id, created_at, author, content
FROM order_comments
WHERE order_fk = 4
ORDER BY created_at
) AS temp
Problem: I would like to search more than 1 order. I should get orders with no comments too.
What can I do?

You can use variables for this type of pivot, but the query is a bit more complicated, because you need to enumerate the values for each order:
SELECT o.order_id,
MAX(case when rank = 1 then created_at end) as created_at_1,
MAX(case when rank = 1 then email end) as email_1,
MAX(case when rank = 1 then content end) as content_1,
MAX(case when rank = 2 then created_at end) as created_at_2,
MAX(case when rank = 2 then email end) as email_2,
MAX(case when rank = 2 then content end) as content_2,
FROM orders o LEFT JOIN
(SELECT oc.*,
(#rn := if(#o = order_fk, #rn + 1,
if(#o := order_fk, 1, 1)
)
) as rank
FROM order_comments oc CROSS JOIN
(SELECT #rn := 0, #o := 0) vars
ORDER BY order_fk, created_at
) oc
ON o.order_id = oc.order_fk
GROUP BY o.order_id;

Related

Get user's highest score from a table

I have a feeling this is a very simple question but maybe i'm having brain fart right now and just can't seem to figure out how to go about it.
I have a MySQL table structure like below
+---------------------------------------------------+
| id | date | score | speed | user_id |
+---------------------------------------------------+
| 1 | 2016-11-17 | 2 | 133291 | 17 |
| 2 | 2016-11-17 | 6 | 82247 | 17 |
| 3 | 2016-11-17 | 6 | 21852 | 17 |
| 4 | 2016-11-17 | 1 | 109338 | 17 |
| 5 | 2016-11-17 | 7 | 64762 | 61 |
| 6 | 2016-11-17 | 8 | 49434 | 61 |
Now i can get a particular user's best performance by doing this
SELECT *
FROM performance
WHERE user_id = 17 AND date = '2016-11-17'
ORDER BY score desc,speed asc LIMIT 1
This should return the row with ID = 3. Now what I want is a single query to run to be able to return that 1 such row for each unique user_id in the table. So the resulting result would be something like this
+---------------------------------------------------+
| id | date | score | speed | user_id |
+---------------------------------------------------+
| 3 | 2016-11-17 | 6 | 21852 | 17 |
| 6 | 2016-11-17 | 8 | 49434 | 61 |
Also further more, can I have another question within this same query that would further sort this eventual resulting table by the same criteria of sort (score desc, speed asc). Thanks
A simple method uses a correlated subquery:
select p.*
from performance p
where p.date = '2016-11-17' and
p.id = (select p2.id
from performance p2
where p2.user_id = p.user_id and p2.date = p.date
order by score desc, speed asc
limit 1
);
This should be able to take advantage of an index on performance(date, user_id, score, speed).
Is easy using variable to emulate row_number() over (partition by Order by)
Explanation:
First create two variables in the subquery.
Order by user_id so when user change the #rn reset to 1
Order by score desc, speed asc so each row will have a row_number, and the one you want always will have rn = 1
#rn := you change #rn for each row
if you have a new user_id then #rn is set to 1
otherwise #rn is set to #rn+1
SQL Fiddle Demo
SELECT `id`, `date`, `score`, `speed`, `user_id`
FROM (
SELECT *,
#rn := if(#user_id = `user_id`,
#rn + 1 ,
if(#user_id := `user_id`,1,1)
) as rn
FROM Table1
CROSS JOIN (SELECT #user_id := 0, #rn := 0) as param
WHERE date = '2016-11-17'
ORDER BY `user_id`, `score` desc, `speed` asc
) T
where T.rn =1
OUTPUT
For mysql
You can try with a double in subselect and group by
select * from performance
where (user_id, score,speed ) in (
SELECT user_id, max_score, max(speed)
FROM performance
WHERE (user_id, score) in (select user_id, max(score) max_score
from performance
group by user_id)
group by user_id, max_score
);

Select quantity of record instances separated by weeks

I have a table like the below:
CompanyID | Logged | UniqueID
A | 2014-06-24 | 8
B | 2014-06-24 | 7
A | 2014-06-16 | 6
B | 2014-06-16 | 5
A | 2014-06-08 | 4
B | 2014-06-08 | 3
A | 2014-06-01 | 2
B | 2014-06-01 | 1
I'm stuck trying to create an SQL statement that will return the quantity of rows found for each unique CompanyID, separated into 4 week periods, so something like the below:
CompanyID | Period (week) | Quantity
A | 0 | 1
B | 0 | 1
A | 1 | 1
B | 1 | 1
A | 2 | 1
B | 2 | 1
A | 3 | 1
B | 3 | 1
I have done something similar before, except by the last 7 days instead of last 4 weeks, but am not sure if this can be reworked:
select CompanyID,
case DATE_FORMAT(Logged, '%Y%m%d')
when '20140618' then '0'
when '20140619' then '1'
when '20140620' then '2'
when '20140621' then '3'
when '20140622' then '4'
when '20140623' then '5'
when '20140624' then '6'
end as period ,
count(UniqueID) as quantity from TABLE
where DATE_FORMAT(Logged, '%Y%m%d')
in (20140618,20140619,20140620,20140621,20140622,20140623,20140624) group by CompanyID,
DATE_FORMAT(Logged, '%Y%m%d')
Is there a more straightforward way to obtain the output desired above?
Maybe something like this?
SQL FIDDLE to test with
Theres the original query that doesn't use any hard coding... that is generally a really bad practice. it will have the count inflated by 1 since it starts with one and you want it to start with zero so to fix this do a select of the original query where you fix the count and then also not show the user defined variable
SELECT CompanyID, Period - 1 as Period, Quantity FROM(
SELECT
CompanyID,
if(#a = Logged, #b, #b := #b + 1) as Period,
COUNT(*) as Quantity,
#a := Logged
FROM test
JOIN (SELECT #a := '', #b := 0) as temp
GROUP BY UniqueID
ORDER BY Period
) as subQuery
ORIGINAL QUERY
SELECT
CompanyID,
if(#a = Logged, #b, #b := #b + 1) as Period,
COUNT(*) as Quantity,
#a := Logged
FROM test
JOIN (SELECT #a := '', #b := 0) as temp
GROUP BY UniqueID
ORDER BY Period

MySQL return first n rows and group the rest

I wnat to draw a pie chart with MySQL data. I need to retrieve the first n rows and group the rest.
The problem is that the first query is already grouped.
SELECT name AS especie, SUM(superficie) AS superficie
FROM ciclos
JOIN cultivos ON id_cultivo = idcultivo
JOIN tbl_especies ON id_especie = idespecie
WHERE fecha_cierre IS NULL
GROUP BY id_especie
ORDER BY superficie DESC
This is what I get:
+------------+------------+
| Especie | Superficie |
+------------+------------+
| Avena | 50.0000 |
| Centeno | 32.4000 |
| Trigo | 18.0000 |
| Almendros | 5.1100 |
| Olivos | 4.7000 |
| Vid | 1.8300 |
| Nogal | 0.3500 |
| Cerezo | 0.2500 |
+------------+------------+
And this is what I need:
+------------+------------+
| Especie | Superficie |
+------------+------------+
| Avena | 50.0000 |
| Centeno | 32.4000 |
| Trigo | 18.0000 |
| Almendros | 5.1100 |
| Rest | 7.1300 |
+------------+------------+
In this case, I need to retrieve the first 4 rows and group the rest.
Is there any way to solve this with one query?
SOLVED:
I took the #Gordon Linoff concept and mixed it with this.
The problem with the #Gordon Linoff solution, was that the row number were added during the order.
SELECT #rn := #rn + 1 AS rn, SUM(superficie) AS superficie, (CASE WHEN #rn <= 4 THEN name ELSE "Other" END) AS especie
FROM (
SELECT name, SUM(superficie) AS superficie
FROM ciclos
JOIN cultivos ON id_cultivo = idcultivo
JOIN tbl_especies ON id_especie = idespecie
WHERE fecha_cierre IS NULL
GROUP BY id_especie
ORDER BY superficie DESC
) AS temp
CROSS JOIN (SELECT #rn := 0) AS const
GROUP BY (CASE WHEN #rn <= 4 THEN name ELSE "Other" END)
ORDER BY superficie DESC
Hope this helps someone. Thanks for the help.
You can do this with one query, but it requires a subquery (in the end, somehow, you have to group already grouped data). Here is one, MySQL-specific way. It adds a sequence number on the rows using a variable, and then uses that for the grouping:
select (case when rn <= 4 then especie else 'otros' end) as grouping,
sum(superficie) as superficie
from (SELECT name AS especie, SUM(superficie) AS superficie, #rn := #rn + 1 as rn
FROM ciclos
JOIN cultivos ON id_cultivo = idcultivo
JOIN tbl_especies ON id_especie = idespecie
cross join (select #rn := 0) const
WHERE fecha_cierre IS NULL
GROUP BY id_especie
ORDER BY superficie DESC
) t
group by (case when rn <= 4 then especie else 'otros' end)

SQL Find Position in table

I have a table in mySql which has the users ID and scores.
What I would like to do is organise the table by scores (simple) but then find where a certain user ID sits in the table.
So far I would have:
SELECT * FROM table_score
ORDER BY Score DESC
How would I find where userID = '1234' is (i.e entry 10 of 12)
The following query will give you a new column UserRank, which specify the user rank:
SELECT
UserID,
Score,
(#rownum := #rownum + 1) UserRank
FROM table_score, (SELECT #rownum := 0) t
ORDER BY Score DESC;
SQL Fiddle Demo
This will give you something like:
| USERID | SCORE | USERRANK |
-----------------------------
| 4 | 100 | 1 |
| 10 | 70 | 2 |
| 2 | 55 | 3 |
| 1234 | 50 | 4 |
| 1 | 36 | 5 |
| 20 | 33 | 6 |
| 8 | 25 | 7 |
Then you can put this query inside a subquery and filter with a userId to get that user rank. Something like:
SELECT
t.UserRank
FROM
(
SELECT *, (#rownum := #rownum + 1) UserRank
FROM table_score, (SELECT #rownum := 0) t
ORDER BY Score DESC
) t
WHERE userID = '1234';
SQL Fiddle Demo
For a given user id, you can do this with a simple query:
select sum(case when ts.score >= thescore.score then 1 else 0 end) as NumAbove,
count(*) as Total
from table_score ts cross join
(select ts.score from table_score ts where userId = '1234') thescore
If you have indexes on score and userid, this will be quite efficient.

How to combine near same item by SQL?

I have some data in database:
id user
1 zhangsan
2 zhangsan
3 zhangsan
4 lisi
5 lisi
6 lisi
7 zhangsan
8 zhangsan
I want keep order, and combine near same user items, how to do it?
When I use shell script, I will(data in file test.):
cat test|cut -d " " -f2|uniq -c
this will get result as:
3 zhangsan
3 lisi
2 zhangsan
But how to do it use sql?
If you try:
SET #name:='',#num:=0;
SELECT id,
#num:= if(#name = user, #num, #num + 1) as number,
#name := user as user
FROM foo
ORDER BY id ASC;
This gives:
+------+--------+------+
| id | number | user |
+------+--------+------+
| 1 | 1 | a |
| 2 | 1 | a |
| 3 | 1 | a |
| 4 | 2 | b |
| 5 | 2 | b |
| 6 | 2 | b |
| 7 | 3 | a |
| 8 | 3 | a |
+------+--------+------+
So then you can try:
SET #name:='',#num:=0;
SELECT COUNT(*) as count, user
FROM (
SELECT #num:= if(#name = user, #num, #num + 1) as number,
#name := user as user
FROM foo
ORDER BY id ASC
) x
GROUP BY number;
Which gives
+-------+------+
| count | user |
+-------+------+
| 3 | a |
| 3 | b |
| 2 | a |
+-------+------+
(I called my table foo and also just used names a and b because I was too lazy to write zhangsan and lisi over and over).
if in oracle, you can do like below.
SELECT NAME,
num - lagnum
FROM (SELECT lagname,
NAME,
num,
nvl(lag(num) over(ORDER BY num), 0) lagnum
FROM (SELECT id,
lag(NAME) over(ORDER BY ID) lagname,
NAME,
lead(NAME) over(ORDER BY ID) leadname,
ROWNUM num
FROM (SELECT * FROM test ORDER BY ID))
WHERE (lagname = NAME AND (NAME <> leadname OR leadname IS NULL))
OR (lagname IS NULL AND NAME <> leadname)
OR (lagname <> NAME AND leadname IS NULL)
ORDER BY ID);
if in sql server, oracle, db2...
with x as(
select c.*, rn = row_number() over (order by c.id)
from test c
left join test n
on c.[user] = n.[user]
and c.[id] + 1 = n.[id]
where n.id is null
)
select a.[user], a.id - coalesce(b.id, 0)
from x a
left join x b
on a.rn = b.rn + 1
I think what you are looking for is to COUNT(ID):
SELECT COUNT(ID) FROM table GROUP BY user
You cannot do this in sql without doing some sort of sequential (iterative) analysis. Remember sql is set operation language.
A little improvement to the selected answer would be not to have to define those variables. So this query can be solved in just a single statement:
SELECT COUNT(*) cnt, user
FROM (
SELECT #num := #num + (#name != user) as number,
#name := user as user
FROM t, (select #num := 0, #name := '') as s
ORDER BY id
) x
GROUP BY number
Output:
| CNT | USER |
|-----|----------|
| 3 | zhangsan |
| 3 | lisi |
| 2 | zhangsan |
Fiddle here