I am working on a results table at the minute, I am wanting to sort a table, by points (highest at the top), if points are equal I want to sort by goal difference, and then if goal difference is equal I want to sort by goals scored.
So a table may look like this,
+--------+--------+----------+-----------------+--------+
| Team | Scored | Conceded | Goal Difference | Points |
+--------+--------+----------+-----------------+--------+
| Team A | 20 | 10 | +10 | 15 |
| Team B | 20 | 15 | +5 | 15 |
| Team C | 10 | 10 | 0 | 9 |
| Team D | 5 | 5 | 0 | 9 |
+--------+--------+----------+-----------------+--------+
So Team A wins the league because it has a better goal difference than Team B, Team C finish above Team D because they score more goals, and all other things are equal.
Is it possible to order this way in mysql, or will I need to parse the results with PHP?
Guess what, you can pass multiple column names to ORDER BY
SELECT * FROM mytable ORDER BY Points DESC, `Goal Difference` DESC, Scored DESC
You haven't given your table structure, but as pointed out by jpg if these fields are not numeric, ORDER by field_name + 0 may be more appropriate
You can do like this using query
SELECT * from `your_table` ORDER BY points DESC, goal_difference DESC, scored DESC
You can order by multiple columns at the same time.
SELECT some_cols
FROM table
WHERE (some conditions)
ORDER BY col1 DESC, col2, col3;
Related
Looking around the site, most questions regarding to Ranks in a highscore table assumes that you will be looking at the entire or the top of the table.
In a lot of examples on this site, the rank is found by ordering the items by score and then counting the rows from the top of the set, or counting the items as they are retrived. Like this
score name rank
1000 test345 1
999 test980 2
950 test234 3
833 test291 4
760 test573 5
731 test981 6
In my situation, I need to look at only a portion of the scores, which may not be at the top of the table, for instance, maybe halfway though the leaderboard:
scores name rank
500 test451 43
433 test768 44
425 test120 45
where the user is only shown the scores around his. The part of the leader board the user is looking at above, isn't at the top of the leader board, so I can't count the rows in the returned scores to determine their rank.
How can I determine the rank of user in a leader board at and arbitrary position efficiently, amusing there a lot of entries.
Also this is my first foray into sql and php. I might not be using the correct terminology.
I'm not really sure what you are trying to do. You can limit you result using a LIMIT clause like this:
SELECT * FROM <table> LIMIT 0, 3
Which will only return the first 3 records.
To order the result based on the rank field you would use an ORDER BY clause:
SELECT * FROM <table> ORDER BY rank DESC LIMIT 0, 3
The above query will return 3 records order by rank in descending order.
If you like to calculate the rank based on the scores column this would work:
SELECT scores,
name,
FIND_IN_SET(scores, (SELECT GROUP_CONCAT(scores ORDER BY scores DESC)
FROM <table>)) as rank
FROM <table> ORDER BY rank DESC LIMIT 0, 3;
Running the above query against a table with only two columns scores and name:
+--------+---------+
| scores | name |
+--------+---------+
| 500 | test451 |
| 433 | test768 |
| 425 | test120 |
| 300 | test001 |
| 250 | test002 |
| 200 | test003 |
+--------+---------+
Would yield the following result:
+--------+---------+------+
| scores | name | rank |
+--------+---------+------+
| 500 | test451 | 1 |
| 433 | test768 | 2 |
| 425 | test120 | 3 |
+--------+---------+------+
The GROUP_CONCAT() maximum length is depending on the group_concat_max_len system variable, so for a large table this needs to be changed and I'm not sure this would be the best approach.
Notice that you could/should add indexes to your table to speed things up:
ALTER <table> ADD INDEX `idx_scores` (`scores`);
I have a table with scores like this:
score | user
-------------------
2 | Mark
4 | Alex
3 | John
2 | Elliot
10 | Joe
5 | Dude
The table is gigantic in reality and the real scores goes from 1 to 25.
I need this:
range | counts
-------------------
1-2 | 2
3-4 | 2
5-6 | 1
7-8 | 0
9-10 | 1
I've found some MySQL solutions but they seemed to be pretty complex some of them even suggested UNION but performance is very important. As mentioned, the table is huge.
So I thought why don't you simply have a query like this:
SELECT COUNT(*) as counts FROM score_table GROUP BY score
I get this:
score | counts
-------------------
1 | 0
2 | 2
3 | 1
4 | 1
5 | 1
6 | 0
7 | 0
8 | 0
9 | 0
10 | 1
And then with PHP, sum the count of scores of the specific ranges?
Is this even worse for performance or is there a simple solution that I am missing?
Or you could probaly even make a JavaScript solution...
Your solution:
SELECT score, COUNT(*) as counts
FROM score_table
GROUP BY score
ORDER BY score;
However, this will not returns values of 0 for count. Assuming you have examples for all scores, then the full list of scores is not an issue. You just won't get counts of zero.
You can do what you want with something like:
select (case when score between 1 and 2 then '1-2'
when score between 3 and 4 then '3-4'
. . .
end) as scorerange, count(*) as count
from score_table
group by scorerange
order by min(score);
There is no reason to do additional processing in php. This type of query is quite typical for SQL.
EDIT:
According to the MySQL documentation, you can use a column alias in the group by. Here is the exact quote:
An alias can be used in a query select list to give a column a
different name. You can use the alias in GROUP BY, ORDER BY, or HAVING
clauses to refer to the column:
SELECT
SUM(
CASE
WHEN score between 1 and 2
THEN ...
Honestly, I can't tell you if this is faster than passing "SELECT COUNT(*) as counts FROM score_table GROUP BY score" into PHP and letting PHP handle it...but it add a level of flexibility to your setup. Create a three column table as 'group_ID', 'score','range'. insert values into it to get your groupings right
1,1,1-2
1,2,1-2
1,3,3-4
1,4,3-4
etc...
Join to it on score, group by range. THe addition of the 'group_ID' allows you to set groups...maybe have group 1 break it into groups of two, and let a group_ID = 2 be a 5 set range (or whatever you might want).
I find the table use like this is decently fast, requires little code changing, and can readily be added to if you require additional groupings or if the groupings change (if you do the groupings in code, the entire case section needs to be redone to change the groupings slightly).
How about this:
select concat((score + (1 * (score mod 2)))-1,'-',(score + (1 * (score mod 2)))) as score, count(*) from TBL1 group by (score + (1 * (score mod 2)))
You can see it working in this fiddle: http://sqlfiddle.com/#!2/215839/6
For the input
score | user
-------------------
2 | Mark
4 | Alex
3 | John
2 | Elliot
10 | Joe
5 | Dude
It generates this:
range | counts
-------------------
1-2 | 2
3-4 | 2
5-6 | 1
9-10 | 1
If you want a simple solution which is very powerful, add an extra field within your table and put a value in it for the score so 1 and 2 have the value 1, 3 and 4 has 2. With that you can group by that value. Only by inserting the score you've to add an extra field. So your table looks like this:
score | user | range
--------------------------
2 | Mark | 1
4 | Alex | 2
3 | John | 2
2 | Elliot | 1
10 | Joe | 5
5 | Dude | 3
Now you can do:
select count(score),range from table group by range;
This is always faster if you've an application where selecting has prior.
By inserting do this:
$scoreRange = 2;
$range = ceil($score/$scoreRange);
I am trying to figure out how to rank based on 2 different column numbers in laravel but raw mysql will do. I have a list of videos, these videos are inside of competitions and are given votes if someone likes the video. Each video will have a vote number and a competition number. I am trying to rank based on votes within competition. So for example below I have competition 8, I need the rank of all the videos in that competition based on votes. I then need the same for competition 5 etc.
|rank|votes|competition|
------------------
| 1 | 100 | 8 |
------------------
| 2 | 50 | 8 |
------------------
| 3 | 30 | 5 |
------------------
| 1 | 900 | 5 |
------------------
| 2 | 35 | 5 |
------------------
I have tried various group and selectby methods but nothing seems to work, any ideas?
In Mysql you can use user-defined variables to calculate rank,case statement checks if competition is same as the previous row then increment rank by one if different then assign 1 an order by is needed to have correct rank for the video
SELECT t.*,
#current_rank:= CASE WHEN #current_rank = competition
THEN #video_rank:=#video_rank +1
ELSE #video_rank:=1 END video_rank,
#current_rank:=competition
FROM t ,
(SELECT #video_rank:=0,#current_rank:=0) r
ORDER BY competition desc, votes desc
See Demo
If you are confused with the last extra column you can use a subselect
See Demo
You can use windowing functions:
select
competition, votes, rank() over (partition by competition order by votes desc) as rank
from
table1
Before I explain my problem, I will quickly go over how the table structure is:
Type: MySQL
Posts/topic Table:
int int** UNIX Time Int Int
--------------------------------------------------
| id | category | postdate | topic_id | is_topic |
--------------------------------------------------
| 1 | a | 12345678 | 1 | 1 |
--------------------------------------------------
| 2 | a | 12345678 | 1 | 0 |
--------------------------------------------------
| 3 | b | 12345678 | 3 | 1 |
--------------------------------------------------
| 4 | b | 12345678 | 3 | 0 |
--------------------------------------------------
| 5 | c | 12345678 | 5 | 1 |
--------------------------------------------------
| 6 | c | 12345678 | 5 | 0 |
--------------------------------------------------
**I'm using letters to make is easier to read the table
I am trying to retrieve the 4 newest rows for each category, and I am able to get the 1 newest from each category with GROUP BY, but I have no idea how to get multiple for each category.
I have tried something like this:
SELECT *
FROM posts p
WHERE NOT EXISTS
(
SELECT *
FROM posts
WHERE category = p.category
LIMIT 4
)
I also tried working with some of the other answers people supplied for some other answers here on SO, but I did not seem to be able to make them fit my purpose.
I am completely lost here, as mysql is not a strong side of mine when it comes to these more complex queries.
Any help or pointers in the right directions would really be appreciated!
Thanks!
UPDATE: Please note that the number of categories is not be static, and will change.
Something like this would probably do :
select * from
(select
#rank:=CASE WHEN #ranked <> category THEN 1 ELSE #rank+1 END as rank,
id,
category,
postdate,
topic_id,
is_topic
#ranked:=category
from
(select #rank := -1) a,
(select #ranked :=- -1) b,
(select * from posts order by category, postdate desc) c
) ranked_posts
where ranked_posts.rank <= 4
Basically what is happening here is that I am trying to create a "ranking" function present in other engines (MS SQL comes in mind).
The query goes through all the posts, ordered by category, postdate and adds a "ranking" number to every row resetting the rank when the category changes. Like:
rank | category
1 a
2 a
...
100 a
1 b
2 b
1 c
2 c
3 c
You do that inside a "sub query" to mimic a table then just select the rows that have rank <= 4, (the ones you need). If you need more or less you can adjust that number.
One thing to keep in mind is that the ordering is important or the ranks will get all screwed up. The categories have to be "grouped", hence the ORDER BY category and then the groups ordered by your criteria postdate desc.
DETAILS
I've combined the following tables
testresults
--------------------------------------------------------------------
| index | uid| start | stop | score|
--------------------------------------------------------------------
| 1 | 23 | 2012-06-06 07:30:20 | 2012-06-06 07:30:34 | 100 |
--------------------------------------------------------------------
| 2 | 34 | 2012-06-06 07:30:21 | 2012-06-06 07:30:40 | 100 |
--------------------------------------------------------------------
usertable
------------------------------
| id | username |
------------------------------
| 23 | MacGyver’s mum |
------------------------------
| 34 | Gribblet |
------------------------------
using this sql
SELECT a.username, b.duration, b.score
FROM usertable AS a
JOIN (SELECT `uid`, `score`,
TIMESTAMPDIFF( SECOND, start, stop ) AS `duration`
FROM `testresults`
WHERE `start` >= DATE(NOW())
ORDER BY `score` DESC, `duration` ASC
LIMIT 100) AS b
ON a.id = b.uid
Problem is I want to rank the results. I think it is probably easier/faster to do it in sql as opposed to php, so based on http://code.openark.org/blog/mysql/sql-ranking-without-self-join this is what I tried
SELECT a.username, b.duration, b.score, COUNT(DISTINCT b.duration, b.score) AS rank
FROM usertable AS a
JOIN (SELECT `uid`, `score`,
TIMESTAMPDIFF( SECOND, start, stop ) AS `duration`
FROM `testresults`
WHERE `start` >= DATE(NOW())
ORDER BY `score` DESC, `duration` ASC
LIMIT 100) AS b
ON a.id = b.uid
but I don't get back the expected ranks. It only returns one row.
QUESTION
What am I doing wrong?
How can I increase rank only when duration and score are unique?
UPDATE1
Using bdenham's "slow method" worked for me, but the second method didn't. I don't really understand what is going on in the "fast method". I've posted the data I was using and the resulting table. You'll see that the ranking is messed up.
-------------------------------------------------------------------
| index | uid| start | stop | score|
--------------------------------------------------------------------
| 1 | 32 | 2012-08-27 05:47:18 | 2012-08-27 05:47:36 | 100 | 18s
| 2 | 32 | 2012-08-27 05:50:36 | 2012-08-27 05:50:42 | 0 | 6s
| 3 | 32 | 2012-08-27 05:51:18 | 2012-08-27 05:51:25 | 100 | 7s
| 4 | 32 | 2012-08-27 05:51:30 | 2012-08-27 05:51:35 | 0 | 5s
| 5 | 32 | 2012-08-27 05:51:39 | 2012-08-27 05:51:44 | 50 | 5s
--------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| username | score | duration | #prevScore:=#currScore | #prevDuration:=#currDuration | #currScore:=r.score | #currDuration:=timestampdiff(second,r.start,r.stop) |rank |
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| bob | 100 | 7 | [BLOB - 1B] | [BLOB - 1B] | 100 | 7 | 3 |
| bob | 100 | 18 | [BLOB - 0B] | [BLOB - 0B] | 100 | 18 | 1 |
| bob | 50 | 5 | [BLOB - 1B] | [BLOB - 1B] | 50 | 5 | 5 |
| bob | 0 | 5 | [BLOB - 3B] | [BLOB - 1B] | 0 | 5 | 4 |
| bob | 0 | 6 | [BLOB - 3B] | [BLOB - 2B] | 0 | 6 | 2 |
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Both methods from the link in your question work with MySQL 5.5.25. Here is the SQL Fiddle. But I am not able to adapt the methods to your slightly more complicated model. You have an additional join, plus your rank is based on two columns instead of just one.
Your attempt does not follow either method, though I suspect you were attempting to follow the slow "traditional" solution. As others have pointed out, that solution requires a self join and group by that you are completely lacking.
Here is my broken attempt at adapting the slow method to your model. The problem is MySQL
only preserves the username of the last row found for a given rank. Earlier rows with the same rank are discarded from the results. The query would not run on most databases because the GROUP BY does not include username. MySQL has non-standard rules for GROUP BY. I don't understand why your moderately complicated model doesn't work, but the simple linked model does work. I think it is a bad idea to have missing GROUP BY terms anyway.
select u.username,
r1.score,
timestampdiff(second,r1.start,r1.stop) duration,
count( distinct concat(r2.score,',',timestampdiff(second,r2.start,r2.stop)) ) rank
from testresults r1
join testresults r2
on r2.score>r1.score
or( r2.score=r1.score
and
timestampdiff(second,r2.start,r2.stop)<=timestampdiff(second,r1.start,r1.stop)
)
join usertable u
on u.id=r1.uid
where r1.start>=date(now())
and r2.start>=date(now())
group by r1.score, duration
order by score desc, duration asc limit 100
Here is a fix for the slow method. It first computes the rank for each unique score/duration pair, then joins that result with each test result. This works, but it is even slower than the original broken method.
select username,
r.score,
r.duration,
r.rank
from testresults tr
join usertable u
on u.id=tr.uid
join (
select r1.score,
timestampdiff(second,r1.start,r1.stop) duration,
count( distinct concat(r2.score,',',timestampdiff(second,r2.start,r2.stop)) ) rank
from testresults r1
join testresults r2
on r2.score>r1.score
or( r2.score=r1.score
and
timestampdiff(second,r2.start,r2.stop)<=timestampdiff(second,r1.start,r1.stop)
)
where r1.start>=date(now())
and r2.start>=date(now())
group by r1.score, duration
) r
on r.score=tr.score
and r.duration=timestampdiff(second,tr.start,tr.stop)
where tr.start>=date(now())
order by rank limit 100
Here is my broken attempt at adapting the fast method to your model. The method does not work because the selected variables are computed prior to the sort operation. Again I don't understand why the simple model in the link works but your model does not.
select u.username,
r.score,
timestampdiff(second,r.start,r.stop) duration,
#prevScore:=#currScore,
#prevDuration:=#currDuration,
#currScore:=r.score,
#currDuration:=timestampdiff(second,r.start,r.stop),
#rank:=if(#prevScore=#currScore and #prevDuration=#currDuration, #rank, #rank+1) rank
from testresults r
join usertable u
on u.id=r.uid
cross join (select #currScore:=null, #currDuration:=null, #prevScore:=null, #prevDuration:=null, #rank:=0) init
where r.start>=date(now())
order by score desc, duration asc limit 100
Here is a "fixed" version of the fast method. But it relies on the order of the sorted rows in the subquery. In general a query should never rely on the order of rows unless there is an explicit SORT operation. The outer query is not sorted, and even if it were, I don't know if the variables would be computed before or after the outer sort.
select username,
score,
duration,
#prevScore:=#currScore,
#prevDuration:=#currDuration,
#currScoure:=score,
#currDuration:=duration,
#rank:=if(#prevScore=score and #prevDuration=duration, #rank, #rank+1) rank
from (
select u.username,
r.score,
timestampdiff(second,r.start,r.stop) duration
from testresults r
join usertable u
on u.id=r.uid
where r.start>=date(now())
order by score desc, duration asc limit 100
) scores,
(
select #currScore:=null,
#currDuration:=null,
#rank:=0
) init
I think you will get just as good performance if you simply select the results without rank, ordered by score and duration. Your PHP can efficiently compute the rank since the results are already sorted. Your PHP can initialize rank to 0 and prev score and duration to null. Then compare each row with the previous values and increment the rank if there is a difference. The big advantage of letting PHP rank the sorted results is it should always work, regardless of the brand or version of the database engine. And it should still be fast.
Here is the SQL Fiddle showing all 4 queries. I modified the WHERE clauses so that the queries continue to work for any date.