Get user rank with MySQL - mysql

I read many topics about this problem but I can't find the solution.
I have a table (called users) with the users of my website. They have points. For example:
+-----------+------------+
| User_id | Points |
+-----------+------------+
| 1 | 12258 |
| 2 | 112 |
| 3 | 9678 |
| 4 | 689206 |
| 5 | 1868 |
+-----------+------------+
On the top of the page the variable $user_id is set. For example the user_id is 4. Now I would like to get the rank of the user by points (output should be 1 if the user_id is 4).
Thank you very much!

SELECT
COUNT(*) AS rank
FROM users
WHERE Points>=(SELECT Points FROM users WHERE User_id=4)
Updated with some more useful stuff:
SELECT
user_id,
points,
(SELECT COUNT(*)+1 FROM users WHERE Points>x.points) AS rank_upper,
(SELECT COUNT(*) FROM users WHERE Points>=x.points) AS rank_lower
FROM
`users` x
WHERE x.user_id = 4
which includes the range of ranks user is at. So for example if the scores for first five places are 5 4 3 3 3, the result would be:
id points rank_upper rank_lower
id 5 1 1
id 4 2 2
id 3 3 5
id 3 3 5
id 3 3 5

This query should do what you want :
SELECT rank FROM (
SELECT User_id, Points,
FIND_IN_SET(
Points,
(SELECT GROUP_CONCAT(
DISTINCT Points
ORDER BY Points DESC
)
FROM users)
) as rank
FROM users )
WHERE User_id = 4;

If you don't want to do it outside mysql you'll need to use variables to compute the rank.
Here's a solution that describes exactly what you want :
http://www.fromdual.ch/ranking-mysql-results
You still need, it you want to have it directly for each record, to store it in the record and to update it yourself. There is no reasonable query that will give you directly the rank without storage on a real table (I mean not just a few hundreds records).

There's already a simple solution, just suited for your purpose.

This may help
SELECT #rank:=#rank+1 AS rank,`User_id`,`Points` FROM `users` u JOIN (SELECT #rank:=0) r ORDER BY u.Points DESC

Related

Return preferred record when there is more than one record for the same user

I have a table where it stores the types of discounts that a user can have.
Some users will get the standard discount, but some will get a bigger and better discount. For users who have the biggest and best discount, there will be two records in the database, one for the default discount and the other for the biggest and best discount. The biggest and best discount will be preferred in the search.
I would like to do a SELECT that would return the record with the highest discount and if you don't find it, return it with the standard discount for me to avoid making two queries in the database or having to filter in the source code.
Ex:
| id | user_id | country | discount | cashback | free_trial |
|-----------------------------------------------------------------------|
| 1 | 1 | EUA | DEFAULT | 10 | false |
| 2 | 1 | EUA | CHRISTMAS | 20 | true |
| 3 | 3 | EUA | DEFAULT | 10 | false |
SELECT *
FROM users
WHERE country = 'EUA'
AND (discount = 'CHRISTMAS' OR discount = 'DEFAULT');
In this example above for user 1 it would return the record with the discount equal to "CHRISTMAS" and for user 3 it would return "DEFAULT" because it is the only one that has. Can you help me please?
You can use the row_number() window function to do this. This function includes a PARTITION BY that lets you start the numbering over with each user, as well as it's own ORDER BY that lets you determine which rows will sort first within each user/partition.
Then you nest this inside another SELECT to limit to rows where the row_number() result is 1 (the discount that sorted best):
SELECT *
FROM (
SELECT *, row_number() OVER (PARTITION BY id, ORDER BY cashback desc) rn
FROM users
WHERE country = 'EUA'
) u
WHERE rn = 1
You could also use a LATERAL JOIN, which is usually better than the correlated join in the other answer, but not as good as the window function.
You can using GROUP BY to do it
SELECT u1.*
FROM users u1
JOIN
(
SELECT COUNT(id) AS cnt,user_id
FROM users WHERE country = 'EUA'
GROUP BY user_id
) u2 ON u1.user_id=u2.user_id
WHERE IF(u2.cnt=1,u1.discount='DEFAULT',u1.discount='CHRISTMAS')
DB Fiddle Demo

