how to select first two rows from each group - mysql

Hi We have 3 Table of a music which is something like this in MySql :
1st Table :
the first table is for playlist table where music playlist is exist.
playlistId playlistTitle categoryId
1 hello 0
2 wow 0
3 wi-fi 0
4 awesome 0
5 sixer 1
6 four 1
7 boundary 2
2nd Table :
2nd table is for `songRelation table where every playlist is associated with thier song
playlistId songId
1 4
1 3
1 43
1 57
1 98
2 56
2 67
2 90
2 78
3 98
3 78
3 89
43 90
3rd Table :
the 3rd table is for song table where song detail exist
songId songTitle
4 hello
3 real hero
43 singalone
57 awesom
98 really
78 sakaka
98 shikwa
89 moha
90 hello2
67 Sneh
actually i want to get result something like this :
playlistId songId categoryId songTitle
1 4 0 hello
1 3 0 real hero
2 56 0 singalone
2 67 0 Sneh
3 78 0 sakaka
3 98 0 Shikwa
where the every playlistId will be with their first 2 songId and with their categoryId and also with songTitle.

You can use variables:
SELECT playlistId, songId, categoryId, songTitle
FROM (
SELECT p.playlistId, s.songId, p.categoryId, s.songTitle,
#r := IF (#pid = p.playlistId,
IF (#pid := p.playlistId, #r+1, #r+1),
IF (#pid := p.playlistId, 1, 1)) AS rn
FROM playlist AS p
CROSS JOIN (SELECT #r:=0, #pid:=0) AS vars
INNER JOIN songRelation AS sr ON p.playlistId = sr.playlistId
INNER JOIN song AS s ON sr.songid = s.songid
ORDER BY p.playlistId, s.songId ) AS t
WHERE t.rn <= 2
Variable #r is used to enumerate records within each playlistId slice. Using this in an outer query, we can easily get 2 records per playlistId slice.
Demo here

you can do this using JOIN, try this code:-
SELECT playlist.playlistid, songRelation.songId, song.songTitle
FROM playlist JOIN songRelation JOIN song
WHERE playlist.playlistId=songRelation.playlistId
AND songRelation.songId=song.songId LIMIT 2
you can add the category table and use the same way to get the result table you need, you can also save this query as a virtual table VIEW by writing this code before the code:-
CREATE VIEW myView AS
Edit:-
SELECT playlist.playlistId from playlist INNER JOIN( SELECT playlist.playlistId, songRelation.songId, song.songTitle
FROM playlist JOIN songRelation JOIN song
WHERE playlist.playlistId=songRelation.playlistId
AND songRelation.songId=song.songId
GROUP BY playlist.playlistId LIMIT 2)
WHERE playlist.playlistId=songRelation.playlistId

Please try it, as it is not tested with data, so if you get any issue then create an sqlfiddle so that I can solve the issue.
SELECT x.*
FROM (SELECT pl.playlistid, sng.songId, sng.songTitle,
CASE
WHEN #category != pl.playlistid THEN #rownum := 1
ELSE #rownum := #rownum + 1
END AS rank,
#category := pl.playlistid AS play_list
FROM playlist AS pl
JOIN songRelation AS sr ON sr.playlistid=pl.playlistid
JOIN song AS sng ON sng.songid=sr.songid
JOIN (SELECT #rownum := NULL, #category := '') r
ORDER BY pl.playlistid,sng.songid) X
WHERE x.rank<=2

Related

MySQL Leaderboard Table

I'm trying to figure out how to Select a specific number of rows from a MySQL table based on WHERE clause. I have a table with 10 dummy users, I want to get 2 previous and 2 next users of specific user with their ranks.
user_id | points
==================
10 200
4 130
2 540
13 230
15 900
11 300
3 600
17 110
20 140
1 430
5 800
I achieved adding a column for ranking like:
user_id | points | rank
===========================
15 900 1
5 800 2
3 600 3
2 540 4
1 430 5
11 300 6
13 230 7
10 200 8
20 140 9
4 130 10
17 110 11
But the problem is that I want only 5 rows. Suppose I'm retrieving data for user with user_id = 11. The output should look like this:
user_id | points | rank
===========================
2 540 4
1 430 5
11 300 6
13 230 7
10 200 8
where user_id = 11 is in the centre with 2 rows above and 2 below. I have tried nesting UNIONS and SELECT statements but nothing seems to work properly.
Here's a suggestion if you're on MySQL 8+:
WITH cte AS (
SELECT user_id, points,
ROW_NUMBER() OVER (ORDER BY points DESC) AS Rnk
FROM mytable)
SELECT cte2.user_id,
cte2.points,
cte2.Rnk
FROM cte cte1
JOIN cte cte2
ON cte1.user_id=11
AND cte2.Rnk >= cte1.Rnk-2
AND cte2.Rnk <= cte1.Rnk+2
Using common table expression (cte) then do a self join with condition of user_id=11 as base to get the Rnk value of -2 and +2.
Demo fiddle
Since you're on older MySQL version, here's what I can suggest:
SET #uid := 11;
SET #Rnk := (SELECT Rnk
FROM
(SELECT user_id, points,
#r := #r+1 AS Rnk
FROM mytable
CROSS JOIN (SELECT #r := 0) r
ORDER BY points DESC) v
WHERE user_id = #uid);
SELECT user_id, points, Rnk
FROM
(SELECT user_id, points,
#r := #r+1 AS Rnk
FROM mytable
CROSS JOIN (SELECT #r := 0) r
ORDER BY points DESC) v
WHERE Rnk >= #Rnk-2
AND Rnk <= #Rnk+2;
If you will only use user_id as base, then the only part here you need to change is the SET #uid. The remaining queries are just fulfilling your condition of getting two positions above and below the rank retrieved according to the user_id. The base query in SET #Rnk is the same as the base query for the last one. The idea is to assign #Rnk variable with Rnk position of user_id=11 then use it in WHERE condition for the last query.
I'm not aware if there's any online fiddle still using MySQL 5.1 but here's probably the closest version to it, MySQL 5.5 demo fiddle.

"SELECT id, title, #natusort:=#natusort + 1 AS ordercount" does not increment as expected

My query in MySQL does not behave as expected.
SET #natusort := 0;
SELECT id, title, #natusort:=#natusort + 1 AS ordercount
FROM categories
JOIN table1 ON id = table1.parentid
ORDER BY title LIMIT 10
I expected a set of results like this:
ID title ordercount
------------------------------------
67 aaa 1
23 aab 2
65 aac 3
47 aad 4
78 aba 5
32 abc 6
43 abd 7
33 aca 8
46 acb 9
12 acd 10
But I got this set instead:
ID title ordercount
------------------------------------
67 aaa 12
23 aab 3
65 aac 12
47 aad 34
78 aba 4
32 abc 36
43 abd 31
33 aca 15
46 acb 19
12 acd 50
How can I get the increment to work sequentially starting from 1 and follow the order by?
You can use ROW_NUMBER(), as in:
SELECT id, title,
row_number() over(order by title) as ordercount
FROM categories
JOIN table1 ON id = table1.parentid
ORDER BY title
LIMIT 10
What appears to be happening here is that first your sequence is being generated across the result set, and then you are limiting to 10 records based on some order. What you're left with isn't necessarily a sequence from 1 to 10. The best fix here might be to use ROW_NUMBER, if you are using MySQL 8+. If you must stick with your current approach, then wrap in a subquery before generating the sequence:
SELECT id, title, #natusort:=#natusort + 1 AS ordercount
FROM
(
SELECT id, title
FROM categories
INNER JOIN table1 ON id = table1.parentid
ORDER BY title
LIMIT 10
) t
ORDER BY title;
For the ROW_NUMBER option, just change your select to:
SELECT id, title, ROW_NUMBER() OVER (ORDER BY title) AS ordercount
FROM categories
...
You should use row_number() in MySQL 8+.
The issue you are having is that ORDER BY and GROUP BY are not compatible with variables in more recent versions of MySQL pre-8.0. I don't remember exactly when this stopped working, but I have in mind GROUP BY stopped working in 5.6 and ORDER BY in 5.7. I wish I could forget such trivia.
In any case, the solution is to order in a subquery:
SELECT tc.*, (#natusort := #natusort + 1) AS ordercount
FROM (SELECT id, title
FROM categories c JOIN
table1 t1
ON c.id = t1.parentid
ORDER BY title
) tc CROSS JOIN
(SELECT #natusort := 0) params
ORDER BY title
LIMIT 10;
Note that I've included the initialization of #natusort in the same query, so only one statement is necessary.
If using SET #natusort := 0at the beginning is not working then you can Initialize it using joins example :
SELECT id, title, (#natusort:=#natusort + 1) AS ordercount
FROM categories
JOIN table1 ON id = table1.parentid
inner join (SELECT #natusort := 0)
ORDER BY title LIMIT 10

MySQL Create Ordinal

I'm trying to create a temp table or view that I can use for charting league members changing handicaps over the season. This requires creating a pivot table.
Even before that, however, I need to create an ordinal column to represent the nth match the person played that season. I can't use date_played since players can play on all different dates, and I'd like each player's 2nd match to line up vertically, and their third to do so as well, etc.
I used code from the answer to this question which seemed like it should work. Here's a copy of the relevant section:
SELECT books.*,
if( #libId = libraryId,
#var_record := #var_record + 1,
if(#var_record := 1 and #libId := libraryId, #var_record, #var_record)
) AS Ordinal
FROM books
JOIN (SELECT #var_record := 0, #libId := 0) tmp
ORDER BY libraryId;
`lwljhb_lwl_matches` # One record for each match played
id
date_played
other fields about the match
id date_played
1 2017-08-23
2 2017-08-29
3 2017-09-26
4 2017-08-24
5 2017-09-02
6 2017-09-21
7 2017-08-24
8 2017-08-31
9 2017-09-05
10 2017-09-15
11 2017-09-17
`lwljhb_users`
id
display_name
id display_name
1 Alan
2 Bill
3 Dave
`lwljhb_lwl_players` # One record per player per match
id
match_id # Foreign key to matches.id
player_id # Foreign key to users.id
end_hcp
other fields about the player's performance in the linked match
id match_id player_id end_hcp
1 1 1 720
2 2 1 692
3 3 1 694
4 4 2 865
5 5 2 868
6 6 2 842
7 7 3 363
8 8 3 339
9 9 3 332
10 10 3 348
11 11 3 374
Normally there would be two records in PLAYERS for each MATCH record, but I didn't add them here. The fact that the id and match_id are the same in every record of PLAYERS is artificial because this isn't real data.
Before I used this snippet, my code looked like this:
SELECT u.display_name,
m.date_played,
p.end_hcp
FROM `lwljhb_lwl_matches` AS m
INNER JOIN `lwljhb_lwl_players` AS p
INNER JOIN `lwljhb_users` AS u
ON m.id = p.match_id AND
p.player_id = u.id
WHERE league_seasons_id = 12 AND
playoff_round = 0
ORDER BY u.display_name, m.date_played
and generated data that looks like this:
display_name date_played end_hcp
Alan 2017-08-23 720
Alan 2017-08-29 692
Alan 2017-09-26 694
Bill 2017-08-24 865
Bill 2017-09-02 868
Bill 2017-09-21 842
Dave 2017-08-24 363
Dave 2017-08-31 339
Dave 2017-09-05 332
Dave 2017-09-15 348
Dave 2017-09-17 374
At any time during the season there will be different numbers of matches played by each of the players, but by the end of the season, they will have all played the same number of matches.
I tried to incorporate Alin Stoian's code into mine like so, changing a variable name and field names as I thought appropriate.
SET #var_record = 1;
SELECT u.display_name,
m.date_played,
p.end_hcp,
if( #player = u.display_name,
#var_record := #var_record + 1,
if(#var_record := 1 and #player := u.display_name, #var_record, #var_record)
) AS Ordinal
FROM `lwljhb_lwl_matches` AS m
INNER JOIN `lwljhb_lwl_players` AS p
INNER JOIN `lwljhb_users` AS u
JOIN (SELECT #var_record := 0, #player := 0) tmp
ON m.id = p.match_id AND
p.player_id = u.id
WHERE league_seasons_id = 12 AND
playoff_round = 0
ORDER BY u.display_name, m.date_played
I was hoping for a new column with the ordinals, but the new column is all zeros. Before I move on to trying the pivot table, I have to get these ordinals, so I hope someone here can show me my mistake.
Try this:
SELECT u.display_name,
m.date_played,
p.end_hcp,
#var_record := if ( #player = u.display_name,
#var_record + 1,
if(#player := u.display_name, 1, 1)
) AS Ordinal
FROM `lwljhb_lwl_matches` AS m
INNER JOIN `lwljhb_lwl_players` AS p
INNER JOIN `lwljhb_users` AS u
JOIN (SELECT #var_record := 0, #player := 0) tmp
ON m.id = p.match_id AND
p.player_id = u.id
WHERE league_seasons_id = 12 AND
playoff_round = 0
ORDER BY u.display_name, m.date_played
Demo
OK, it turns out that I wasn't getting all 1s in my Ordinal column after all, just in the 1st 25 records. Since it was obviously wrong I didn't look further.
The sample data I provided is in physical order as well as logical order, but MY ACTUAL data is not. That was the only difference I could think of, so I investigated further and found that in the 1st 1,000 records of output I got 7 records with 2 as the ordinal, not coincidentally where the m.ids were consecutive.
I created a view to put the data in order like in the sample and the JOINed that to the rest of the tables but still got bad data for the Ordinal.
When I swapped out the view for a temporary table it worked. It turns out that an ORDER BY in a view will be ignored if there's an ORDER by joining to it.
Bill Karwin pointed this out to me in a different question (46892912).

Aggregate fields selectively based on MAX(value)

I have a very large union query that produces the following results:
p_id title_id title is_live
1 37 TITANIC 1
2 37 TITANIC 0
3 41 AVATAR 0
4 41 AVATAR 0
5 44 HOME ALONE 0
6 11 COMPUTE 1
7 11 COMPUTE 1
8 11 COMPUTE 1
From this result, I want to aggregate it so that I get at least one of each title (title_id), but all titles with is_live = True. (In other words, for each title_id, I want to get exactly one result if MAX(is_live)=0, but all results if MAX(is_live)=1 for that title_id.) Here's an example of the result set that I want, with an explanation for each title:
p_id title_id title is_live
1 37 TITANIC 1 // live one for titanic
3 41 AVATAR 0 # at least one of each title (which one doesn't matter)
5 44 HOME ALONE 0 /* at least one of each title */
6 11 COMPUTE 1 // keep all, since all are live
7 11 COMPUTE 1
8 11 COMPUTE 1
The query that I have so far is along the lines of the following:
SELECT * FROM (
80-line UNION query
) d
GROUP BY
d.title_id
But this of course isn't accurate enough and is more an outline. How would I do the above?
Here is a SQLFiddle for the question: http://sqlfiddle.com/#!9/739a36/2/0
You can do this with variables:
select q.*
from (select q.*,
(#rn := if(#t = title, #rn + 1,
if(#t := title, 1, 1)
)
) as rn
from (<your query here>) q cross join
(select #t := '', #rn := 0) params
order by title, is_live desc
) q
where is_live = 1 or rn = 1;
Here is the SQL Fiddle.
One way to do it using union all, first selecting all is_live true rows and selecting one false is_live row for the title_id's not previously selected.
select *
from `a`
where is_live
union all
select max(p_id),title_id,max(title),max(is_live)
from `a` a1
where not is_live
and not exists (select 1 from `a` a2
where a1.title_id=a2.title_id and a2.is_live)
group by title_id

Create a Cumulative Sum Column in MySQL Based On an ID

I have a "simplified" table that looks like this:
player round point
1 1 25
2 1 18
3 1 15
1 2 18
2 2 25
3 2 15
I wanna create a view that calculates pointTot cumulatively based upon plrID
plrID rndID pnt [pointTot]
1 1 25 25
2 1 18 18
3 1 15 15
1 2 18 43
2 2 25 43
3 2 15 30
I've been playing around with different methods for the last few hours.
I would need a variable var based upon the plrID
This is as far as I got without being able to work out how to create a
#psum[#plrID]
set #psum := 0;
select `plrID`, `rndID`, `pnt`, (#psum := #psum + `pnt`) as `pointTot`
from `table`
order by `plrID`;
You can do this using below query
select t.plrID,t.rndID,t.pnt,sum(t1.pnt)
from table t
join table t1
on t.plrID = t1.plrID
and t1.rndID<=t.rndID
group by plrID,rndID
You can do this as:
select `plrID`, `rndID`, `pnt`,
(#psum := if(#p = plrId, #psum + pnt,
if(#p := plrId, pnt, pnt)
)
) as pointTot
from `table` cross join
(select #psum := 0, #p := -1) param
order by `plrID`, rndID;
You cannot add this as a view, because variables are not allowed in a view. You can use this version:
select `plrID`, `rndID`, `pnt`,
(select sum(t2.pnt)
from `table` t2
where t2.plrId = t.plrId and t2.rndId <= t.rndId
) as pointTot
from `table` t ;