MySQL SET rank over First Column and Second Column - mysql

I'm trying to make a query setting rank column by First and Second column. Like Rank over Partition which doesn't exist MySQL
For example,
From
+----+-------+--------+------+
| id | First | Second | Rank |
+----+-------+--------+------+
| 1 | a | 10 | |
| 2 | a | 9 | |
| 3 | b | 10 | |
| 4 | b | 7 | |
| 5 | a | 1 | |
| 6 | b | 1 | |
+----+-------+--------+------+
To
+----+-------+--------+------+
| id | First | Second | Rank |
+----+-------+--------+------+
| 1 | a | 10 | 3 |
| 2 | a | 9 | 2 |
| 3 | b | 10 | 3 |
| 4 | b | 7 | 2 |
| 5 | a | 1 | 1 |
| 6 | b | 1 | 1 |
+----+-------+--------+------+
The Rank doesn't continue. It starts from 1 again when it reaches the last value of 'a' of 'First' column.
And it's gotta be SET not SELECT.
I wouldn't mind using SELECT but my point is I'm not trying to retrieve data from Database but setting values.
Cheers in advance mates.

DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
,first CHAR(1) NOT NULL
,second INT NOT NULL
);
INSERT INTO my_table VALUES
(1,'a',10),
(2,'a',9),
(3,'b',10),
(4,'b',7),
(5,'a',1),
(6,'b',1);
SELECT id
, first
, second
, rank
FROM
( SELECT x.*
, CASE WHEN #prev = first THEN #i:=#i+1 ELSE #i:=1 END rank
, #prev:=first
FROM my_table x
, (SELECT #prev:=null,#i:=0) vars
ORDER
BY first
, second
, id
) a
ORDER
BY id;
+----+-------+--------+------+
| id | first | second | rank |
+----+-------+--------+------+
| 1 | a | 10 | 3 |
| 2 | a | 9 | 2 |
| 3 | b | 10 | 3 |
| 4 | b | 7 | 2 |
| 5 | a | 1 | 1 |
| 6 | b | 1 | 1 |
+----+-------+--------+------+
6 rows in set (0.00 sec)

Came up with a solution which I was looking for.
I'm not sure if these queries are completely safe but so far no harms.
SET #rank = 0, #First = ''
UPDATE 'Table' SET
rank = IF(#First = First, #rank:= #rank +1, #rank := 1 AND #First := First)
ORDER BY First ASC, Second;

One method is a correlated subquery. For rank() you can do:
select t.*,
(select count(*) + 1
from t t2
where t2.first = t.first and t2.second < t.second
) as rank
from t;
Ranks are tricky to handle with variables (dense_rank() and row_number() are simpler).
EDIT:
This is easy to turn into an update:
update t join
(select t.*,
(select count(*) + 1
from t t2
where t2.first = t.first and t2.second < t.second
) as new_rank
from t
) tt
on t.id = tt.id
set t.rank = tt.new_rank;

Related

Update table set same value to all rows with duplicate value

I have a table of sport results. When using following code, the table get places according to the times.
SET #pos := 0;
UPDATE table SET Place = ( SELECT #pos := #pos + 1 ) ORDER BY Time ASC;
In case of same times (like rows 1,3 and 4,5), it updates it according to the ID-s, so the result is following:
ID | Time | Place
1 | 00:12:14 | 1
2 | 00:12:18 | 3
3 | 00:12:14 | 2
4 | 00:12:25 | 4
5 | 00:12:25 | 5
How could I update the table so, that if there is multiple rows of same time, all rows would get the best place (like in following table)?
ID | Time | Place
1 | 00:12:14 | 1
2 | 00:12:18 | 3
3 | 00:12:14 | 1
4 | 00:12:25 | 4
5 | 00:12:25 | 4
You can do it with a join of the table to a query returning for each row the number of rows less than its Time:
update tablename t inner join (
select t.id, (
select count(*) counter from tablename
where time < t.time
) counter
from tablename t
) c on c.id = t.id
set t.place = c.counter + 1;
See the demo.
Results:
| ID | Time | Place |
| --- | -------- | ----- |
| 1 | 00:12:14 | 1 |
| 2 | 00:12:18 | 3 |
| 3 | 00:12:14 | 1 |
| 4 | 00:12:25 | 4 |
| 5 | 00:12:25 | 4 |

Sequentially update mysql table

Given the following table, how can I sequentially reorder position from 1 to N using a single query after one or more rows have been deleted and still preserve the order of position?
+---------+----------+-----+
| id (pk) | position | fk |
+---------+----------+-----+
| 4 | 1 | 123 |
| 2 | 2 | 123 |
| 18 | 3 | 123 |
| 5 | 4 | 123 |
| 3 | 5 | 123 |
+---------+----------+-----+
For instance, if position=1 (id=4) was deleted, the desired final records are:
+---------+----------+-----+
| id (pk) | position | fk |
+---------+----------+-----+
| 2 | 1 | 123 |
| 18 | 2 | 123 |
| 5 | 3 | 123 |
| 3 | 4 | 123 |
+---------+----------+-----+
and if position=3 (id=18) was deleted, the desired final records are:
+---------+----------+-----+
| id (pk) | position | fk |
+---------+----------+-----+
| 4 | 1 | 123 |
| 2 | 2 | 123 |
| 5 | 3 | 123 |
| 3 | 4 | 123 |
+---------+----------+-----+
I can do something like the following if only row was deleted but not for multiple rows.
DELETE FROM mytable WHERE fk=123 AND position = 4;
UPDATE mytable SET position=position-1 WHERE fk=123 AND position > 4;
User-defined variables to the rescue if you're not already using MySQL 8, which provides window functions like ROW_NUMBER():
UPDATE t
JOIN (
SELECT
t.*
, #n := #n + 1 as n
FROM t
, (SELECT #n := 0) var_init
ORDER BY position
) sq ON t.id = sq.id
SET t.position = sq.n;
see it working live in an sqlfiddle
BONUS:
It gets slightly more complicated, when you have multiple groups.
For example, for sample data like this
| id | position | fk |
|-----|----------|-----|
| 4 | 1 | 123 |
| 2 | 2 | 123 |
| 5 | 4 | 123 |
| 3 | 5 | 123 |
| 40 | 1 | 234 |
| 20 | 2 | 234 |
| 180 | 3 | 234 |
| 30 | 5 | 234 |
the query would be
UPDATE t
JOIN (
SELECT
t.*
, #n := if(#prev_fk != fk, 1, #n + 1) as n
, #prev_fk := fk
FROM t
, (SELECT #n := 0, #prev_fk := NULL) var_init
ORDER BY fk, position
) sq ON t.id = sq.id
SET t.position = sq.n;
Here you just save the current fk in another variable. When the next row is processed, the variable still holds the value of the "previous row". Then you reset the #n variable, when the value changes.
see it working live in an sqlfiddle
UPDATE:
In MySQL 8 you can use the window function row_number() like this:
update t join (
select t.*, row_number() over (partition by fk order by position) as new_pos
from t
) sq using (id) set t.position = sq.new_pos;
You can use update and the ROW_NUMBER() function. If you order by position you it should be ok.
UPDATE [1]
SET POSITION = [2].RN
FROM t [1]
JOIN (
SELECT
t.ID
, ROW_NUMBER() OVER (ORDER BY POSITION DESC) AS RN
FROM t
) [2]
ON [1].id = [2].id
As people have mentioned this is not applicable for MySql. Sorry for the missinformation as i didnt saw the tag.

Counting and limiting number of rows per field value not working

I'm trying to limit the number of rows per field value of a given query. I've found this answered question:
here
As in the first answer of the link, I've created the following table:
create table mytab (
id int not null auto_increment primary key,
first_column int,
second_column int
) engine = myisam;
Inserted this data:
insert into mytab (first_column,second_column) values
(1,1),
(1,4),
(2,10),
(3,4),
(1,4),
(2,5),
(1,6);
And finally run this query
select
id,
first_column,
second_column,
row_num
from
(select
*,
#num := if(#first_column = first_column, #num + 1, 1) as row_num,
#first_column:=first_column as c
from mytab
order by first_column,id) as t,
(select #first_column:='',#num:=0) as r;
But instead of getting this result, where the row_num increases whenever first_column is repeated,
+----+--------------+---------------+---------+
| id | first_column | second_column | row_num |
+----+--------------+---------------+---------+
| 1 | 1 | 1 | 1 |
| 2 | 1 | 4 | 2 |
| 5 | 1 | 4 | 3 |
| 7 | 1 | 6 | 4 |
| 3 | 2 | 10 | 1 |
| 6 | 2 | 5 | 2 |
| 4 | 3 | 4 | 1 |
+----+--------------+---------------+---------+
I get this result:
+----+--------------+---------------+---------+
| id | first_column | second_column | row_num |
+----+--------------+---------------+---------+
| 1 | 1 | 1 | 1 |
| 2 | 1 | 4 | 1 |
| 5 | 1 | 4 | 1 |
| 7 | 1 | 6 | 1 |
| 3 | 2 | 10 | 1 |
| 6 | 2 | 5 | 1 |
| 4 | 3 | 4 | 1 |
+----+--------------+---------------+---------+
I literally copied the code from the link. I checked in SQL Fiddle and code works fine. I'm using XAMPP. Could that be the reason? If it is, is there any workaround to get something like the above working?
I'd really appreciate some help. Thanks in advance.
The variable assignment has to be in the sub-query.
select
id,
first_column,
second_column,
row_num
from
(select
m.*,
#num := if(#first_column = first_column, #num + 1, 1) as row_num,
#first_column:=first_column as c
from mytab m
cross join (select #first_column:='',#num:=0) r --this was in the outer query previously
order by first_column,id
) t

Sum up values from rows in MySQL table

I have added a new column 'sums' to my table and I am trying to sum up values from 'vals' column then update 'sums' in the same table, according to the algorithm shown below in the table.
I could write a few loops in PHP but I don't think it would be nice.
Any clue how to write it nicely?
--------------------------------------------------
| id | sets | parts | vals | sums |
|-------|---------|---------|----------|---------|
| 1 | 1 | 1 | 2 | 2 |
|-------|---------|---------|----------|---------|
| 2 | 1 | 2 | 3 | 2+3=5 |
|-------|---------|---------|----------|---------|
| 3 | 1 | 3 | 5 |2+3+5=10 |
|-------|---------|---------|----------|---------|
| 4 | 2 | 1 | 4 | 4 |
|-------|---------|---------|----------|---------|
| 5 | 2 | 2 | 1 | 4+1=5 |
|-------|---------|---------|----------|---------|
| 6 | 2 | 3 | 2 | 4+1+2=7 |
|-------|---------|---------|----------|---------|
| 7 | 3 | 1 | 6 | 6 |
|-------|---------|---------|----------|---------|
This should return the value you want for sums:
SELECT t.id
, IF(t.sets=#prev_sets,#i:=#i+t.vals,#i:=t.vals) AS `sums`
, #prev_sets := t.sets AS prev_sets
FROM mytable t
JOIN (SELECT #prev_sets := NULL, #i := 0 ) i
ORDER BY t.sets, t.parts
You can use this as an inline view in an update statement
UPDATE ( SELECT t.id
, IF(t.sets=#prev_sets,#i:=#i+t.vals,#i:=t.vals) AS `sums`
, #prev_sets := t.sets AS prev_sets
FROM mytable t
JOIN (SELECT #prev_sets := NULL, #i := 0 ) i
ORDER BY t.sets, t.parts
) s
JOIN mytable r
ON r.id = s.id
SET r.sums = s.sums
From the example data, it looks like you want the "groupings" on sets, and you want to order on parts within each group.

MySQL SELECT value before MAX

How to select 1st, 2nd or 3rd value before MAX ?
usually we do it with order by and limit
SELECT * FROM table1
ORDER BY field1 DESC
LIMIT 2,1
but with my current query I don't know how to make it...
Sample table
+----+------+------+-------+
| id | name | type | count |
+----+------+------+-------+
| 1 | a | 1 | 2 |
| 2 | ab | 1 | 3 |
| 3 | abc | 1 | 1 |
| 4 | b | 2 | 7 |
| 5 | ba | 2 | 1 |
| 6 | cab | 3 | 9 |
+----+------+------+-------+
I'm taking name for each type with max count with this query
SELECT
`table1b`.`name`
FROM
(SELECT
`table1a`.`type`, MAX(`table1a`.`count`) AS `Count`
FROM
`table1` AS `table1a`
GROUP BY `table1a`.`type`) AS `table1a`
INNER JOIN
`table1` AS `table1b` ON (`table1b`.`type` = `table1a`.`type` AND `table1b`.`count` = `table1a`.`Count`)
and I want one more column additional to name with value before max(count)
so result should be
+------+------------+
| name | before_max |
+------+------------+
| ab | 2 |
| b | 1 |
| cab | NULL |
+------+------------+
Please ask if something isn't clear ;)
AS per your given table(test) structure, the query has to be as follows :
select max_name.name,before_max.count
from
(SELECT type,max(count) as max
FROM `test`
group by type) as type_max
join
(select type,name,count
from test
) as max_name on (type_max.type = max_name.type and count = type_max.max )
left join
(select type,count
from test as t1
where count != (select max(count) from test as t2 where t1.type = t2.type)
group by type
order by count desc) as before_max on(type_max.type = before_max .type)