SQL User Score ranking by grouping - mysql

I have the following ranking system.
SET #1=0;
SELECT id, username, magic_xp, #i:=#i+1 AS rank
FROM hs_users
ORDER
BY magic_xp DESC;
hs_users
id username magic_xp rank
988 5hapescape 14926854 1
737 Ozan 13034431 2
989 Kurt 13034431 3
6 LEGACY 0 4
11 Bobby 0 5
276 Bobby123 0 6
345 Mynamesjason 0 7
450 Demon Spawn 0 8
987 Satan 0 9
As you see I have 2 users have the same xp.
I want to make them both have rank = 2 and the rest should follow from 3.
How can I group them like this?
| username | magic_xp | rank |
| ---------- + -------- + ---- |
| ShapeScape | 1000 | 1 |
| Kurt | 100 | 2 |
| Ozan | 100 | 2 |
| Legacy | 10 | 3 |

In MySQL, the most efficient way is to use variables:
select t.*,
(#rank := if(#magic_xp = magic_xp, #rank,
if(#magic_xp := magic_xp, #rank + 1, #rank + 1)
)
) as rank
from table t cross join
(select #rank := 0, #magic_xp := NULL) params
order by magic_xp desc;
Note the complicated expression for the variables. The assignment of both variables is in a single expression. This is on purpose. MySQL does not guarantee the order of assignment of expressions in a SELECT, and sometimes, it does not even evaluate them in order. A single expression is the safe way to do this logic.
A more standard approach in SQL is to use a correlated subquery:
select t.*,
(select count(distinct t2.magic_xp)
from table t2
where t2.magic_xp >= t.magic_xp
) as rank
from table t;

query
set #i := 0;
set #lagxp := null;
select id, username, magic_xp,
#i := if(#lagxp = magic_xp, #i,
if(#lagxp := magic_xp, #i + 1, #i + 1)) as rank
from hs_users
order by magic_xp desc
;
or
SELECT id, username, magic_xp,
IF (#score=hs_users.magic_xp, #rank:=#rank, #rank:=#rank+1) as rank,
#score:=hs_users.magic_xp score
FROM hs_users, (SELECT #score:=0, #rank:=0) r
ORDER BY magic_xp DESC;
output
+-----+------------+----------+------+----------+
| id | username | magic_xp | rank | lagxp |
+-----+------------+----------+------+----------+
| 988 | Shapescape | 14926894 | 1 | 14926894 |
| 737 | Ozan | 13034431 | 2 | 13034431 |
| 989 | Kurt | 13034431 | 2 | 13034431 |
| 6 | Legacy | 0 | 3 | 0 |
+-----+------------+----------+------+----------+
sqlfiddle

Sound the solution :)
SELECT id, username, magic_xp,
IF (#score=hs_users.magic_xp, #rank:=#rank, #rank:=#rank+1) as rank,
#score:=hs_users.magic_xp score
FROM hs_users, (SELECT #score:=0, #rank:=0) r
ORDER BY magic_xp DESC;
Thanks to #amdixon

select
#rank:=if(magic_xp=#prev_magic_xp,#rank,#rank+1) as rank,
username,
magic_xp,
#prev_magic_xp:=magic_xp as prev_magic_xp
from user,(select #rank:=0,#prev_magic_xp="") t
order by magic_xp desc
For your reference: http://sqlfiddle.com/#!9/09bb3/2

Related

How to convert this query from MySQL to SQL Server?

Here is the source table:
+----+-------+
| Id | Score |
+----+-------+
| 1 | 3.50 |
| 2 | 3.65 |
| 3 | 4.00 |
| 4 | 3.85 |
| 5 | 4.00 |
| 6 | 3.65 |
+----+-------+
Here is the result table:
+-------+------+
| Score | Rank |
+-------+------+
| 4.00 | 1 |
| 4.00 | 1 |
| 3.85 | 2 |
| 3.65 | 3 |
| 3.65 | 3 |
| 3.50 | 4 |
+-------+------+
I have the MySQL version query, how to convert it to SQL server version? I tried to do the declare but I have no idea how to update the value of the variables.
SELECT Score, ranking AS Rank
FROM
(
SELECT
Score,
CASE
WHEN #dummy = Score
THEN #ranking := #ranking
ELSE #ranking := #ranking + 1
END as ranking,
#dummy := Score
FROM Scores, (SELECT #ranking := 0, #dummy := -1) init
ORDER BY Score DESC
)AS Result
Your code is a MySQL work-around for the ANSI standard DENSE_RANK() function (as explained by Sean Lange in a comment). The code simply looks like:
SELECT s.*, DENSE_RANK() OVER (ORDER BY Score DESC) as rank
FROM Scores s
ORDER BY Score DESC;
Incidentally, the MySQL code itself is not really accurate. The following is much safer:
SELECT Score, ranking AS Rank
FROM (SELECT Score,
(#rn := if(#dummy = Score, #rn,
if(#dummy := Score, #rn + 1, #rn + 1)
)
) as ranking
FROM Scores CROSS JOIN
(SELECT #rn := 0, #dummy := -1) init
ORDER BY Score DESC
) s;
The key difference is that all the assignments to variables occur in a single expression. MySQL does not guarantee the order of evaluations of expressions in a SELECT, so you should not use #dummy in one expression and then assign it in another.
SQL Server doesn't support variables used in that manner, so the syntax doesn't translate. Scalar variables are set once by a query, not set once for each row of the result set. MySQL's syntax here is a hack to get analytic-function-like behavior without analytic function support. You should just use:
SELECT Score,
DENSE_RANK() OVER(ORDER BY Score DESC) AS Rank
FROM Scores
ORDER BY Score DESC;
If you insist on not using DENSE_RANK(), you can use the SQL Server syntax from SQL Server 2000:
SELECT s1.Score,
(SELECT COUNT(DISTINCT s2.Score) FROM Scores s2 WHERE s1.Score <= s2.Score) AS Ranking
FROM Scores s1
ORDER BY s1.Score DESC;

How can I grouping an unix time per day?

I have a table like this:
// requests
+----+----------+-------------+
| id | id_user | unix_time |
+----+----------+-------------+
| 1 | 2353 | 1339412843 |
| 2 | 2353 | 1339412864 |
| 3 | 5462 | 1339412894 |
| 4 | 3422 | 1339412899 |
| 5 | 3422 | 1339412906 |
| 6 | 2353 | 1339412906 |
| 7 | 7785 | 1339412951 |
| 8 | 2353 | 1339413640 |
| 9 | 5462 | 1339413621 |
| 10 | 5462 | 1339414490 |
| 11 | 2353 | 1339414923 |
| 12 | 2353 | 1339419901 |
| 13 | 8007 | 1339424860 |
| 14 | 7785 | 1339424822 |
| 15 | 2353 | 1339424902 |
+----+----------+-------------+
I want to grouping unix_time column based on separated days. Actually I'm trying to make this for an specific user:
As you see I need tow numbers for an user:
the number of all days which there is a foot print of the user into requests table
the number of biggest consecutive days
How can I do that?
Actually I can use WHERE id_user = :id to select user's rows. And I can calculate the number of days by SUM(). And by using MAX() I can calculate the biggest consecutive range. Just I need to grouping those unix times.
Please give it a try:
SELECT
t.id_user,
COUNT(*) totalVisits,
MAX(t.max_cons) maxCons
FROM
(SELECT
id_user,
#lastUnixTime AS lastUnixTimeOfuser,
IF(#uid <> id_user, #currentMax := 1 , #currentMax),
IF(#uid <> id_user, #lastUnixTime := 0, #lastUnixTime := #lastUnixTimeOfLastRecord),
IF(#uid = id_user,
IF((#lastUnixTime + 86400) >= utime, #currentMax := #currentMax + 1, #currentMax := 1), #lastUnixTime := 0),
IF(#currentMax > #max, #max := #currentMax, #max ),
IF(#uid <> id_user , #max := 1 ,#max),
#uid := id_user,
#lastUnixTimeOfLastRecord := utime,
#max AS max_cons
FROM
(
SELECT
id_user,
(unix_time DIV 86400) * 86400 AS utime
FROM requests
GROUP BY id_user, utime ) dayWiseRequestTable ,
(
SELECT
#uid := 0,
#currentMax := 0,
#max := 0,
#lastUnixTime := 0,
#lastUnixTimeOfLastRecord := 0
) vars
ORDER BY id_user, utime) t
GROUP BY t.id_user;
SQL FIDDLE DEMO
Output:
The final output looks like below:
id_user Total_Visits Maximum_Consecutive_Visits
2353 7 2
3422 2 2
5462 3 2
7785 2 1
8007 1 1
EDIT:
In order to get output for a specific user you need to add a WHERE clause in the inner query.
Please check this SQL FIDDLE
Use can extract the day using from_unixtime(). Then you can get count the days using variables:
select id_user, d,
(#rn := if(#di = concat_ws(':', d - interval 1 day, id_user), #rn + 1,
if(#di := concat_ws(':', d, id_user), 1, 1)
)
) as rn
from (select id_user, date(from_unixtime(unix_time)) as d
from t
group by id_user, d
) cross join
(select #di := '', #rn := 0) params
order by id_user, d;
From here to the summary is just an aggregation:
select id_user, count(*) as numdays, max(rn) as maxconsecutive
from (select id_user, d,
(#rn := if(#di = concat_ws(':', d - interval 1 day, id_user), #rn + 1,
if(#di := concat_ws(':', d, id_user), 1, 1)
)
) as rn
from (select id_user, date(from_unixtime(unix_time)) as d
from t
group by id_user, d
) cross join
(select #di := '', #rn := 0) params
order by id_user, d
) ud
group by id_user;
Here is a SQL Fiddle illustrating the code.

Why outer order by does not work correctly?

I have a query like this:
SELECT #rank := #rank + 3 `rank`, id, subject, name
FROM quran, (select #rank := -2) q
WHERE MATCH (subject, name) AGAINST ('anything') and aye IN ("10")
UNION DISTINCT
SELECT #rank1 := #rank1 + 3 `rank`, id, subject, name
FROM quran, (select #rank1 := -1) q
WHERE MATCH (subject, name) AGAINST ('anything')
UNION ALL
SELECT #rank2 := #rank2 + 3 `rank`, id, subject, byA
FROM hadith, (select #rank2 := 0) q
WHERE MATCH (subject) AGAINST ('anything')
ORDER BY rank LIMIT 0, 11
Now I optimized my query and combined tow first SELECT clause to one, like this: (because they have the same table name)
(SELECT #rank1 := #rank1 + 2 `rank`, id, subject, name
FROM quran, (select #rank1 := -1) q
WHERE MATCH (subject, name) AGAINST ('anything')
ORDER BY CASE WHEN aye IN ('10')
THEN 0
ELSE 1
END
)
UNION ALL
(SELECT #rank2 := #rank2 + 2 `rank`, id, subject, byA
FROM hadith, (select #rank2 := 0) q
WHERE MATCH (subject) AGAINST ('anything')
)
ORDER BY rank LIMIT 0, 11
But I don't know why the sort of result is not identical with the first query. Why? And how can I fix it?
Edit: Here is some examples:
// quran // hadith
+----+---------+--------+ +----+---------+-------+
| id | subject | name | | id | subject | byA |
+----+---------+--------+ +----+---------+-------+
| 1 | hello | jack | | 1 | blue | jack |
| 2 | blue | peter | | 2 | how | hello |
| 3 | jack | red | | 3 | jack | blue |
| 4 | back | blue | +----+---------+-------+
| 10 | jack | how |
+----+---------+--------+
Now, I want this output: So first priority is that $number, and then subject column and then name column, Also the result is alternating for both tables.
$anything = 'jack', $number = 10
+----+---------+--------+
| id | subject | name |
+----+---------+--------+
| 10 | jack | how |
| 3 | jack | blue |
| 3 | jack | red |
| 1 | blue | jack |
| 1 | hello | jack |
+----+---------+--------+
I'm not saying this is the best way to go about things, but it is the least modification to your existing attempts.
(
SELECT IF(aye IN ("10"), 0, 1) AS sortGroup
, IF(aye IN ("10"), #rank := #rank + 3, #rank1 := #rank1 + 3) AS `rank`
, id, subject, name
FROM quran
, (select #rank := -2) AS rq, (select #rank1 := -1) AS r1q
WHERE MATCH (subject, name) AGAINST ('anything')
)
UNION ALL
(
SELECT 2 AS sortGroup
, #rank2 := #rank2 + 2 `rank`
, id, subject, byA
FROM hadith
, (select #rank2 := 0) AS q
WHERE MATCH (subject) AGAINST ('anything')
)
ORDER BY sortGroup, rank
LIMIT 0, 11
Actually, I am not positive you can merge the first two unioned queries and get the same results. In the original query, with UNION DISTINCT and the separate computation of rank in the original, records that satisfy the aye IN ("10") criteria will probably often appear twice (but with different rank values).

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