Aggregate fields selectively based on MAX(value) - mysql

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

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 Winning Streak for every Player

I have a table with winner and loser statistics from a game:
id winner_id loser_id
1 1 2
2 1 2
3 3 4
4 4 3
5 1 2
6 2 1
7 3 4
8 3 2
9 3 5
10 3 6
11 2 3
12 3 6
13 2 3
I want a result table where i can find the highest winning streak of every player in the game. A streak of a player is broken, when he lost a game (player_id = loser_id). It should look like:
player_id win_streak
1 3
2 2
3 4
4 1
5 0
6 0
I tried many queries with user defined variables etc. but i can't find a solution. Thanks!
SQL Fiddle : http://sqlfiddle.com/#!9/3da5f/1
Is this the same as Alex's approach; I'm not quite sure, except that it seems to have one distinct advantage.... ;-)
SELECT player_id, MAX(CASE WHEN result = 'winner' THEN running ELSE 0 END) streak
FROM
( SELECT *
, IF(player_id = #prev_player,IF(result=#prev_result,#i:=#i+1,#i:=1),#i:=1) running
, #prev_result := result
, #prev_player:=player_id
FROM
( SELECT id, 'winner' result, winner_id player_id FROM my_table
UNION
SELECT id, 'loser', loser_id FROM my_table
) x
,
( SELECT #i:=1,#prev_result = '',#prev_player:='' ) vars
ORDER
BY x.player_id
, x.id
) a
GROUP
BY player_id;
I guess you should better to do that on php (or any other language you use) side.
But just to give you some idea and as experiment and example for some unique cases (hope it could be useful somewhere)
Here is my approach:
http://sqlfiddle.com/#!9/57cc65/1
SELECT r.winner_id,
(SELECT MAX(IF(winner_id=r.winner_id,IF(#i IS NULL, #i:=1,#i:=#i+1), IF(loser_id = r.winner_id, #i:=0,0)))
FROM Results r1
WHERE r1.winner_id = r.winner_id
OR r1.loser_id = r.winner_id
GROUP BY IF(winner_id=r.winner_id, winner_id,loser_id)) win_streak
FROM ( SELECT winner_id
FROM Results
GROUP BY winner_id
) r
It returns not all ids now but only who had ever win. So to make it better, probably you have user table. If so it would simplify a query. If you have no user table you need to union all somehow users who had never win.
You are welcome if any questions.

How to get rank in MySQL from 2 tables?

I have 2 different tables in my database by the name of: rank, settings.
Here is how each table looks like with a few records in them:
Table #rank:
id points userid
-- ----- ------
1 500 1
2 300 2
3 900 3
4 1500 4
5 100 5
6 700 6
7 230 7
8 350 8
9 850 9
10 150 10
Table #settings:
userid active
------ ------
1 0
2 1
3 1
4 1
5 1
6 0
7 1
8 1
9 0
10 1
I want to get the rank of a specific user by user_id from the rank table ordering by their points. Also I would Only want to include the users in the ranking results, if they have active = 1 set in the settings table.
I have a simple ranking query, but it is not really effective, because it does include everyone even if the user is not active:
SELECT * FROM
(SELECT #sort:=#sort+1 AS sort, points, userid
FROM rank,
(SELECT #sort := 0) s
ORDER BY points DESC) t
WHERE userid= 8
Any idea, how could I achieve my goals here?
Few sub queries. First gets all the users who are active in the right order. That is used as a source for another query to add the rank. Then this is used as the source for the points and rank for the userid you are actually interested in
SELECT sort, points
FROM
(
SELECT #sort:=#sort + 1 AS sort, points, userid
FROM
(
SELECT rank.points, rank.userid
FROM rank
INNER JOIN settings
ON rank.userid = settings.userid
WHERE settings.active = 1
ORDER BY points DESC
) sub0
CROSS JOIN (SELECT #sort:=0) sub2
) sub1
WHERE sub1.userid = 8
Borrowing the idea from: https://stackoverflow.com/a/4474389/92063
SELECT
#rn:=#rn+1 AS RANK
,USER_ID
,POINTS
FROM (
SELECT
R.userid AS USER_ID
,R.points AS POINTS
FROM
rank R
INNER JOIN
settings S
ON R.userid = S.userid
WHERE
S.active = 1
ORDER BY
R.points DESC
) t1, (SELECT #rn:=0) t2;

How to make a fake column with an autoincrement number in a "group by" query

I have data in a table like this:
fgid qty ntid
1 100 10
2 90 10
6 200 11
1 80 11
1 120 12
6 100 12
6 30 13
And i make query :
SELECT fgid, SUM(qty) AS total_qty, COUNT(ntid) AS nt_count FROM sofg
GROUP BY fgid
AND the result is :
fgid total_qty nt_count
1 300 3
2 90 1
6 330 3
Then i want to make the result like this :
no fgid total_qty nt_count
1 1 300 3
2 2 90 1
3 6 330 3
How to do that with a query? where 'no' is (like) autoincrement number.
Try this query.
SELECT
#rownum := #rownum + 1 rownum,
t.*
FROM (SELECT #rownum:=0) r,
(
SELECT fgid, SUM(qty) AS total_qty, COUNT(ntid) AS nt_count FROM sofg GROUP BY fgid
) t;
Basically the same as Dhinakaran's answer, but there's no need to put the whole main query into a subquery. There's no difference to his answer appart from maybe being more pleasing to the eye, but please accept Dhinakaran's answer, as he was faster.
SELECT
#rownum:=#rownum + 1 as rownumber,
fgid,
SUM(qty) AS total_qty,
COUNT(ntid) AS nt_count
FROM sofg
, (select #rownum:=0) v
GROUP BY fgid