SQL group & sort by two columns [duplicate] - php

This question already has an answer here:
Closed 10 years ago.
Possible Duplicate:
SQL ORDER BY total within GROUP BY
UPDATE: I've found my solution, which I've posted here. Thanks to everyone for your help!
I'm developing a Facebook application which requires a leaderboard. Scores and time taken to complete the game are recorded and these are organised by score first, then in the case of two identical scores, the time is used. If a user has played multiple times, their best score is used.
The lower the score, the better the performance in the game.
My table structure is:
id
facebook_id - (Unique Identifier for the user)
name
email
score
time - (time to complete game in seconds)
timestamp - (unix timestamp of entry)
date - (readable format of timestamp)
ip
The query I thought would work is:
SELECT *
FROM entries
ORDER BY score ASC, time ASC
GROUP BY facebook_id
The problem I'm having is in some cases it's pulling in the user's first score in the database, not their highest score. I think this is down to the GROUP BY statement. I would have thought the ORDER BY statement would have fixed this, but apparently not.
For example:
----------------------------------------------------------------------------
| ID | NAME | SCORE | TIME | TIMESTAMP | DATE | IP |
----------------------------------------------------------------------------
| 1 | Joe Bloggs | 65 | 300 | 1234567890 | XXX | XXX |
----------------------------------------------------------------------------
| 2 | Jane Doe | 72 | 280 | 1234567890 | XXX | XXX |
----------------------------------------------------------------------------
| 3 | Joe Bloggs | 55 | 285 | 1234567890 | XXX | XXX |
----------------------------------------------------------------------------
| 4 | Jane Doe | 78 | 320 | 1234567890 | XXX | XXX |
----------------------------------------------------------------------------
When I use the query above, I get the following result:
1. Joe Bloggs - 65 - 300 - (Joes First Entry, not his best entry)
2. Jane Doe - 72 - 280
I would have expected...
1. Joe Bloggs - 55 - 285 - (Joe's best entry)
2. Jane Doe - 72 - 280
It's like the Group By is ignoring the Order - and just overwriting the values.
Using MIN(score) with the group by selects the lowest score, which is correct - however it merges the time from the users first record in the database, so often returns incorrectly.
So, how can I select a user's highest score and the associated time, name, etc and order the results by score, then time?
Thanks in advance!

Your query does not actually make sense, because the order by should be after the group by. What SQL engine are you using? Most would give an error.
I think what you want is more like:
select e.facebookid, minscore, min(e.time) as mintime -- or do you want maxtime?
from entries e join
(select e.facebookid, min(score) as minscore
from entries e
group by facebookid
) esum
on e.facebookid = esum.facebookid and
e.score = e.minscore
group by e.facebookid, minscore
You can also do this with window functions, but that depends on your database.

One approach would be this:
SELECT entries.facebook_id, MIN(entries.score) AS score, MIN(entries.time) AS time
FROM entries
INNER JOIN (
SELECT facebook_id, MIN(score) AS score
FROM entries
GROUP BY facebook_id) highscores
ON highscores.facebook_id = entries.facebook_id
AND entries.score = highscores.score
GROUP BY entries.facebook_id
ORDER BY MIN(entries.score) ASC, MIN(entries.time) ASC
If you need more information from the entries table, you can then use this as a subquery, and join again on the information presented (facebook_id, score, time) to get one row per user.
You need to aggregate twice, is the crux of this; once to find the minimum score for the user, and again to find the minimum time for that user and score. You could reverse the order of the aggregation, but I would expect that this will filter most quickly and thus be most efficient.
You might also want to check which is faster, aggregating the second time: using the minimum score or grouping using the score as well.

You need to min the score
SELECT
facebook_id,
name,
email,
min(score) as high_score
FROM
entries
GROUP BY
facebook_id,
name,
email
ORDER BY
min(score) ASC

Thanks for your help. #Penguat had the closest answer.. Here was my final Query for anyone who might have the same issue...
SELECT f.facebook_id, f.name, f.score, f.time FROM
(SELECT facebook_id, name, min(score)
AS highscore FROM golf_entries
WHERE time > 0
GROUP BY facebook_id)
AS x
INNER JOIN golf_entries as f
ON f.facebook_id = x.facebook_id
AND f.score = x.highscore
ORDER BY score ASC, time ASC
Thanks again!

If you want their best time, you want to use the MIN() function - you said that the lower the score, the better they did.
SELECT facebook_id, MIN(score), time, name, ...
FROM entries
GROUP BY facebook_id, time, name, ...
ORDER BY score, time

Related

How to select all entries of a user from a database?

I am trying to make a "top purchaser" module on my store and I am a bit confused about the MySQL query.
I have a table with all transactions and I need to select the person (which could have one or many transactions) with the highest amount of money spent in the past month.
What I have:
name | money spent
------------------
john | 50
mike | 12
john | 10
jane | 504
carl | 99
jane | 12
jane | 1
What I want to see:
With a query, I need to see:
name | money spent last month
-----------------------------
jane | 517
carl | 99
john | 60
mike | 12
How do I do that?
I do not really seem to find many good solutions since my MySQL query skills are quite basic. I thought of making a table in which money is added to the user when he buys something.
That's a simple aggregated query :
SELECT t.name, SUM(t.moneyspent) money_spent_last_month
FROM mytable t
GROUP BY t.name
ORDER BY t.money_spent_last_month DESC
LIMIT 1
The query sums the total money sped by customer name. The results are ordered by descending total money spent, and only the first row is retained.
If you are looking to filter data over last month, you need a column in the table that keeps track of the transaction date, say transaction_date, and then you can just add a WHERE clause to the query, like :
SELECT t.name, SUM(t.moneyspent) money_spent_last_month
FROM mytable t
WHERE
t.transaction_date >=
DATE_ADD(LAST_DAY(DATE_SUB(NOW(), INTERVAL 2 MONTH)), INTERVAL 1 DAY)
AND t.transaction_date <=
DATE_SUB(NOW(), INTERVAL 1 MONTH)
GROUP BY t.name
ORDER BY t.money_spent_last_month DESC
LIMIT 1
This method is usually more efficient than using DATE_FORMAT to format dates as string and compare the results.

Select distinct by highest or lowest value

I'm working on a track and field ranking database in MySQL/PHP5 whereby I'm struggling to find the best way to query results per unique athlete by highest value.
just
SELECT distinct name, event
FROM results
sample database
name | event | result
--------------------------
athlete 1 | 40 | 7.43
athlete 2 | 40 | 7.66
athlete 1 | 40 | 7.33
athlete 1 | 60 | 9.99
athlete 2 | 60 | 10.55
so let's say that in this case I'd like to rank the athletes on the 40m dash event by best performance I tried
SELECT distinct name, event
FROM results
WHERE event = 40
ORDER by result DESC
but the distinct only leaves the first performance (7.43) of the athlete which isn't the best (7.33). Is there an easy way other than creating a temp table first whereby the results are ordered first and performing a select on the temp table afterwards?
You might be interested in group by:
SELECT name, min(result) as result
FROM results
WHERE event = 40
GROUP BY name
This gives you the best result per athlete.
As suggested by spencer, you can also order the list by appending this:
ORDER BY min(result) ASC
The problem is that the columns used in the ORDER BY aren't specified in the DISTINCT. To do this, you need to use an aggregate function to sort on, and use a GROUP BY to make the DISTINCT work.
SELECT distinct name, event
FROM results
WHERE event = 40
GROUP BY name
ORDER by result DESC

mysql rank by column value grouped by column value

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

Mysql Sorted items by user get an overall order

I have a list of films that users can rank in order of which they like best using jQuery UI Sortable (all works well). The lower the order number the better the film (1) and the higher (26) the worse it is. The list of films could be endless but is fixed in the database (users can't add more), so the user can only select from x list of films.
Films do not have to be in the users list, if they haven't seen film 5 then it won't get included (this may be compounding the problem).
Currently this is stored in the table:
film_id | user_id | order
4 2 3
5 3 3
6 2 1
7 2 2
7 3 1
8 3 2
What I want, and don't know where to start is an overall 'Top 10' style list. i.e. film 7 is the most popular because it appears higher up peoples lists and is in more lists. Film 6 could be the most popular but it's only in one list?!
I am stuck on both the logic and the Mysql queries to do it!
I am thinking I might need to weight the order somehow? Or have a separate table with the score per film and just update it after every edit. The following query seems like the right idea if it was just based on the count of items in the table but not when I want to add position in to the equation.
SELECT ff.film_id, COUNT(ff.film_id) AS cnt, SUM(ff.order) AS rank FROM
`favourite_film` AS ff GROUP BY ff.film_id ORDER BY cnt DESC, rank ASC
I guess I need the count of all the films in the table and the sum of the order (but reversed?), my theory then goes flat!
Any help or links would be greatly appreciated. Thanks.
Depending your "business rules", I think you should find some sort of calculation to both take into account the position and the number of "votes".
Just a random guess, but why not sorting by COUNT(votes)/AVG(pos) ? For maintainability reason, you might want to factor out the ranking function:
CREATE FUNCTION ranking(average_pos REAL, vote_count INT)
RETURNS REAL
DETERMINISTIC
RETURN vote_count/average_pos;
The query is now simply:
SELECT film_id,
AVG(pos) as a, COUNT(*) as c, ranking(AVG(pos),COUNT(*)) AS rank
FROM vote GROUP BY film_id
ORDER BY ranking(AVG(pos), COUNT(*)) DESC;
Producing with your example:
+----------+------+----+----------------+
| FILM_ID | A | C | RANK |
+----------+------+----+----------------+
| 7 | 1.5 | 2 | 1.333333333333 |
| 6 | 1 | 1 | 1 |
| 8 | 2 | 1 | 0.5 |
| 5 | 3 | 1 | 0.333333333333 |
| 4 | 3 | 1 | 0.333333333333 |
+----------+------+----+----------------+
See http://sqlfiddle.com/#!2/3b1d9/1
you should have reverted the list before saving it. this way you could leave the unselected movies out of the rating.
a workaround might be:
Count the amount of lists SELECT COUNT(DISTINCT(user_id) save this as $AMOUNT_OF_LISTS
now count the points using
SELECT film_id, (SUM(order)+($AMOUNT_OF_LISTS-COUNT(DISTINCT(user_id)))*POINTS_FOR_NOT_IN_LIST) as points FROM table GROUP BY film_id
logic: sum up all points and add POINTS_FOR_NOT_IN_LIST points for every time not in a list (total amount of lists - amount of times movie is in the list)
insert a value POINTS_FOR_NOT_IN_LIST to your liking. (might be 26 or 27 or even lower)
you probably want to add ORDER BY points DESC LIMIT 10 to the query to get 10 highest points
SELECT MIN( `order` ) , COUNT( * ) AS cnt, `film_id`
FROM `favourite_film`
GROUP BY `film_id`
ORDER BY cnt DESC , `order`
I would do this, I would assign a higher value to the movies with the higher ranking. Then I would sum the values per movie and order by the total descending to get the overall ranking. This way you are giving weight to both the popularity and rankings of each movie.
So if you wanted to do it by the top 3 ranked movies per user you could do this:
SELECT film_id, SUM(3 -- The max number of ranked movies per user
- order -- the ranking
+ 1) total_score
FROM TABLE_NAME
GROUP BY film_id
ORDER BY total_score DESC;
Obviously you could remove the comments
This way the top rated movie would get the higher score, the next highest, the next highest score, etc. If you were counting the top 10 movies per user, just change the 3 to 10.

MySQL: SELECT a Winner, returning their rank

Earlier I asked this question, which basically asked how to list 10 winners in a table with many winners, according to their points.
This was answered.
Now I'm looking to search for a given winner X in the table, and find out what position he is in, when the table is ordered by points.
For example, if this is the table:
Winners:
NAME:____|__POINTS:
Winner1 | 1241
Winner2 | 1199
Sally | 1000
Winner4 | 900
Winner5 | 889
Winner6 | 700
Winner7 | 667
Jacob | 623
Winner9 | 622
Winner10 | 605
Winner11 | 600
Winner12 | 586
Thomas | 455
Pamela | 434
Winner15 | 411
Winner16 | 410
These are possible inputs and outputs for what I want to do:
Query: "Sally", "Winner12", "Pamela", "Jacob"
Output: 3 12 14 623
How can I do this? Is it possible, using only a MySQL statement? Or do I need PHP as well?
This is the kind of thing I want:
WHEREIS FROM Winners WHERE Name='Sally' LIMIT 1
Ideas?
Edit - NOTE: You do not have to deal with the situation where two Winners have the same Points (assume for simplicity's sake that this does not happen).
I think this will get you the desired result. Note that i properly handles cases where the targeted winner is tied for points with another winner. (Both get the same postion).
SELECT COUNT(*) + 1 AS Position
FROM myTable
WHERE Points > (SELECT Points FROM myTable WHERE Winner = 'Sally')
Edit:
I'd like to "plug" Ignacio Vazquez-Abrams' answer which, in several ways, is better than the above.
For example, it allows listing all (or several) winners and their current position.
Another advantage is that it allows expressing a more complicated condition to indicate that a given player is ahead of another (see below). Reading incrediman's comment to the effect that there will not be "ties" prompted me to look into this; the query can be slightly modified as follow to handle the situation when players have same number of points (such players would formerly have been given the same Position value, now the position value is further tied to their relative Start values).
SELECT w1.name, (
SELECT COUNT(*)
FROM winners AS w2
WHERE (w2.points > w1.points)
OR (W2.points = W1.points AND W2.Start < W1.Start) -- Extra cond. to avoid ties.
)+1 AS rank
FROM winners AS w1
-- WHERE W1.name = 'Sally' -- optional where clause
SELECT w1.name, (
SELECT COUNT(*)
FROM winners AS w2
WHERE w2.points > w1.points
)+1 AS rank
FROM winners AS w1

Categories