Create a MS SQL Server query to determine ranking - sql

I'm creating an application where users do workouts. They pass on their results via an app, and these results are stored in an SQL Server database. Results are saved in this way in a SQL Server table:
I want to write a query to create a ranking based on the best score of each user. This is what I have so far (thanks to this post):
SELECT id, workout_id, level_id, user_id, total_time, score, datetime_added
FROM nodefit_rankings_fitness nrf
WHERE nrf.id = (SELECT TOP (1) nrf2.id
FROM nodefit_rankings_fitness nrf2
WHERE nrf2.user_id = nrf.user_id
ORDER BY nrf2.score DESC
)
ORDER BY score DESC, datetime_added DESC;
This generates following, where a ranking is created based on the best score for a user:
When a certain user submits a new workout, I want to check his ranking based on the last submitted workout, compared to the best performances of other users. So suppose user_id 2 adds a new workout, and his score is, say, 12, what is his current ranking based on that new performance? In that case he has a second place in this table. Thanks.

Try this:
DECLARE #CurrentUser INT = 5;
WITH DataSource AS
(
SELECT id,
workout_id,
level_id,
user_id,
total_time,
score,
datetime_added,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY score DESC, datetime_added DESC) rowID
FROM nodefit_rankings_fitness
WHERE user_id <> #CurrentUser
UNION ALL
SELECT id,
workout_id,
level_id,
user_id,
total_time,
score,
datetime_added,
ROW_NUMBER() OVER (ORDER BY datetime_added DESC) rowID
FROM nodefit_rankings_fitness
WHERE user_id = #CurrentUser
)
SELECT *
,ROW_NUMBER() OVER (ORDER BY score DESC) AS RankID
FROM DataSource
WHERE rowID = 1
ORDER BY RankID

Related

Optimize a query for creating a ranking in MS SQL Server

I'm creating an application where users do workouts. They pass on their results via an app, and these results are stored in an SQL Server database. Results are saved in this way in a SQL Server table:
I want to write a query to create a ranking based on the best score of each user. This is what I have so far:
SELECT id,
workout_id,
level_id,
a.user_id,
total_time,
score,
datetime_added
FROM nodefit_rankings_fitness as a INNER JOIN
(
SELECT user_id,
MAX(score) AS MAXSCORE
FROM nodefit_rankings_fitness
GROUP BY user_id
) AS lookup
ON lookup.user_id = a.user_id
AND
lookup.MAXSCORE = a.score
ORDER BY score DESC,
datetime_added DESC
This generates this ranking:
The problem is that if a user has achieved the same maximum score a number of times, he will appear multiple times in the ranking. The query must be adjusted so that when a user has the same maximum score a few times, only the result of the last attempt (based on the datetime_added column) is displayed in the rankings.
Unfortunately, I cannot find a solution myself. Help is certainly appreciated.
If you care about performance, you should also try a correlated subquery:
SELECT id, workout_id, level_id, a.user_id, total_time, score, datetime_added
FROM nodefit_rankings_fitness nrf
WHERE nrf.id = (SELECT TOP (1) nrf2.id
FROM nodefit_rankings_fitness nrf2
WHERE nrf2.user_id = nrf.user_id
ORDER BY nrf2.score DESC
)
ORDER BY score DESC, datetime_added DESC;
In particular, this can take advantage of an index on nodefit_rankings_fitness(user_id, score desc, id).
Window functions make stuff like this easy. Something like:
SELECT id, workout_id, level_id, user_id, total_time, score, datetime_added
FROM (SELECT *, row_number() OVER (PARTITION BY user_id ORDER BY score DESC, datetime_added DESC) AS rn
FROM nodefit_rankings_fitness) AS a
WHERE rn = 1
ORDER BY score DESC, datetime_added DESC;

SQL return all fields from max of count

I have tried but just cannot get the results so I'm asking for help here. I have a table of logins for users on computers. I want to count the number of times a user has logged into a computer and then get the max so that max user is considered the primary user of the computer.
Depending on what dbms you are using, you can use a window function ROW_NUMBER() to rank the counts per comp. Then pull the highest ranking.
SELECT
Comp,
[User],
Cnt
FROM
(SELECT
Comp,
[User],
COUNT(1) AS Cnt,
ROW_NUMBER() OVER (PARTITION BY Comp ORDER BY Count(1) DESC) AS Rnk
FROM UserHist
GROUP BY Comp, [User]) BaseTable
WHERE Rnk = 1
You can RANK the counts and then select the largest. This uses a CTE to get the data with the count and the rank.
WITH CTECOMP AS
(
SELECT COMP, [user], COUNT([USER]) [USERCOUNT],
RANK() OVER (PARTITION BY COMP ORDER BY COUNT([USER]) DESC ) [MYRANK]
FROM #TableData
GROUP BY comp, [user]
)
SELECT *
FROM CTECOMP
WHERE MYRANK = 1
select Comp, [User], count([User]) as MaxCnt
from TableHere
group by Comp, [User]

Aggregate function like MAX for most common cell in column?

Group by the highest Number in a column worked great with MAX(), but what if I would like to get the cell that is at most common.
As example:
ID
100
250
250
300
200
250
So I would like to group by ID and instead of get the lowest (MIN) or highest (MAX) number, I would like to get the most common one (that would be 250, because there 3x).
Is there an easy way in SQL Server 2012 or am I forced to add a second SELECT where I COUNT(DISTINCT ID) and add that somehow to my first SELECT statement?
You can use dense_rank to return all the id's with the highest counts. This would handle cases when there are ties for the highest counts as well.
select id from
(select id, dense_rank() over(order by count(*) desc) as rnk from tablename group by id) t
where rnk = 1
A simple way to do what you want uses top and order by:
SELECT top 1 id
FROM t
GROUP BY id
ORDER BY COUNT(*) DESC;
This is a statistic called the mode. Getting the mode and max is a bit challenging in SQL Server. I would approach it as:
WITH cte AS (
SELECT t.id, COUNT(*) AS cnt,
row_number() OVER (ORDER BY COUNT(*) DESC) AS seqnum
FROM t
GROUP BY id
)
SELECT MAX(id) AS themax, MAX(CASE WHEN seqnum = 1 THEN id END) AS MODE
FROM cte;

