I have a users table with phase1 and phase2 columns that i need to calculate the users rank in each phase and store it in these fields.
the ranking is calculated based on a different table points where i have the points by phase for each user.
what i am trying to do is
sum all points for each user by phase and calculate his rank based on that
in case the user points are equal compare the sum of grade1 in case that is also equal compare the sum of grade2
update users table with his rank in each phase
here is how my new table look like with some demo data
sql fiddle demo
currently I use the below code to calculate the ranking from my old table where both rank and user info are in the same table
old sql fiddle demo
update users a
join (
select id,
(
select count(distinct total)
from users d
where c.total < d.total
) +1 rank
from users c
) b on a.id = b.id
set a.rank = b.rank
there are analytics function in oracle called as rank() and dense_rank() which can be useful to get your result.
As you are using mysql, I tried to convert those function in mysql equivalent.
You can get the desired result with following query which you can use to update users table. You may have to change it further if for the logic when there is tie on grades as well.
set #pk1 ='';
set #rn1 =1;
set #tot ='';
set #val =1;
SELECT id,
name,
phase,
phasetotal,
denseRank
FROM
(
SELECT id,
name,
phase,
phasetotal,
#rn1 := if(#pk1=phase, #rn1+#val,1) as denseRank,
#val := if(#pk1=phase, if(#tot=phasetotal, #val+1, 1),1) as value,
#pk1 := phase,
#tot := phasetotal
FROM
(
select users.id,users.name, points.phase, sum(points.points)
as phasetotal from users,points where users.id = points.userid
group by users.id, points.phase order by points.phase, phasetotal desc, points.grade1 desc, points.grade2 desc
) A
) B;
Here's the update query
set #pk1 ='';
set #rn1 =1;
set #tot ='';
set #val =1;
UPDATE users u join (
SELECT id,
name,
phase,
phasetotal,
denseRank
FROM
(
SELECT id,
name,
phase,
phasetotal,
#rn1 := if(#pk1=phase, #rn1+#val,1) as denseRank,
#val := if(#pk1=phase, if(#tot=phasetotal, #val+1, 1),1) as value,
#pk1 := phase,
#tot := phasetotal
FROM
(
select users.id,users.name, points.phase, sum(points.points)
as phasetotal from users,points where users.id = points.userid
group by users.id, points.phase order by points.phase, phasetotal desc, points.grade1 desc, points.grade2 desc
) A
) B ) C on u.id = C.id
SET u.phase1 = CASE WHEN C.phase = 1 and u.phase1 = 0 THEN C.denseRank ELSE u.phase1 END,
u.phase2 = CASE WHEN C.phase = 2 and u.phase2 = 0 THEN C.denseRank ELSE u.phase2 END;
Related
I am using a modified version of a query similiar to another question here:Convert SQL Server query to MySQL
Select *
from
(
SELECT tbl.*, #counter := #counter +1 counter
FROM (select #counter:=0) initvar, tbl
Where client_id = 55
ORDER BY ordcolumn
) X
where counter >= (80/100 * #counter);
ORDER BY ordcolumn
tbl.* contains the field 'client_id' and I am attempting to get the top 20% of the records for each client_id in a single statement. Right now if I feed it a single client_id in the where statement it gives me the correct results, however if I feed it multiple client_id's it simply takes the top 20% of the combined recordset instead of doing each client_id individually.
I'm aware of how to do this in most databases, but the logic in MySQL is eluding me. I get the feeling it involves some ranking and partitioning.
Sample data is pretty straight forward.
Client_id rate
1 1
1 2
1 3
(etc to rate = 100)
2 1
2 2
2 3
(etc to rate = 100)
Actual values aren't that clean, but it works.
As an added bonus...there is also a date field associated to these records and 1 to 100 exists for this client for multiple dates. I need to grab the top 20% of records for each client_id, year(date),month(date)
You need to do the enumeration for each client:
SELECT *
FROM (SELECT tbl.*, #counter := #counter +1 counter
(#rn := if(#c = client_id, #rn + 1,
if(#c := client_id, 1, 1)
)
)
FROM (select #c := -1, #rn := 0) initvar CROSS JOIN tbl
ORDER BY client_id, ordcolumn
) t cross join
(SELECT client_id, COUNT(*) as cnt
FROM tbl
GROUP BY client_id
) tt
where rn >= (80/100 * tt.cnt);
ORDER BY ordcolumn;
Using Gordon's answer as a starting point, I think this might be closer to what you need.
SELECT t.*
, (#counter := #counter+1) AS overallRow
, (#clientRow := if(#prevClient = t.client_id, #clientRow + 1,
if(#prevClient := t.client_id, 1, 1) -- This just updates #prevClient without creating an extra field, though it makes it a little harder to read
)
) AS clientRow
-- Alteratively (for everything done in clientRow)
, #clientRow := if(#prevClient = t.client_id, #clientRow + 1, 1) AS clientRow
, #prevClient := t.client_id AS extraField
-- This may be more reliable as well; I not sure if the order
-- of evaluation of IF(,,) is reliable enough to guarantee
-- no side effects in the non-"alternatively" clientRow calculation.
FROM tbl AS t
INNER JOIN (
SELECT client_id, COUNT(*) AS c
FROM tbl
GROUP BY client_id
) AS cc ON tbl.client_id = cc.client_id
INNER JOIN (select #prevClient := -1, #clientRow := 0) AS initvar ON 1 = 1
WHERE t.client_id = 55
HAVING clientRow * 5 < cc.c -- You can use a HAVING without a GROUP BY in MySQL
-- (note that clientRow is derived, so you cannot use it in the `WHERE`)
ORDER BY t.client_id, t.ordcolumn
;
I have a table that stores messages sent to users, the layout is as follows
id (auto-incrementing) | message_id | user_id | datetime_sent
I'm trying to find the first N message_id's that each user has received, but am completely stuck. I can do it easily on a per-user basis (when defining the user ID in the query), but not for all users.
Things to note:
Many users can get the same message_id
Message ID's aren't sent sequentially (i.e. we can send message 400 before message 200)
This is a read only mySQL database
EDIT: On second thought I removed this bit but have added it back in since someone was kind enough to work on it
The end goal is to see what % of users opened one of the first N messages they received.
That table of opens looks like this:
user_id | message_id | datetime_opened
This is an untested answer to the original question (with 2 tables and condition on first 5):
SELECT DISTINCT user_id
FROM (
SELECT om.user_id,
om.message_id,
count(DISTINCT sm2.message_id) messages_before
FROM opened_messages om
INNER JOIN sent_messages sm
ON om.user_id = sm.user_id
AND om.message_id = sm.message_id
LEFT JOIN sent_messages sm2
ON om.user_id = sm2.user_id
AND sm2.datetime_sent < sm.datetime_sent
GROUP BY om.user_id,
om.message_id
HAVING messages_before < 5
) AS base
The subquery joins in sm2 to count the number of preceding messages that were sent to the same user, and then the having clause makes sure that there are fewer than 5 earlier messages sent. As for the same user there might be multiple messages (up to 5) with that condition, the outer query only lists the unique users that comply to the condition.
To get the first N (here 2) messages, try
SELECT
user_id
, message_id
FROM (
SELECT
user_id
, message_id
, id
, (CASE WHEN #user_id != user_id THEN #rank := 1 ELSE #rank := #rank + 1 END) AS rank,
(CASE WHEN #user_id != user_id THEN #user_id := user_id ELSE #user_id END) AS _
FROM (SELECT * FROM MessageSent ORDER BY user_id, id) T
JOIN (SELECT #cnt := 0) c
JOIN (SELECT #user_id := 0) u
) R
WHERE rank < 3
ORDER BY user_id, id
;
which uses a RANK substitute, derived from #Seaux response to Does mysql have the equivalent of Oracle's “analytic functions”?
To extend this to your original question, just add the appropriate calculation:
SELECT
COUNT(DISTINCT MO.user_id) * 100 /
(SELECT COUNT(DISTINCT user_id)
FROM (
SELECT
user_id
, message_id
, id
, (CASE WHEN #user_id != user_id THEN #rank := 1 ELSE #rank := #rank + 1 END) AS rank,
(CASE WHEN #user_id != user_id THEN #user_id := user_id ELSE #user_id END) AS _
FROM (SELECT * FROM MessageSent ORDER BY user_id, id) T
JOIN (SELECT #cnt := 0) c
JOIN (SELECT #user_id := 0) u
) R2
WHERE rank < 3
) AS percentage_who_read_one_of_the_first_messages
FROM MessageOpened MO
JOIN
(SELECT
user_id
, message_id
FROM (
SELECT
user_id
, message_id
, id
, (CASE WHEN #user_id != user_id THEN #rank := 1 ELSE #rank := #rank + 1 END) AS rank,
(CASE WHEN #user_id != user_id THEN #user_id := user_id ELSE #user_id END) AS _
FROM (SELECT * FROM MessageSent ORDER BY user_id, id) T
JOIN (SELECT #cnt := 0) c
JOIN (SELECT #user_id := 0) u
) R
WHERE rank < 3) MR
ON MO.user_id = MR.user_id
AND MO.message_id = MR.message_id
;
With no CTEs in MySQL, and being in a read-only database - I see no way around having the above query twice in the statement.
See it in action: SQL Fiddle.
Please comment if and as this requires adjustment / further detail.
I am trying to generate row number for each row selected from my database but it seems that the row number follows the sequence of the table before it's arranged (order by).
Actual table
https://www.dropbox.com/s/otstzak20yxcgt6/test1.PNG?dl=0
After query
https://www.dropbox.com/s/i9jaoy04vq6u2zh/test2.PNG?dl=0
Code
SET #row_num = 0;
SELECT #row_num := #row_num + 1 as Position, Student.Stud_ID, Student.Stud_Name, Student.Stud_Class, SUM(Grade.Percentage) AS Points
FROM Student, Student_Subject, Grade
WHERE Student.Stud_ID = Student_Subject.Stud_ID
AND Student_Subject.Stud_Subj_ID = Grade.Stud_Subj_ID
AND Student.Stud_Form = '1'
AND Grade.Quarter = '1'
GROUP BY Student.Stud_ID
ORDER BY Points DESC
Pls help. Looking forward to receiving replies from yall. Thanks!
Try an inner select, so the row number will be generated after the ORDER BY like so:
SET #row_num = 0;
SELECT #row_num := #row_num + 1 as Position, s.*
FROM
(
SELECT
Student.Stud_ID, Student.Stud_Name, Student.Stud_Class, SUM(Grade.Percentage) AS Points
FROM Student, Student_Subject, Grade
WHERE Student.Stud_ID = Student_Subject.Stud_ID
AND Student_Subject.Stud_Subj_ID = Grade.Stud_Subj_ID
AND Student.Stud_Form = '1'
AND Grade.Quarter = '1'
GROUP BY Student.Stud_ID
ORDER BY Points DESC
) AS s;
I have a quite complex query to get some data from the database, sort them and rank them accordingly.
Here is the SQL fiddle for it: SQL Fiddle
Now what I want to do is, to add a WHERE statement to this query, so only limited users will be selected (3 users above and 3 users below, the id = 8).
WHERE sort BETWEEN #userpos - 3 AND #userpos + 3
So it should look something like this, but with the first example:
SQL Fiddle
I have already tried to implement this WHERE statement to this query, but I couldn't figure it out where should I add, as I've always received error (that the column cannot be found).
Any suggestion and / or solution for my problem? Should I rewrite the whole query for this?
If I understand correctly, you can do this with a subquery:
SET #userid = 8
SELECT *
FROM (SELECT #pos := #pos + 1 AS sort, points, r.userid, s.active
FROM rank r JOIN
settings s
USING (userid) CROSS JOIN
(SELECT #pos := 0) p
WHERE s.active = 1
ORDER BY points DESC
) list
WHERE userid = #userid;
Note that this eliminates a layer of subqueries that you have. Otherwise, it is quite similar to your query.
EDIT:
The above was based more on the SQL Fiddle than on the question. (Oops.)
To get three rows before and after a given row is possible and just a small tweak, using a trick. The trick is to define another variable with the user pos and then use that variable in the outer query:
SELECT *
FROM (SELECT #pos := #pos + 1 AS sort, points, r.userid, s.active,
if(userid = #userid, #userpos := #pos, 0)
FROM rank r JOIN
settings s
USING (userid) CROSS JOIN
(SELECT #pos := 0, #userpos := 0) p
WHERE s.active = 1
ORDER BY points DESC
) list
WHERE `sort` between #userpos - 3 and #userpos + 3;
Note: MySQL does not guarantee the order of evaluation for variables in the select. The following is a bit safer in terms of order of execution:
SELECT *
FROM (SELECT (case when (#pos := #pos + 1) is NULL then NULL
when (case when (userid = #userid) then #userpos := #pos else 1 end) is null
then NULL
else #pos
end) AS sort, points, r.userid, s.active,
if(userid = #userid, #userpos := #pos)
FROM rank r JOIN
settings s
USING (userid) CROSS JOIN
(SELECT #pos := 0, #userpos := 0) p
WHERE s.active = 1
ORDER BY points DESC
) list
WHERE `sort` between #userpos - 3 and #userpos + 3;
The weird case statements are to ensure statement executions. The is null is to ensure that the when clauses fail, so the assignments are made sequentially.
I have a list of players. The players are sorted by points. What I'd like to know is how do I get the ranking number of a CERTAIN player?
This is my code so far (which doesn't work because it seems to have some bugs):
$rank = mysql_query (SET #rank := 0;
SELECT *, #rank := #rank + 1
FROM ava_users
WHERE id = '".$id."'
ORDER BY points DESC);
$rank_res = mysql_fetch_array($rank);
When I try to use my query I get an error message:
mysql_fetch_array() expects parameter 1 to be resource, boolean given in /Users/***/Documents/Arcades/Arc_development/arc_projects/***/arc_dev_website/arc_offline/includes/profile/profile_main.inc.php
$rank = mysql_query (
"SELECT a.*,
(
SELECT COUNT(1)
FROM ava_users b
WHERE (b.points, b.id) >= (a.points, a.id)
) AS rank
FROM ava_users a
WHERE a.`user` = '$id'"
);
Try this:
SELECT `user`, rank
FROM (
SELECT `user`, ( #rank := #rank + 1 ) as rank
FROM ava_users, ( select (#rank := 0 ) ) rnk
ORDER BY points DESC
) ranks
WHERE `user` = '".$id."'
user is a key word, therefore use user in order to check parameters equality.
Also, mysql_query can only execute 1 query at a time.