SQL Order results by Match Against Relevance and display the price based on sellers rank

Looking to display results based on 'relevance' of the users search along with the price of the seller that ranks highest. A live example to what i'm after is Amazons search results, now I understand their algorithm is extremely complicated, but i'm after a simplified version.
Lets say we search for 'Jumper' the results that are returned are products related to 'Jumper' but then the price is not always the cheapest is based on the sellers rank. The seller with the highest rank gets his/hers prices displayed.
Heres what I have been working on but not giving me the expected results at mentioned above, and to be honest I don't think this is very efficient.
SELECT a.catalogue_id, a.productTitle, a.prod_rank, b.catalogue_id, b.display_price, b.sellers_rank
FROM
(
SELECT c.catalogue_id,
c.productTitle,
MATCH(c.productTitle) AGAINST ('+jumper*' IN BOOLEAN MODE) AS prod_rank
FROM catalogue AS c
WHERE c.catalogue_id IN (1, 2, 3)
) a
JOIN
(
SELECT inventory.catalogue_id,
inventory.amount AS display_price,
(accounts.comsn + inventory.quantity - inventory.amount) AS sellers_rank
FROM inventory
JOIN accounts ON inventory.account_id = accounts.account_id
WHERE inventory.catalogue_id IN (1, 2, 3)
) AS b
ON a.catalogue_id = b.catalogue_id
ORDER BY a.prod_rank DESC
LIMIT 100;
Sample Tables:
Accounts:
----------------------------
account_id | comsn
----------------------------
1 | 100
2 | 9999
Catalogue:
----------------------------
catalogue_id | productTitle
----------------------------
1 | blue jumper
2 | red jumper
3 | green jumper
Inventory:
-----------------------------------------------
product_id | catalogue_id | account_id | quantity | amount |
-----------------------------------------------
1 | 2 | 1 | 6 | 699
2 | 2 | 2 | 2 | 2999
Expected Results:
Product Title:
red jumper
Amount:
29.99 (because he/she has sellers rank of: 7002)
First, you should limit the results only to the matches for the first subquery:
Second, you should eliminate the second subquery:
SELECT p.catalogue_id, p.productTitle, p.prod_rank,
i.amount as display_price,
(a.comsn + i.quantity - i.amount)
FROM (SELECT c.catalogue_id, c.productTitle,
MATCH(c.productTitle) AGAINST ('+jumper*' IN BOOLEAN MODE) AS prod_rank
FROM catalogue AS c
WHERE c.catalogue_id IN (1, 2, 3)
HAVING prod_rank > 0
) p JOIN
inventory i
ON i.catalogue_id = c.catalogue_id join
accounts a
ON i.account_id = a.account_id
ORDER BY c.prod_rank DESC
LIMIT 100;
I'm not sure if you can get rid of the final ORDER BY. MATCH with JOIN can be a bit tricky in that respect. But only ordering by the matches should help.

Sorting rows in MySql based on algebra on two or more columns