Excluding only one MIN value on Oracle SQL

I am trying to select all but the lowest value in a column (GameScore), but when there are two of this lowest value, my code excludes both (I know why it does this, I just don't know exactly how to correct it and include one of the two lowest values).
The code looks something like this:
SELECT Id, SUM(Score) / COUNT(Score) AS Score
FROM
(SELECT Id, Score
FROM GameScore
WHERE Game_No = 1
AND Score NOT IN
(SELECT MIN(Score)
FROM GameScore
WHERE Game_No = 1
GROUP BY Id))
GROUP BY Id
So if I am drawing from 5 values, but one of the rows only pulls 3 scores because the bottom two are the same, how do I include the 4th? Thanks.
In order to do this you have to separate them up somehow; your current issue is that the 2 lowest scores are the same so any (in)equality operation performed on either values treats the other one identically.
You could use something like the analytic query ROW_NUMBER() to uniquely identify rows:
select id, sum(score) / count(score) as score
from ( select id, score, row_number() over (order by score) as score_rank
from gamescore
where gameno = 1
)
where score_rank <> 1
group by id
ROW_NUMBER():
assigns a unique number to each row to which it is applied (either each row in the partition or each row returned by the query), in the ordered sequence of rows specified in the order_by_clause, beginning with 1.
As the ORDER BY clause is on SCORE in ascending order one of the lowest score will be removed. This will be a random value unless you add other tie-breaker conditions to the ORDER BY.
You can do this a few ways, including what #Ben shows. From a mostly SQL Server background I was curious if just ROWNUM could be used and found this piece on ROWNUM vs ROW_NUMBER interesting. I'm not sure if it is dated.
All in a SQLFiddle.
Note: I'm using a subquery factoring/CTE as I think the read more clearly than in-line subqueries.
Using ROWNUM:
WITH OrderedScore AS (
SELECT id, game_no, score
,rownum as score_rank
FROM GameScore
WHERE game_no = 1
ORDER BY Score ASC
)
SELECT id
,sum(score)/count(score)
FROM OrderedScore
WHERE score_rank > 1
GROUP BY id;
Using ROW_NUMBER() OVER(ORDER BY...) as Ben does:
WITH OrderedScore AS (
SELECT id, game_no, score
,ROW_NUMBER() OVER(ORDER BY score ASC) as score_rank
FROM GameScore
WHERE game_no = 1
ORDER BY Score ASC
)
SELECT id
,sum(score)/count(score)
FROM OrderedScore
WHERE score_rank > 1
GROUP BY id;
Using ROW_NUMBER() OVER(PARTION BY...ORDER BY...) which I think leads to more flexibility if you want to remove the low score by game_no or id at some point:
WITH OrderedScore AS (
SELECT id, game_no, score
,ROW_NUMBER() OVER(PARTITION BY id ORDER BY score ASC) as score_rank
FROM GameScore
WHERE game_no = 1
ORDER BY Score ASC
)
SELECT id
,sum(score)/count(score)
FROM OrderedScore
WHERE score_rank > 1
GROUP BY id;

How to query for records based on a group and ordering?

Given the following models:
User
id
UserPulses
id, user_id, group_id, created_at
What I would like to do is obtain all of a user's UserPulses grouped by (group_id) and only obtain the most recent UserPulse per group_id. I've been able to do this by looping through group by group, but that takes a large number of queries. Is this possible with one query?
Something like:
user.user_pulses.group("group_id)")
Any ideas? Thanks
You can't do this reliably through the usual ActiveRecord interface but you can do it through SQL using a window function. You want some SQL like this:
select id, user_id, group_id, created_at
from (
select id, user_id, group_id, created_at,
row_number() over (partition by group_id order by created_at desc, id desc) as r
from user_pulses
where user_id = :user_id
) dt
where r = 1
and then wrap that in find_by_sql:
pulses = UserPulses.find_by_sql([%q{
select id, user_id, group_id, created_at
from (
select id, user_id, group_id, created_at,
row_number() over (partition by group_id order by created_at desc, id desc) as r
from user_pulses
where user_id = :user_id
) dt
where r = 1
}, :user_id => user.id])
The window function part essentially does a local GROUP BY with each group_id, sorts them (with id desc as the secondary sort key as a "just in case" tie breaker), and tacks the per-group row number in r. Then the outer query filters out the first in each group (where r = 1)and peels off the originaluser_pulses` columns.
You can use the PostgreSQL specific extension of the SQL feature DISTINCT: DISTINCT ON
SELECT DISTINCT ON (group_id)
id, user_id, group_id, created_at
FROM user_pulses
WHERE user_id = :user_id
ORDER BY group_id, created_at DESC, id; -- id just to break ties
Simpler than window functions (but not as portable) and probably fastest.
More details under this related question.
Something like this, perhaps. But there could be multiple records for a user/group_id combo if they share the same date.
SELECT p.id, p.user_id, p.group_id, p.created_at
FROM UserPulses p
,( SELECT user_id, group_id, MAX(created_at) as max_date
FROM UserPulses
GROUP BY user_id, group_id ) m
WHERE u.user_id = m.user_id
AND u.group_id = m.group_id
AND u.created_at = m.max_date