Update a MySQL table with record rankings within groups - mysql

I have a table called 'winners' with columns 'id', 'category', 'score', 'rank'.
I need to update my table and asign a rank within the subcategories (category_id) which as per the sample is 2 but could be more than that in the future.
Most anwers I've found are based around select statements which simply tends to just output the table view but I did find a very good 'Update' answer (https://stackoverflow.com/a/2727239/4560380) specifically the answer update where ties are required to share the same rank.
Sample
CREATE TABLE winners (
id int,
category_id int,
score double,
rank int
);
INSERT INTO winners VALUES
(1, 1, 4.36, NULL),
(2, 1, 2.35, NULL),
(3, 1, 1.25, NULL),
(4, 2, 4.21, NULL),
(5, 2, 3.33, NULL),
(6, 1, 4.24, NULL),
(7, 1, 1.22, NULL),
(8, 1, 1.25, NULL),
(9, 2, 4.21, NULL),
(10, 2, 3.63, NULL);
+----------+-------------+-------+------+
| id | category_id | score | rank |
+----------+-------------+-------+------+
| 1 | 1 | 4.36 | |
| 2 | 1 | 2.35 | |
| 3 | 1 | 1.25 | |
| 4 | 2 | 4.21 | |
| 5 | 2 | 3.33 | |
| 6 | 1 | 4.24 | |
| 7 | 1 | 1.22 | |
| 8 | 1 | 1.25 | |
| 9 | 2 | 4.21 | |
| 10 | 2 | 3.63 | |
+----------+-------------+-------+------+
The linked answer above works perfectly for the data when there is only one category to worry about but not when there are multiple categories or subgroups to rank within.
I had attempted to add in a where clause to the code (line 8)
1. UPDATE winners
2. JOIN (SELECT w.score,
3. IF(#lastPoint <> w.score,
4. #curRank := #curRank + 1,
5. #curRank) AS rank,
6. #lastPoint := w.rating
7. FROM winners w
8. WHERE category_id = 1
9. JOIN (SELECT #curRank := 0, #lastPoint := 0) r
10. ORDER BY w.score DESC
11. ) ranks ON (ranks.score = winners.score)
12. SET winners.rank = ranks.rank;
with the intention of attempting to run the code twice for each category_id but the script fails.
Any options on modifying the answer above for multiple categories would be fantastic.
Needed result just to clarify (ranked within categories).
+----------+-------------+-------+------+
| id | category_id | score | rank |
+----------+-------------+-------+------+
| 1 | 1 | 4.36 | 1 |
| 6 | 1 | 4.24 | 2 |
| 2 | 1 | 2.35 | 3 |
| 8 | 1 | 1.25 | 4 |
| 3 | 1 | 1.25 | 4 |
| 7 | 1 | 1.22 | 5 |
| 4 | 2 | 4.21 | 1 |
| 9 | 2 | 4.21 | 1 |
| 10 | 2 | 3.63 | 2 |
| 5 | 2 | 3.33 | 3 |
+----------+-------------+-------+------+
Thanks Guys!
UPDATE
Managed to find another bit of code https://stackoverflow.com/a/13270603/4560380 that I had somehow originally missed and was able to modifiy it with the where clause for each category_id succesfully. Not an ideal way of doing it - running multiple times for multiple categories but at this point in time it is fine.
set #currentRank = 0,
#lastRating = null,
#rowNumber = 1;
update winners r
inner join (
select
score,
#currentRank := if(#lastRating = score, #currentRank, #rowNumber) rank,
#rowNumber := #rowNumber + if(#lastRating = score, 0, 1) rowNumber,
#lastRating := score
from winners
where category_id = 1
order by score desc
) var on var.score = r.score
set r.rank = var.rank
further answers for a more 'automatic' handling of multiple categories within the ranking in 1 script run are still very welcome and appreciated.
Thanks
UPDATE
Just noticed that the answer that I found doesn't deal with zero scores (0.00) very well and places them ranked in the middle of other scores.
shawnt00 answer below is working and evaluates zero scores correctly.
https://stackoverflow.com/a/34667112/4560380

update winners
set rank = (
select count(score) + 1
from winners w2
where w2.category_id = winners.category_id and w2.score > winners.score
)
count(*) will evaluate to zero even when no rows match the condition, i.e., the top ranking. If you wanted a dense rank you could do this:
update winners
set rank = (
select count(distinct score) + 1
from winners w2
where w2.category_id = winners.category_id and w2.score > winners.score
)
EDIT: Per your comment I've found a query that does work. (It works on SQL Server and I'm not familiar with MySQL's quirks.) http://sqlfiddle.com/#!9/1159f/1
update winners
inner join (
select w.id, w.category_id, count(w2.score) + 1 rank
from winners w left outer join winners w2
on w2.category_id = w.category_id and w2.score > w.score
group by w.id
) r
on r.id = winners.id
set winners.rank = r.rank

Related

how to group two specific items together

I have a database with questions with columns Question, Answer, Type.
Currently, this is the sql statement I am running:
SELECT Question, Answer, Type FROM goodquestions ORDER BY RAND() LIMIT 0,20
As you can see, I select random values from the table and I would like it to be that way. However, when type is 12 I would like to access the table row prior to that entry and print them out in conjunction
Like this
RANDOM
RANDOM
RANDOM
Question before 12 type
12 type question
RANDOM
RANDOM
RANDOM
It can also be like this:
Question before 12 type
12 type question
RANDOM
RANDOM
RANDOM
RANDOM
RANDOM
RANDOM
I just need them to be together and I am unable to do this right now.
I guess I see what you want.
Please try this query, I changed limit 1,20 to limit 1, 10:
select #next_line_id:=0, #next_line_type:=0;
select g.*, tt.is_property
from
(select * from
(select *,
rand() as rand_val,
case
when #next_line_type= 12 or Type = 12 then 1
else 0
end is_property,
#next_line_id as next_line_id,
#next_line_id:=id as current_id,
#next_line_type:=Type as current_type
from goodquestions order by id desc
) t
where t.Type <> 12
order by rand_val limit 0,10) tt
join goodquestions g on g.id = tt.id or (g.id = tt.next_line_id and tt.is_property = 1 and tt.Type <> 12)
group by g.id, g.Question, g.Answer, g.Type, tt.is_property
order by is_property desc, id
limit 0, 10;
The following are the query of creating test table:
create table goodquestions (
id int unsigned auto_increment primary key,
Question varchar(255) not null,
Answer varchar(255) not null,
Type int unsigned,
index idx_type (Type)
) engine=innodb DEFAULT CHARSET=latin1;
insert into goodquestions (Question, Answer, Type)
values ('q1', 'a1', 1),
('q2', 'a2', 2),
('q3', 'a3', 3),
('q4', 'a4', 4),
('q5', 'a5', 5),
('q6', 'a6', 6),
('q7', 'a7', 7),
('q8', 'a8', 8),
('q9', 'a9', 9),
('q10', 'a10', 10),
('q11', 'a11', 11),
('q12', 'a12', 12),
('q13', 'a13', 13),
('q14', 'a14', 14),
('q15', 'a15', 15),
('q16', 'a16', 16),
('q17', 'a17', 17),
('q18', 'a18', 18);
Please note, using rand() function may have a bad performance for a large
table. If there are performance issue, I could provide another solution for better performance.
The following query which result list must have and only have one record of type 12:
select #total_type_12:=(select count(*) from goodquestions where Type=12);
select #random_type_12:=(floor(rand()*#total_type_12) + 1) * 2;
select #next_line_id:=0, #next_line_type:=0, #is_property:=0;
select g.*, tt.is_property
from
(select * from
(select *,
case
when (#next_line_type= 12 or Type = 12) and #random_type_12 > 0 and #random_type_12 <= 2 then #is_property:=1
else #is_property:=0
end is_property,
rand() as rand_val,
#random_type_12 as cur_random_type_counter,
case
when (#next_line_type= 12 or Type = 12) and #random_type_12 > 0 then #random_type_12:=#random_type_12-1
else #random_type_12
end as next_rand_type_counter,
#next_line_id as next_line_id,
#next_line_id:=id as current_id,
#next_line_type:=Type as current_type
from goodquestions order by id desc
) t
where t.Type <> 12
order by is_property desc, rand_val limit 0,10) tt
join goodquestions g on g.id = tt.id or (g.id = tt.next_line_id and tt.is_property = 1 and tt.Type <> 12)
group by g.id, g.Question, g.Answer, g.Type, tt.is_property
order by is_property desc, id
limit 0, 10;
Test data set is as following:
mysql> select * from goodquestions;
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id: 84
Current database: test
+----+----------+--------+------+
| id | Question | Answer | Type |
+----+----------+--------+------+
| 1 | q1 | a1 | 1 |
| 2 | q2 | a2 | 2 |
| 3 | q3 | a3 | 3 |
| 4 | q4 | a4 | 4 |
| 5 | q5 | a5 | 5 |
| 6 | q6 | a6 | 6 |
| 7 | q7 | a7 | 7 |
| 8 | q8 | a8 | 8 |
| 9 | q9 | a9 | 9 |
| 10 | q10 | a10 | 10 |
| 11 | q11 | a11 | 11 |
| 12 | q12 | a12 | 12 |
| 13 | q13 | a13 | 13 |
| 14 | q14 | a14 | 14 |
| 15 | q15 | a15 | 15 |
| 16 | q16 | a16 | 16 |
| 17 | q17 | a17 | 17 |
| 18 | q18 | a18 | 18 |
| 19 | q21 | a21 | 12 |
| 20 | q22 | a22 | 22 |
| 21 | q23 | a23 | 12 |
+----+----------+--------+------+
21 rows in set (0.34 sec)
For MySQL 8+ it can be something similar to
WITH
-- SELECT 20 random rows
cte AS ( SELECT Question, Answer, Type
FROM goodquestions
ORDER BY RAND() LIMIT 0,20 )
( SELECT Question, Answer, Type
FROM cte )
-- add pre-row if Type=12 row is selected and pre-row is not selected
UNION DISTINCT
( SELECT Question, Answer, Type
FROM goodquestions
WHERE Type = 'pre-type for type 12'
AND EXISTS ( SELECT NULL
FROM cte
WHERE Type = 12 ) )
-- sort placing pre-row and type=12 row at the top
ORDER BY Type = 'pre-type for type 12' DESC,
Type = 12 DESC,
RAND()
-- remove excess row if Type=12 row was selected in CTE
-- and pre-row was not selected in CTE but added in UNION
LIMIT 0, 20
The query assumes that goodquestions.Type is unique.

MySQL get rows starting with specific id after sort / order by

I got this table:
id | score
1 | 1
2 | 4
3 | 4
4 | 3
5 | 2
6 | 2
7 | 1
8 | 4
9 | 2
10 | 3
I need to order it by score desc:
id | score
2 | 4
3 | 4
8 | 4
4 | 3
10 | 3
5 | 2
6 | 2
9 | 2
1 | 1
7 | 1
and get first 3 rows which starts with id 6
So the result should be:
6 | 2
9 | 2
1 | 1
Is this possible? Thanks in advance
I would approach this with a cumulative sum() (available in MySQL 8.0):
select
id,
score
from mytable
order by
sum(id = 6) over(order by score desc, id) desc,
score desc,
id
limit 3
The sum() orders record in the required direction; as soon as the record that has id = 6 is met, the sum takes value 1. It allows to put these records on top. The rest is just adding the additional sorting criteria and limiting the number of results.
Demo on DB Fiddle:
| id | score |
| --- | ----- |
| 6 | 2 |
| 9 | 2 |
| 1 | 1 |
In earlier versions of mysql, you can emulate the window sum with a user variable, as follows:
select
id,
score
from
(select #sm := 0) s
cross join (select id, score from mytable order by score desc, id) t
order by
case when id = 6 then #sm := #sm + 1 end desc,
score desc,
id
limit 3
Demo on DB Fiddle: same results
With this:
select t.*
from tablename t cross join (select * from tablename where id = 6) c
where t.score < c.score or (t.score = c.score and t.id >= c.id)
order by t.score desc, t.id
limit 3
See the demo.
Results:
| id | score |
| --- | ----- |
| 6 | 2 |
| 9 | 2 |
| 1 | 1 |
With this table
CREATE TABLE table3
(`id` int, `score` int)
;
INSERT INTO table3
(`id`, `score`)
VALUES
(1, 1),
(2, 4),
(3, 4),
(4, 3),
(5, 2),
(6, 2),
(7, 1),
(8, 4),
(9, 2),
(10, 3)
;
And this select
SELECT `id`, `score`
FROM (SELECT `id`,`score`,if (id = 8,#scoreid:= #scoreid +1,#scoreid) scoreid
From table3, (SELECT #scoreid :=0) s Order by score desc) t1
Where scoreid > 0 LIMIT 3;
you get
id score
8 4
4 3
10 3
DBFiddle example https://dbfiddle.uk/?rdbms=mysql_5.7&fiddle=95e2051d560c2ac27fdcc8f9d04acf5d

mysql random selection in inner join

Question Mysql Random Row Query on Inner Join is much the same as mine but it was never answered.
I have a master table m and slave s. S contains 1 to many rows for each m. I would like a query that selects every master row joined to exactly one randomly chosen slave.
If the table schemas were:
M
---
id
S
---
id
mid
then, in pseudo code the query would be:
select * from m inner join s on m.id = s.mid where s.id is one randomly chosen from the values that exist
Can this be translated into real SQL?
I think the following query does the required job but using a subquery (not inner join):
SELECT *, (SELECT id FROM S WHERE S.mid = M.id ORDER BY RAND() LIMIT 1) AS S_id
FROM M
Here is a link to test it.
Hope it helps.
This can be solved using Row_Number() concept. We need to randomly assign row number values within a partition of mid in the table s. And, do a Join from the m table to s using mid and row_number = 1. This will pick a single Random row everytime.
In MySQL version below 8, we can use User-defined Variables to emulate Row_Number(). To understand how this works, you may check this answer for the explanation: https://stackoverflow.com/a/53465139/2469308
Note that this technique will be efficient on Large tables than using a Subquery (in the SELECT clause), as it will be doing overall table Sorting only once
View on DB Fiddle
create table m (id int, m_nm varchar(10));
create table s (id int,
mid int references m(mid),
s_nm varchar(10));
insert into m values(1, "a");
insert into m values(2, "b");
insert into m values(3, "c");
insert into s values(1, 1, "aa");
insert into s values(2, 1, "aa");
insert into s values(3, 2, "bb");
insert into s values(4, 2, "bbb");
insert into s values(5, 2, "bbbb");
insert into s values(6, 3, "cc");
insert into s values(7, 3, "ccc");
Query
SELECT
m.*, s_dt.id, s_dt.mid, s_dt.s_nm
FROM
m
JOIN
(
SELECT
#rn := IF(#m = dt.mid, #rn+1, 1) AS row_num,
#m := dt.mid AS mid,
dt.id,
dt.s_nm
FROM
(
SELECT
id, mid, s_nm, RAND() as rand_num
FROM s
ORDER BY mid, rand_num ) AS dt
CROSS JOIN (SELECT #rn:=0, #m:=0) AS user_vars
) AS s_dt
ON s_dt.mid = m.id AND
s_dt.row_num = 1;
Result (Run #1)
| id | m_nm | id | mid | s_nm |
| --- | ---- | --- | --- | ---- |
| 1 | a | 2 | 1 | aa |
| 2 | b | 5 | 2 | bbbb |
| 3 | c | 7 | 3 | ccc |
Result (Run #2)
| id | m_nm | id | mid | s_nm |
| --- | ---- | --- | --- | ---- |
| 1 | a | 1 | 1 | aa |
| 2 | b | 4 | 2 | bbb |
| 3 | c | 6 | 3 | cc |
Result (Run #3)
| id | m_nm | id | mid | s_nm |
| --- | ---- | --- | --- | ---- |
| 1 | a | 1 | 1 | aa |
| 2 | b | 3 | 2 | bb |
| 3 | c | 7 | 3 | ccc |
MySQL 8.0.2+ / MariaDB 10.3+ solution would be simply the following:
SELECT
m.*, s_dt.id, s_dt.mid, s_dt.s_nm
FROM
m
JOIN
(
SELECT
s.*,
ROW_NUMBER() OVER w AS row_num
FROM s
WINDOW w AS (PARTITION BY mid
ORDER BY RAND())
) AS s_dt
ON s_dt.mid = m.id AND
s_dt.row_num = 1
View on DB Fiddle

mysql - can i differentiate values of same column?

Is there a way to make two or more same values in the same column to be different by database engine?
Imagine that I have column like this:
id | votes
----------
1 | 20
2 | 20
3 | 19
4 | 16
5 | 15
I have to make all the votes different in that way that i increment first of two equal values, and substract other one.
After one iteration my DB should look like this:
----------
1 | 21
2 | 19
3 | 19
4 | 16
5 | 15
Because we still have two same values (id 2 and 3) we keep going with second iteration:
----------
1 | 21
2 | 20
3 | 18
4 | 16
5 | 15
Can i update mysql database somehow to make that for me? Or should i select values, compare them in php and update?
Thank you!
This does not exactly match your expected result, but may lead you toward an acceptable compromise.
MySQL does not yet have window functions such as rank(), dense_rank() or row_number() but you can mimic row_number using #variables such as seen below.
See this demo
MySQL 5.6 Schema Setup:
CREATE TABLE Table1
(`id` int, `votes` int)
;
INSERT INTO Table1
(`id`, `votes`)
VALUES
(1, 20),
(2, 20),
(3, 19),
(4, 16),
(5, 15)
;
Query 1:
SELECT
t.id
, t.votes
, m.v - #row_num AS adjusted_votes
, #row_num := #row_num+1 AS RowNumber
FROM Table1 t
CROSS JOIN (SELECT MAX(votes) v from Table1) m
CROSS JOIN (SELECT #row_num :=0) vars
ORDER BY
t.votes DESC
, t.id
Results:
| id | votes | adjusted_votes | RowNumber |
|----|-------|----------------|-----------|
| 1 | 20 | 20 | 1 |
| 2 | 20 | 19 | 2 |
| 3 | 19 | 18 | 3 |
| 4 | 16 | 17 | 4 |
| 5 | 15 | 16 | 5 |
Note that the reduction of 20 to 19 for id 2 appears to be completely arbitrary and that id is used purely as a tie-breaker when values are equal.

select random rows in mysql

I have one table named Mydata as follows
id name type
--------------------------------------------
1 vinu 1
2 rinu 2
3 dilu 1
4 raju 2
5 manu 3
6 saju 3
7 ragu 3
8 sonu 1
9 sam 1
10 rag 1
--------------------------------------------
I want to print records with alternating type, for example:
First row with type =1
Second row with type =2
Third row with type =3
4th row type=1
5th row type=2 and so on
Required result as follows
id name type
-----------------------------------------
1 vinu 1
2 rinu 2
5 manu 3
3 dilu 1
4 raju 2
6 saju 3
8 sonu 1
7 ragu 3
9 sam 1
10 rag 1
----------------------------------------------
Sample data:
CREATE TABLE t
(`id` int, `name` varchar(4), `type` int)
;
INSERT INTO t
(`id`, `name`, `type`)
VALUES
(1, 'vinu', 1),
(2, 'rinu', 2),
(3, 'dilu', 1),
(4, 'raju', 2),
(5, 'manu', 3),
(6, 'saju', 3),
(7, 'ragu', 3),
(8, 'sonu', 1),
(9, 'sam', 1),
(10, 'rag', 1)
;
Query:
SELECT id, name, type FROM (
SELECT
t.*,
#rn := IF(#prev_type = type, #rn + 1, 1) AS rownumber,
#prev_type := type
FROM
t
, (SELECT #rn := 0, #prev_type := NULL) var_init_subquery
ORDER BY type
) sq
ORDER BY rownumber, type
Result:
| id | name | type |
|----|------|------|
| 1 | vinu | 1 |
| 4 | raju | 2 |
| 5 | manu | 3 |
| 9 | sam | 1 |
| 2 | rinu | 2 |
| 7 | ragu | 3 |
| 8 | sonu | 1 |
| 6 | saju | 3 |
| 10 | rag | 1 |
| 3 | dilu | 1 |
see it working live in an sqlfiddle
Caveat:
Don't expect this to be performant when you have lots of data. It's doing a full table scan.
Here's a manual entry to read when you're interested about how this variables work.
This cannot be done via a raw SQL query. Extract the rows you need to display, and then sort them via your application.
Alternatively... you could write a stored procedure, but I don't recommend this. You will need a temporary table and a cursor (that transparently creates another temporary table). Too much for a query that should be executed often.