Getting first N rows between 2 timestamps for each ID Mysql - mysql

I have table like below, start_t is my timestamp in unixtime for example (1438326239412) but for simplicity I wrote small numbers here:
user_id | start_t | duration
1 12 1
1 15 2
1 4 5
2 9 6
2 10 5
3 9 6
I want to get first N rows for each user_id between two time stamps. This is my code but it returns more than the limit I want:
SELECT us.* FROM (SELECT us.*, (#rn := if(#i = us.user_id, #rn + 1, if(#i := us.user_id, 1, 1) ) ) AS seqnum FROM user_stats us,tourn_user tu CROSS JOIN (SELECT #rn := 0, #i := -1) params WHERE (us.start_t+us.duration)<= 20 AND us.start_t >= 4 ORDER BY us.user_id, start_t ASC ) us WHERE seqnum <= 1
The result should look like this for that specific example:
user_id | start_t | duration
1 4 5
2 9 6
3 9 6

Here is the solution in case someone wants:
select us.* from (select us.*, (#rn := if(#i = us.user_id, #rn + 1, if(#i := us.user_id, 1, 1) ) ) as seqnum from user_stats us cross join (select #rn := 0, #i := -1) params where (us.start_t+us.duration)<= 15 AND us.start_t >= 0 order by us.user_id, start_t ASC ) us where seqnum <= 1

Related

SQL SUM TOP 5 values per category then SUM category totals per ID

I'm trying to produce an SQL statement to SUM top 5 values within categories per userID to create an overall total. Is this possible and how do I achieve it? I can sum the top 5 per single category or ALL but struggling to see how I can SUM each separate category total together.
For example,
ID Cater Weight
--------------------------------
1 Cheese 10
2 Bacon 15
1 Cheese 5
2 Bacon 10
1 Cheese 22
2 Cheese 5
1 Bacon 10
1 Cheese 10
2 Cheese 5
1 Cheese 20
2 Bacon 10
1 Cheese 30
The results i'm looking for is,
ID Total_Weight
-------------------
1 102 Top 5 Cheese (10+22+10+20+30) + Top 5 Bacon (10)
2 45 Top 5 Cheese (5+5) + Top 5 Bacon (15+10+10)
Any values outside of the Top 5 are ignored.
The code below displays the SUM of the top 5 weights from ALL categories AS total weight. Can I achieve what I want from a single statement?
$log = "SELECT id, cater,
SUM(weight) AS total_weight
FROM ( SELECT id,
CASE WHEN #ID = ID THEN #ROW_NUMBER := #ROW_NUMBER + 1
ELSE #ROW_NUMBER := 1
END AS rn,
cater,
weight,
#id := id
FROM individual,
(SELECT #ROW_NUMBER := 1, #ID := '') r
ORDER
BY
id, weight DESC
) TMP
WHERE rn <= 5
AND cater <> ''
GROUP
BY id
ORDER
BY total_weight DESC";
There might have some other better solution. But this will provide your expected result-
SELECT B.id,
SUM(T_weight) Total_Weight,
group_concat(concat('TOP 5 ',B.cater,' (',B.T,')') SEPARATOR ' ') Details
FROM
(
SELECT ID,cater,SUM(Weight) T_weight,group_concat(weight SEPARATOR '+') T
FROM
(
SELECT * FROM
(
SELECT id,cater, CASE WHEN #ID = ID THEN #ROW_NUMBER := #ROW_NUMBER + 1 ELSE #ROW_NUMBER := 1 END AS rn,weight,#id := id
FROM your_table,(SELECT #ROW_NUMBER := 1, #ID := ''
) r
WHERE cater = 'Cheese' ORDER BY id, weight DESC
)A WHERE rn < 6
UNION ALL
SELECT * FROM
(
SELECT id,cater, CASE WHEN #ID = ID THEN #ROW_NUMBER := #ROW_NUMBER + 1 ELSE #ROW_NUMBER := 1 END AS rn,weight,#id := id
FROM your_table,(SELECT #ROW_NUMBER := 1, #ID := ''
) r
WHERE cater = 'Bacon' ORDER BY id, weight DESC
)A WHERE rn < 6
UNION ALL
SELECT * FROM
(
SELECT id,cater, CASE WHEN #ID = ID THEN #ROW_NUMBER := #ROW_NUMBER + 1 ELSE #ROW_NUMBER := 1 END AS rn,weight,#id := id
FROM your_table,(SELECT #ROW_NUMBER := 1, #ID := ''
) r
WHERE cater = 'Cat3' ORDER BY id, weight DESC
)A WHERE rn < 6
UNION ALL
SELECT * FROM
(
SELECT id,cater, CASE WHEN #ID = ID THEN #ROW_NUMBER := #ROW_NUMBER + 1 ELSE #ROW_NUMBER := 1 END AS rn,weight,#id := id
FROM your_table,(SELECT #ROW_NUMBER := 1, #ID := ''
) r
WHERE cater = 'Cat4' ORDER BY id, weight DESC
)A WHERE rn < 6
UNION ALL
SELECT * FROM
(
SELECT id,cater, CASE WHEN #ID = ID THEN #ROW_NUMBER := #ROW_NUMBER + 1 ELSE #ROW_NUMBER := 1 END AS rn,weight,#id := id
FROM your_table,(SELECT #ROW_NUMBER := 1, #ID := ''
) r
WHERE cater = 'Cat5' ORDER BY id, weight DESC
)A WHERE rn < 6
UNION ALL
SELECT * FROM
(
SELECT id,cater, CASE WHEN #ID = ID THEN #ROW_NUMBER := #ROW_NUMBER + 1 ELSE #ROW_NUMBER := 1 END AS rn,weight,#id := id
FROM your_table,(SELECT #ROW_NUMBER := 1, #ID := ''
) r
WHERE cater = 'Cat6' ORDER BY id, weight DESC
)A WHERE rn < 6
)A
GROUP BY ID,Cater
)B
group by id
The output is-
1 191 TOP 5 Cheese (10+22+20+10+30) TOP 5 Cat3 (25+9+20+16+13) TOP 5 Bacon (10)
2 45 TOP 5 Cheese (5+5) TOP 5 Bacon (15+10+10)
If you have MySql version 8 or higher, you can use this code:
SELECT id, SUM (weight)
FROM (SELECT test.*,
ROW_NUMBER ()
OVER (PARTITION BY id, categ ORDER BY weight DESC)
rn
FROM test) sub
WHERE sub.rn < 6
GROUP BY id;
You need MySql 8+ because I used the function row_number and only from version 8 it exists.
In the below fiddle example, I used Microsoft Sql Server 2017 because they didn't had MySql 8 or higher.
You can see your example here.

Mysql group by (custom number for result)

I have a table
Id Name Id_collection Price
1 good1 2 10
2 good2 1 101
3 good3 3 102
4 good4 2 10
5 good5 2 10
I need to Group By id_collection, but i need to show 2 rows (to be able to change this value via variable or ... Ex: to change to 3 or 4 )
not
1
2
3
Ex
1
1
2
2
3
3
or
Ex
1
1
1
2
2
2
3
3
3
so the result must be
Id Name Id_collection Price
1 good1 2 10
4 good4 2 10
2 good2 1 101
3 good3 3 102
I was thinking about procedure or loop, but i didn't that before, Please help!!!
You want to group by adjacent values . . . in MySQL. You can use variables to assign the group. Alternatively, you can use this method to assign the group: count the number of rows that have id_collection different from each row with a smaller id.
You don't specify how to calculate the other columns, but here is a guess:
select min(id) as id, min(name) as name, id_collection, avg(price) as price
from (select t.*,
(select count(*)
from t t2
where t2.id_collection <> t.id_collection and
t2.id < t.id
) as grp
from t
) t
group by id_collection, grp;
EDIT:
I just realized that you probably don't want to aggregate the results; you probably just want the first row. For that, use variables:
select t.*
from (select t.*,
(#rn := if(#id = id_collection, #rn,
if(#id := id_collection, #rn + 1, #rn + 1)
)
) as rn
from t cross join
(select #id = -1, #rn := 0) params
order by id
) t
where rn = 1;
I edited a little the answer which #Gordon_Linoff posted, and it works now, you just have to change rn <= 3 this number and will get the various results.
#Gordon_Linoff - Thank you, this really helped me
select t.*
from (select t.*,
(#rn := if(#id = id_collection, #rn + 1,
if(#id := id_collection, 1, 0)
)
) as rn
from t cross join
(select #id := -1, #rn := 0) params
order by id_collection
) t
where rn <= 3;

Select recent n number of entries of all users from table

I have a below table and wants to select only last 2 entries of all users.
Source table:
-------------------------------------
UserId | QuizId(AID)|quizendtime(AID)|
--------------------------------------
1 10 2016-5-12
2 10 2016-5-12
1 11 2016-6-12
2 12 2016-8-12
3 12 2016-8-12
2 13 2016-8-12
1 14 2016-9-12
3 14 2016-9-12
3 11 2016-6-12
Expected output is like, (should list only recent 2 quizid entries for all users)
-------------------------------------
UserId | QuizId(AID)|quizendtime(AID)|
--------------------------------------
1 14 2016-9-12
1 11 2016-6-12
2 13 2016-8-12
2 12 2016-8-12
3 14 2016-9-12
3 12 2016-8-12
Any idea's to produce this output.
Using MySQL user defined variables you can accomplish this:
SELECT
t.UserId,
t.`QuizId(AID)`,
t.`quizendtime(AID)`
FROM
(
SELECT
*,
IF(#sameUser = UserId, #a := #a + 1 , #a := 1) row_number,
#sameUser := UserId
FROM your_table
CROSS JOIN (SELECT #a := 1, #sameUser := 0) var
ORDER BY UserId , `quizendtime(AID)` DESC
) AS t
WHERE t.row_number <= 2
Working Demo
Note: If you want at most x number of entries for each user then change the condition in where clause like below:
WHERE t.row_number <= x
Explanation:
SELECT
*,
IF(#sameUser = UserId, #a := #a + 1 , #a := 1) row_number,
#sameUser := UserId
FROM your_table
CROSS JOIN (SELECT #a := 1, #sameUser := 0) var
ORDER BY UserId , `quizendtime(AID)` DESC;
This query sorts all the data in ascending order of userId and descending order of quizendtime(AID).
Now take a walk on this (multi) sorted data.
Every time you see a new userId assign a row_number (1). If you see the same user again then just increase the row_number.
Finally filtering only those records which are having row_number <= 2 ensures the at most two latest entries for each user.
EDIT: As Gordon pointed out that the evaluation of expressions using user defined variables in mysql is not guaranteed to follow the same order always so based on that the above query is slightly modified:
SELECT
t.UserId,
t.`QuizId(AID)`,
t.`quizendtime(AID)`
FROM
(
SELECT
*,
IF (
#sameUser = UserId,
#a := #a + 1,
IF(#sameUser := UserId, #a := 1, #a:= 1)
)AS row_number
FROM your_table
CROSS JOIN (SELECT #a := 1, #sameUser := 0) var
ORDER BY UserId , `quizendtime(AID)` DESC
) AS t
WHERE t.row_number <= 2;
WORKING DEMO V2
User-defined variables are the key to the solution. But, it is very important to have all the variable assignments in a single expression. MySQL does not guarantee the order of evaluation of expressions in a select -- and, in fact, sometimes processes them in different orders.
select t.*
from (select t.*,
(#rn := if(#u = UserId, #rn + 1,
if(#u := UserId, 1, 1)
)
) as rn
from t cross join
(select #u := -1, #rn := 0) params
order by UserId, quizendtime desc
) t
where rn <= 2;

Getting the latest n records for each group

Lets say I have the following table:
id coulmn_id value date
1 10 'a' 2016-04-01
1 11 'b' 2015-10-02
1 12 'a' 2016-07-03
1 13 'a' 2015-11-11
2 11 'c' 2016-01-10
2 23 'd' 2016-01-11
3 11 'c' 2016-01-09
3 111 'd' 2016-01-11
3 222 'c' 2016-01-10
3 333 'd' 2016-01-11
for n = 3, I want to get the latest n records<=3 for each id. So I will have the following output:
id column_id value date
1 10 'a' 2016-04-01
1 12 'a' 2016-07-03
1 13 'a' 2015-11-11
2 11 'c' 2016-01-10
2 23 'd' 2016-01-11
3 111 'd' 2016-01-11
3 222 'c' 2016-01-10
3 333 'd' 2016-01-11
I am answering because the referenced question has an unstable answer (I'll comment on that there).
Here is a solution that should work:
select t.*
from (select t.*,
(#rn := if(#id = id, #rn + 1,
if(#id := id, 1, 1)
)
) as seqnum
from t cross join
(select #rn := 0, #id := -1) params
order by id, date desc
) t
where seqnum <= 3;
The difference in the solutions is that the variable assignments are all in a single expression. MySQL does not guarantee the order of evaluation of expressions, so this is very important if the code is going to work consistently.
You could do this with the use of variables. First go through the results in reverse order and assign a row number, then filter the results for row numbers less or equal to 3, and re-order:
select id, value, date
from (
select id, value, date,
#rn := if(#id = id, #rn+1, if (#id := id, 1, 1)) rn
from mytable,
cross join (#id := null, #rn := null) init
order by id, date desc
) as base
where rn <= 3
order by id, date asc

Ordinal Ranking in MySQL Update

I've got a table like this:
id name incidence placeRef
1 John 10 1
2 Ann 9 1
3 Paul 9 1
4 Carl 8 1
5 John 4 1
6 Ann 4 1
7 Paul 7 1
8 Carl 1 1
I want to rank these using the ordinal ranking method. Which would add ranks to my table as such:
id name incidence placeRef rank
1 John 10 1 1
2 Ann 9 1 2
3 Paul 9 1 2
4 Carl 8 1 4
5 John 4 1 2
6 Ann 4 1 2
7 Paul 7 1 1
8 Carl 1 1 4
How can this be achieved?
N.B. I am going to answer my own question, but would like to know if anyone has any better solutions as it is a bit hacky; though I found numerous posts recommending hacks for this situation.
You can do this with variables or a correlated subquery. The subquery method looks like this:
select t.*,
(select 1 + count(t2.incidence)
from table t2
where t2.incidence > t.incidence
) as rank
from table t;
The variables method is a bit trickier because you have to remember the number of matching rows with a given value:
select t.*,
(#rn := if(#i = t.incidence, if(#cnt := #cnt + 1, #rn, #rn),
#cnt + if(#i := t.incidence, if(#cnt := 1, #rn, #rn), #rn)
)
) as rank
from table t cross join
(select #i := 0, #rn := 0, #cnt := 1) vars
order by incidence desc;
EDIT:
If you want to update the table, just use update with a join:
update table t join
(<either subquery above>) s
on t.id = s.id
set t.rank = s.rank;
The following works:
UPDATE names
JOIN ( SELECT * FROM names ORDER BY placeRef, incidence DESC ) AS p ON p.id = names.id,
( SELECT #curRank := 0, #nextRank := 0, #prevInc := 9999999999, #prevPlace := 0 ) AS v
SET
names.rank = IF( #prevPlace != p.placeRef, #curRank := 0, 0 ),
names.rank = IF( #prevPlace != p.placeRef, #nextRank := 0, 0 ),
names.rank = IF( #prevInc = p.incidence, #nextRank := #nextRank + 1, #curRank := #nextRank := #nextRank + 1 ),
names.rank = IF( #prevInc = p.incidence, #curRank := #curRank, #curRank := #nextRank ),
names.incidence = #prevInc := names.incidence,
names.placeRef = #prevPlace := names.placeRef;
Explanation:
UPDATE names
1 - Sets the table to be updated
JOIN ( SELECT * FROM names ORDER BY placeRef, incidence DESC ) AS p ON p.id = names.id,
2 - This makes a virtual table with the results ordered, so that rankings can be applied
( SELECT #curRank := 0, #nextRank := 0, #prevInc := 9999999999, #prevPlace := 0 ) AS v
3 - This set some variables that will be used to tell when to incrament and reset the rank
names.rank = IF( #prevPlace != p.placeRef, #curRank := 0, 0 ),
4 - This is a hack that resets the current rank to 0 when MySQL iterates into a new place
names.rank = IF( #prevPlace != p.placeRef, #nextRank := 0, 0 ),
5 - This is a hack that resets the next rank to 0 when MySQL iterates into a new place
names.rank = IF( #prevInc = p.incidence, #nextRank := #nextRank + 1, #curRank := #nextRank := #nextRank + 1 ),
6 - This is a hack that updates the next and current ranks when the current incidence is the same as the previous incidence
names.rank = IF( #prevInc = p.incidence, #curRank := #curRank, #curRank := #nextRank ),
7 - This sets the rank to the same as the last rank when its incidence is the same as the previous or increments the rank if it isn't
names.incidence = #prevInc := names.incidence,
8 - This is a hack that sets a variable to contain the previous incidence, so we can tell what to do in the next itteration
names.placeRef = #prevPlace := names.placeRef;
9 - This is a hack that sets a variable to contain the previous place, so we can tell what to do in the next itteration