I am designing a Test-System for a school.The table is like this
------------------------------------------------------------------------------
Student_id | Total Questions | Questions Attempted | Correct
------------------------------------------------------------------------------
36 | 60 | 42 | 20
19 | 60 | 38 | 32
119 | 60 | 37 | 31
Now, marking scheme is +3 for correct and -1 for wrong
Number of wrong questions will be calculated as wrong = attempted - correct
Questions
1) I want to give the give the student some points based on their ranks, so I want to sort the table on the decreasing order of their score i.e. score = 3 * correct - wrong.Though,I could have stored the score as well but since it is redundant data I don't want to store it into the table.How can I sort the table using SQL query.
2)When I will be updating the points of students based on their performance into the table student,I am picking student_id from result table and making updations into the student table i.e. 1 query per student.This means that if 4000 students sat for the test ,4000 queries !!! .Can I improve the situation (minimise queries)?
EDIT
Student schema for question 2
------------------------------------------------------------------------------
Student_id | fname | lname | contact | points
------------------------------------------------------------------------------
you can just specify what you want in the sort
select student_id,correct, attempted,field4,field5, (3 * correct - (attempted-correct)) as score
from students sort by score desc
yes, take a look at bulk update of sql, you can prepare the query and update 10 by 10 or 100 by 100, but not too much since sql command have limit on its length
Question 1.
Supposing the table is named Results, and that Student_id is unique, here is a possible solution to your question:
SELECT Results.*, (3*Correct-(Total_Questions-Correct)) AS score
FROM Results
ORDER BY score DESC
Question 2.
Supposing the Students are already added to the table Students, or that they already have a score, this is a possible SQL Query to update the students table without making the 4k queries:
UPDATE StudentsTable AS s
INNER JOIN PointsTable AS p
ON s.Student_id = p.Student_id
SET
s.Points = s.Points + (3 * p.Correct - (p.Questions_Attempted - p.Correct))
If you need to perform more tests in the future you can add a Test_ID column to you Points Table and then add a WHERE clause to the UPDATE query in order to just add up the score from a given test.
Optimization
You can optimize the queries a little bit by changing the way you calculate the score:
SELECT Results.*, (2*Correct-Total_Questions) AS score
FROM Results
ORDER BY score DESC
UPDATE StudentsTable AS s
INNER JOIN PointsTable AS p
ON s.Student_id = p.Student_id
SET
s.Points = s.Points + (2 * p.Correct - p.Questions_Attempted)
To rank students by score you can do
SELECT student_id,
(
SELECT 1 + COUNT(*)
FROM student_results
WHERE 3 * correct - (total - correct) >=
3 * r.correct - (r.total - r.correct)
AND student_id <> r.student_id
) rank
FROM student_results r
Output:
| STUDENT_ID | RANK |
|------------|------|
| 36 | 3 |
| 19 | 1 |
| 119 | 2 |
Now you can update student points in one go using multi-table UPDATE syntax instead of hitting the database with number of update queries.
UPDATE students s JOIN
(
SELECT student_id,
(
SELECT 1 + COUNT(*)
FROM student_results
WHERE 3 * correct - (total - correct) >=
3 * r.correct - (r.total - r.correct)
AND student_id <> r.student_id
) rank
FROM student_results r
) q
ON s.student_id = q.student_id
SET s.points = s.points +
CASE q.rank -- implement your logic of translating ranks to points here
WHEN 1 THEN 100
WHEN 2 THEN 50
WHEN 3 THEN 10
ELSE 0
END;
Here is SQLFiddle demo

MySQL How can I add values of a column together and remove the duplicate rows?

Good day,
I have a MySQL table which has some duplicate rows that have to be removed while adding a value from one column in the duplicated rows to the original.
The problem was caused when another column had the wrong values and that is now fixed but it left the balances split among different rows which have to be added together. The newer rows that were added must then be removed.
In this example, the userid column determines if they are duplicates (or triplicates). userid 6 is duplicated and userid 3 is triplicated.
As an example for userid 3 it has to add up all balances from rows 3, 11 and 13 and has to put that total into row 3 and then remove rows 11 and 13. The balance columns of both of those have to be added together into the original, lower ID row and the newer, higher ID rows must be removed.
ID | balance | userid
---------------------
1 | 10 | 1
2 | 15 | 2
3 | 300 | 3
4 | 80 | 4
5 | 0 | 5
6 | 65 | 6
7 | 178 | 7
8 | 201 | 8
9 | 92 | 9
10 | 0 | 10
11 | 140 | 3
12 | 46 | 6
13 | 30 | 3
I hope that is clear enough and that I have provided enough info. Thanks =)
Two steps.
1. Update:
UPDATE
tableX AS t
JOIN
( SELECT userid
, MIN(id) AS min_id
, SUM(balance) AS sum_balance
FROM tableX
GROUP BY userid
) AS c
ON t.userid = c.userid
SET
t.balance = CASE WHEN t.id = c.min_id
THEN c.sum_balance
ELSE 0
END ;
2. Remove the extra rows:
DELETE t
FROM
tableX AS t
JOIN
( SELECT userid
, MIN(id) AS min_id
FROM tableX
GROUP BY userid
) AS c
ON t.userid = c.userid
AND t.id > c.min_id
WHERE
t.balance = 0 ;
Once you have this solved, it would be good to add a UNIQUE constraint on userid as it seems you want to be storing the balance for each user here. That will avoid any duplicates in the future. You could also remove the (useless?) id column.
SELECT SUM(balance)
FROM your_table
GROUP BY userid
Should work, but the comment saying fix the table is really the best approach.
You can create a table with the same structure and transfer the data to it with this query
insert into newPriceTable(id, userid, balance)
select u.id, p.userid, sum(balance) as summation
from price p
join (
select userid, min(id) as id from price group by userid
) u ON p.userid = u.userid
group by p.userid
Play around the query here: http://sqlfiddle.com/#!2/4bb58/2
Work is mainly done in MSSQL but you should be able to convert the syntax.
Using a GROUP BY UserID you can SUM() the Balance, join that back to your main table to update the balance across all the duplicates. Finally you can use RANK() to order your duplicate Userids and preserve only the earliest values.
I'd select all this into a new table and if it looks good, deprecate your old table and rename then new one.
http://sqlfiddle.com/#!3/068ee/2

