Determine ranking with single mysql query - mysql

I am selecting a set of items from my table and determine their ranking to display this on my page, my code for selecting the items:
<?
$attra_query=mysqli_query($link, "select * from table WHERE category ='4'");
if(mysqli_num_rows($attra_query)>
0){
while($attra_data=mysqli_fetch_array($attra_query,1)){
?>
In the while loop I determine the ranking for each of those items like so:
SELECT COUNT(mi.location) + 1 rank
FROM table m
LEFT JOIN (
SELECT id,location,country, ROUND(COALESCE(total_rating/total_rating_amount,0),10) rating_per_vote
FROM table WHERE category = '4'
) mi
ON mi.location = m.location
AND mi.country = m.country
AND mi.rating_per_vote > ROUND(COALESCE(m.total_rating/m.total_rating_amount,0),10)
WHERE m.id = '$attra_id';
I figure this is highly inefficient, is there a way to combine the 2 queries into a single one so I don't have to run the ranking query for each item separately ?
//EDIT
Sample data:
id | location | country | category | total_rating | total_rating_amount
1 berlin DE 4 12 2
2 munich DE 4 9 1
Vote system is 1-10 points, for the sample data berlin has received a total rating of 12 with 2 votes, munich has received a rating of 9 with 1 vote, so berlin would have a rating of 6/10 and munich a rating of 9/10 and therefore should be ranked #1

SELECT COUNT(m.id) rank, m.id
FROM
(SELECT * FROM table WHERE category = '4') m
LEFT JOIN (
SELECT id,location,country, ROUND(COALESCE(total_rating/total_rating_amount,0),10) rating_per_vote
FROM table WHERE category = '4'
) mi
ON (mi.location = m.location
AND mi.country = m.country
AND mi.rating_per_vote > ROUND(COALESCE(m.total_rating/m.total_rating_amount,0),10))
OR mi.id=m.id
GROUP BY m.id
This should do I suppose. I don't know if this is the best possible solution.

In MySQL, you can do the ranking using variables. It is a bit hard to tell what you want to rank by from your query, but it would be something like this:
select t.*, (#rn := #rn + 1) as ranking
from table t cross join
(select #rn := 0) vars
where category = '4'
order by rating_per_vote;
If you provide sample data and desired results, it would be possible to refine this solution.

Related

Select MAX value with restriction to rows

I have 3 tables:
matchdays:
matchday_id | season_id | userid | points | matchday
----------------------------------------------------
1 | 1 | 1 | 33 | 1
2 | 1 | 2 | 45 | 1
etc
players
userid | username
-----------------
1 | user1
2 | user2
etc.
seasons
seasons_id | title | userid
----------------------------
1 | 2011 | 3
2 | 2012 | 10
3 | 2013 | 5
My query:
SELECT s.title, p.username, SUM(points) FROM matchdays m
INNER JOIN players p ON p.userid = m.userid
INNER JOIN seasons s ON m.userid = s.userid
group by s.season_id
This results in (example!):
title | username | SUM(points)
------------------------------
2011 | user3 | 3744
2012 | user10 | 3457
2013 | user5 | 3888
What it should look like is a table with the winner (max points) of every season. Right now, the title and username is correct, but the sum of the points is way too high. I couldn't figure out what sum is calculated. Ideally, the sum is the addition of every matchday of a season for every user.
Your main issue is that you group by seasons only. Thus your SUM is running on all points over a season, regardless of the player.
The whole approach is wrong anyway. The "flaw" with userid in the season table is your biggest issue, and you seem to know it.
I will explain you how to calculate your rankings in the database one time for all, and to have them at your disposal at all times, which will save you a lot of headaches, and obviously save some CPU and loading times as well.
Start by creating a new table "Rankings":
CREATE table rankings (season_id INT, userid INT, points INT, rank INT)
If you have a lot of players, index all columns but points
Then, populate the table for each season:
This is a oneshot operation to run each time a season has ended.
So for the time being, you will have to run it several times for each season.
The key here is to compute the rank of each player for the season, which is a must-have that will be super-handy for later. Because MySQL doesnt have a window function for that, we have to use an old trick : incrementing a counter.
I decompose.
This will compute the points of a season, and provide the ranking for that season:
SELECT season_id, userid, SUM(points) as points
FROM matchdays
WHERE season_id = 1
GROUP BY season_id, userid
ORDER BY points DESC
Now we adapt this query to add a rank column :
SELECT
season_id, userid, points,
#curRank := #curRank + 1 AS rank
FROM
(
SELECT season_id, userid, SUM(points) as points
FROM matchdays
WHERE season_id = 1
GROUP BY season_id, userid
) T,
(
SELECT #curRank := 0
) R
ORDER BY T.points DESC
That's it.
Now we can INSERT the results of this computation into our ranking table, to store it once for good :
INSERT INTO rankings
SELECT
season_id, userid, points,
#curRank := #curRank + 1 AS rank
FROM
(
SELECT season_id, userid, SUM(points) as points
FROM matchdays
WHERE season_id = 1
GROUP BY season_id, userid
) T,
(
SELECT #curRank := 0
) R
ORDER BY T.points DESC
Change the season_id = 1 and repeat for each season.
Save this query somewhere, and in the future, run it once each time a season has ended.
Now you have a proper database-computed ranking and a nice ranking table that you can query whenever you want.
You want the winner for each season ? As simple as that:
SELECT S.title, P.username, R.points
FROM Ranking R
INNER JOIN seasons S ON R.season_id=S.season_id
INNER JOIN players P ON R.userid=P.userid
WHERE R.rank = 1
You will discover over the time that you can do a lot of different things very simply with your ranking table.
You're join is wrong, try something like:
SELECT s.title, p.username, SUM(m.points) as points FROM matchdays m
JOIN players p ON p.userid = m.userid
JOIN seasons s ON m.season_id = s.season_id
group by s.season_id, p.userid
ORDER by points DESC;
As pointed out, userid does'nt belong/is not needed in 'seasons' table.

Nested queries and Join

As a beginner with SQL, I’m ok to do simple tasks but I’m struggling right now with multiple nested queries.
My problem is that I have 3 tables like this:
a Case table:
id nd date username
--------------------------------------------
1 596 2016-02-09 16:50:03 UserA
2 967 2015-10-09 21:12:23 UserB
3 967 2015-10-09 22:35:40 UserA
4 967 2015-10-09 23:50:31 UserB
5 580 2017-02-09 10:19:43 UserA
a Value table:
case_id labelValue_id Value Type
-------------------------------------------------
1 3633 2731858342 X
1 124 ["864","862"] X
1 8981 -2.103 X
1 27 443 X
... ... ... ...
2 7890 232478 X
2 765 0.2334 X
... ... ... ...
and a Label table:
id label
----------------------
3633 Value of W
124 Value of X
8981 Value of Y
27 Value of Z
Obviously, I want to join these tables. So I can do something like this:
SELECT *
from Case, Value, Label
where Case.id= Value.case_id
and Label.id = Value.labelValue_id
but I get pretty much everything whereas I would like to be more specific.
What I want is to do some filtering on the Case table and then use the resulting id's to join the two other tables. I'd like to:
Filter the Case.nd's such that if there is serveral instances of the same nd, take the oldest one,
Limit the number of nd's in the query. For example, I want to be able to join the tables for just 2, 3, 4 etc... different nd.
Use this query to make a join on the Value and Label table.
For example, the output of the queries 1 and 2 would be:
id nd date username
--------------------------------------------
1 596 2016-02-09 16:50:03 UserA
2 967 2015-10-09 21:12:23 UserB
if I ask for 2 different nd. The nd 967 appears several times but we take the oldest one.
In fact, I think I found out how to do all these things but I can't/don't know how to merge them.
To select the oldest nd, I can do someting like:
select min((date)), nd,id
from Case
group by nd
Then, to limit the number of nd in the output, I found this (based on this and that) :
select *,
#num := if(#type <> t.nd, #num + 1, 1) as row_number,
#type := t.nd as dummy
from(
select min((date)), nd,id
from Case
group by nd
) as t
group by t.nd
having row_number <= 2 -- number of output
It works but I feel it's getting slow.
Finally, when I try to make a join with this subquery and with the two other tables, the processing keeps going on for ever.
During my research, I could find answers for every part of the problem but I can't merge them. Also, for the "counting" problem, where I want to limit the number of nd, I feel it's kind of far-fetch.
I realize this is a long question but I think I miss something and I wanted to give details as much as possible.
to filter the case table to eliminate all but oldest nds,
select * from [case] c
where date = (Select min(date) from case
where nd = c.nd)
then just join this to the other tables:
select * from [case] c
join value v on v.Case_id = c.Id
join label l on l.Id = v.labelValue_id
where date = (Select min(date) from [case]
where nd = c.nd)
to limit it to a certain number of records, there is a mysql specific command, I think it called Limit
select * from [case] c
join value v on v.Case_id = c.Id
join label l on l.Id = v.labelValue_id
where date = (Select min(date) from [case]
where nd = c.nd)
Limit 4 -- <=== will limit return result set to 4 rows
if you only want records for the top N values of nd, then the Limit goes on a subquery restricting what values of nd to retrieve:
select * from [case] c
join value v on v.Case_id = c.Id
join label l on l.Id = v.labelValue_id
where date = (Select min(date) from [case]
where nd = c.nd)
and nd In (select distinct nd from [case]
order by nd desc Limit N)
So finally, here is what worked well for me:
select *
from (
select *
from Case
join (
select nd as T_ND, date as T_date
from Case
where nd in (select distinct nd from Case)
group by T_ND Limit 5 -- <========= Limit of nd's
) as t
on Case.nd = t.T_ND
where date = (select min(date)
from Case
where nd = t.T_ND)
) as subquery
join Value
on Value.context_id = subquery.id
join Label
on Label.id = Value.labelValue_id
Thank you #charlesbretana for leading me on the right track :).

Complex SQL Union

Any SQL Guru's out there I could use some help! I am creating a stored procedure that I believe needs a Union so that all the results are brought back with 1 SELECT statement.
I have simplified my problem to the tables below:
user
user_id username name DOB
------------------------------------------------------
1 JohnSmith1 John Smith 01/01/1990
2 LisaGreen17 Lisa Green 03/07/1986
3 BarneyB Barney Brown 09/12/1960
user_team
user_team_id user_id team_id total_score
-------------------------------------------------------------
1 1 1 29
2 2 7 37
3 3 2 15
private_league
priv_league_id league_name host_user league_password
-------------------------------------------------------------------------
1 Lisa's League 2 CSUASH429d9
2 Barney's Bonanza 3 Jkap89f5I01
user_team_private_league_M2M
id priv_league_id user_team_id
----------------------------------------
1 1 1
2 1 2
3 1 3
4 2 1
5 2 3
I would like to run a stored procedure with an input of a user_id which will bring back all leagues entered by the user, the host of each of those leagues, how many total players have entered in each league and what position the user is in for each of those leagues(sorted by total score).
At the moment I have:
CREATE DEFINER=`root`#`localhost` PROCEDURE `user_private_leagues`(IN v_user_id INT)
BEGIN
DECLARE userteamid INT;
# Retrieve user team from a user_id
SELECT user_team_id INTO userteamid
FROM user_team
WHERE user_id = v_user_id;
# Retrieve private league name and host user (for a userteam)
SELECT private_league.league_name, private_league.host_user
FROM user_team_private_league_M2M
INNER JOIN privateleague
ON user_team_private_league_M2M.priv_league_id=private_league.priv_league_id
WHERE user_team_id = userteamid;
END
This query does not include the total number of players for each league and the current position of the user
I have created a query to bring back the total users for each private league, with no user filter like so:
SELECT private_league_id, COUNT(*) AS total_users
FROM classicseasonmodel_classicseasonuserteamprivateleague
GROUP BY private_league_id;
A query for the user's current position can be worked out by using the answer to this question and using total_score.
I am extremely stuck with this at the moment - the perfect result from the SP will be as follows:
CALL user_private_leagues(3); (user id of BarneyB)
priv_league_name current_position total_users host_user
-----------------------------------------------------------------------
Lisa's League 3 3 LisaGreen17
Barney's Bonanza 2 2 BarneyB
Thanks!
Sorry but I didn't create the DB to test the SQL below. But you can start from there. No need for UNION. I didn't understand the business rule to compute the user's position in the league, since it may come from the team or the user.
select priv_league_id, league_name, host_user_name, count(*) as total_users
from (
select A.priv_league_id, A.league_name, D.name as host_user_name, B.user_team_id, C.user_id, D.
from private_league A
join user_team_private_league_M2M B
on A.priv_league_id = B. priv_league_id
join user_team C
on B.user_team_id = C. user_team_id
join user D
on A.host_user = D.user_id
) D
group by priv_league_id
Let take it step by step....
First ID, Count for pleague
SELECT private_league_id, COUNT(*) AS total_users
FROM classicseasonmodel_classicseasonuserteamprivateleague
GROUP BY private_league_id;
Now add in Name and host user
SELECT PL.league_name, LC.uCNT, PL.host_user
FROM (SELECT private_league_id AS pID, COUNT(*) AS uCNT
FROM classicseasonmodel_classicseasonuserteamprivateleague
GROUP BY private_league_id ) AS LC
LEFT JOIN private_league PL ON PL.priv_league_id = LC.pID
Now add in host user name
SELECT PL.league_name, LC.uCNT as total_users, hu.name as host_user
FROM (SELECT private_league_id AS pID, COUNT(*) AS uCNT
FROM classicseasonmodel_classicseasonuserteamprivateleague
GROUP BY private_league_id ) AS LC
LEFT JOIN private_league PL ON PL.priv_league_id = LC.pID
LEFT JOIN user hu ON PL.host_user = hu.user_id
Don't know where current position is.
This query will give users and position for each team, join to this and limit by user id to get one users position for each team:
select UT.user_id,
UT.team_id,
ROW_NUMBER() OVER (PARTITION BY team_id ORDER BY total_score DESC) AS team_position
from private_league L
join user_team_private_league_M2M LJ ON L.priv_league_id = LJ.priv_league_id
join user_team UT ON LJ.user_team_id = UT.user_team_id

making an unique column value after order by mysql

So, my problem is that I have a list of customers (table now has around 100k records) with income per each customer. When I group it by country I get around 60 countries with sum of income. Than I need to order it by the income DESC, my query looks something like this:
SELECT s2.i,s1.year,s1.short_c,s1.country,s1.uges FROM
(SELECT u.year,k.short_c,s.country, IFNULL(ROUND(SUM(u.income)),0) as uges
FROM im_income u,im_contact k,td_countries s
WHERE u.year=2012
AND u.customer_id=k.id
AND k.kat='K'
AND k.short_c=s.short_c
GROUP BY k.short_c, u.year
ORDER BY u.year ASC,uges DESC) s1
CROSS JOIN
(SELECT #i:=#i+1 as i FROM (SELECT #i:= 0) AS i) s2
And I know that this with CROSS JOIN is wrong since it is not giving me what I need, but is there a way to make an unique id after ORDER BY since I need to order countries with income DESC and than assing them id that would represent a rank number???
Result looks like this now:
+-+----+-------+---------+------+
|i|year|short_c|country |uges |
+-+----+-------+---------+------+
|1|2012|USA |United S.|123456|
+-+----+-------+---------+------+
|1|2012|RU |Russia |23456 |
+-+----+-------+---------+------+
And I would want it in this way, but to assign after order by the unique i value:
+-+----+-------+---------+------+
|i|year|short_c|country |uges |
+-+----+-------+---------+------+
|1|2012|USA |United S.|123456|
+-+----+-------+---------+------+
|2|2012|RU |Russia |23456 |
+-+----+-------+---------+------+
|3| | | | |
+-+----+-------+---------+------+
Any help would be appreciated.
I think this is what you are looking for:
SELECT #i := #i + 1 as i, s1.year, s1.short_c, s1.country, s1.uges
FROM (SELECT u.year,
k.short_c,
s.country,
IFNULL(ROUND(SUM(u.income)),0) as uges
FROM im_income u join
im_contact k
on u.customer_id = k.id join
td_countries s
on k.short_c = s.short_c
WHERE u.year = 2012 AND k.kat = 'K'
GROUP BY k.short_c, u.year
) s1
CROSS JOIN
(SELECT #i:= 0) const
ORDER BY year, uges desc;
The variable evaluation occurs when the results are being "output", after the order by.
I also fixed your join syntax. You should learn to use the explicit join rather than implicit joins in the where clause.

Identifying groups in Group By

I am running a complicated group by statement and I get all my results in their respective groups. But I want to create a custom column with their "group id". Essentially all the items that are grouped together would share an ID.
This is what I get:
partID | Description
-------+---------+--
11000 | "Oven"
12000 | "Oven"
13000 | "Stove"
13020 | "Stove"
12012 | "Grill"
This is what I want:
partID | Description | GroupID
-------+-------------+----------
11000 | "Oven" | 1
12000 | "Oven" | 1
13000 | "Stove" | 2
13020 | "Stove" | 2
12012 | "Grill" | 3
"GroupID" does not exist as data in any of the tables, it would be a custom generated column (alias) that would be associated to that group's key,id,index, whatever it would be called.
How would I go about doing this?
I think this is the query that returns the five rows:
select partId, Description
from part p;
Here is one way (using standard SQL) to get the groups:
select partId, Description,
(select count(distinct Description)
from part p2
where p2.Description <= p.Description
) as GroupId
from part p;
This is using a correlated subquery. The subquery is finding all the description values less than the current one -- and counting the distinct values. Note that this gives a different set of values from the ones in the OP. These will be alphabetically assigned rather than assigned by first encounter in the data. If that is important, the OP should add that into the question. Based on the question, the particular ordering did not seem important.
Here's one way to get it:
SELECT p.partID,p.Description,b.groupID
FROM (
SELECT Description,#rn := #rn + 1 AS groupID
FROM (
SELECT distinct description
FROM part,(SELECT #rn:= 0) c
) a
) b
INNER JOIN part p ON p.description = b.description;
sqlfiddle demo
This gets assigns a diferent groupID to each description, and then joins the original table by that description.
Based on your comments in response to Gordon's answer, I think what you need is a derived table to generate your groupids, like so:
select
t1.description,
#cntr := #cntr + 1 as GroupID
FROM
(select distinct table1.description from table1) t1
cross join
(select #cntr:=0) t2
which will give you:
DESCRIPTION GROUPID
Oven 1
Stove 2
Grill 3
Then you can use that in your original query, joining on description:
select
t1.partid,
t1.description,
t2.GroupID
from
table1 t1
inner join
(
select
t1.description,
#cntr := #cntr + 1 as GroupID
FROM
(select distinct table1.description from table1) t1
cross join
(select #cntr:=0) t2
) t2
on t1.description = t2.description
SQL Fiddle
SELECT partID , Description, #s:=#s+1 GroupID
FROM part, (SELECT #s:= 0) AS s
GROUP BY Description