MySQL join query or sub query

I'm trying to do a query that selects mike if it isn't in the three highest bids for a keyword. Rows 4 and 7 should be selected.
So in final, if mike isn't in the three highest bids for a keyword, then select.
How do I solve this? With a sub query?
$construct = "SELECT child.* FROM `temp-advertise` child
LEFT JOIN `temp-advertise` parent on child.keyword=parent.keyword
WHERE child.name='mike'
ORDER BY child.id DESC";
id | name| keyword | bid |
1 | mike| one | 7 |
2 | tom | one | 4 |
3 | ced | one | 6 |
4 | mike| two | 1 |
5 | tom | two | 5 |
6 | har | two | 5 |
7 | mike| one | 3 |
8 | har | two | 3 |
SELECT *
FROM `temp-advertise` ta
WHERE ta.keyword = 'one'
AND ta.name = 'mike'
AND ta.bid <
(
SELECT bid
FROM `temp-advertise` tai
WHERE tai.keyword = 'one'
ORDER BY
bid DESC
LIMIT 2, 1
)
Your structure doesn't look too promising, nor your sample data. However, that said, you want to know if "Mike" was in the top 3 per keyword... and that he has 3 bids.... 2 for "one", 1 for "two". From the raw data, it looks like Mike is in 1st place and 4th place for the "one" keyword, and 4th place for "two" keyword.
This should get you what you need with SOME respect to not doing a full query of all keywords. The first innermost query is to just get keywords bid on by "mike" (hence alias "JustMike"). Then join that to the temp-advertise on ONLY THOSE keywords.
Next, by using MySQL variables, we can keep track of the rank PER KEYWORD. The trick is the ORDER BY clause needs to return them in the order that represents proper ranking. In this case, each keyword first, then within each keyword, ordered by highest bid first.
By querying the records, then using the #variables, we increase the counter, start at 1 every time the keyword changes, then preserve the keyword into the #grpKeyword variable for comparison of the next record. Once ALL bids are processed for the respective keywords, it then queries THAT result but ONLY for those bid on by "mike". These records will have whatever his rank position was.
select RankPerKeyword.*
from
( SELECT ta.*,
#grpCnt := if( #grpKeyword = ta.Keyword, #grpCnt +1, 1 ) as KWRank,
#grpKeyword := ta.Keyword as carryForward
FROM
( select distinct ta1.keyword
from `temp-advertise` ta1
where ta1.name = "mike" ) as JustMike
JOIN `temp-advertise` ta
on JustMike.Keyword = ta.Keyword,
( select #grpCnt := 0,
#grpKeyword := '' ) SqlVars
ORDER BY
ta.Keyword,
ta.Bid DESC" ) RankPerKeyword
where
RankPerKeyword.name = "mike"
(Run above to just preview the results... should show 3 records)
So, if you want to know if it was WITHIN the top 3 for a keyword you could just change to
select RankPerKeyword.keyword, MIN( RankPerKeyword.KWRank ) as BestRank
from (rest of query)
group by RankPerKeyword.Keyword
Try this:
Select ID, name, keyword from temp-advertise e
where 3 <= (select count(name) from temp-advertise
where e.keyword = keyword and bid > e.bid)
Try
SELECT .. ORDER BY bid LIMIT 3,999