快速组 rank() 函数
Fast group rank() function
人们尝试在 MySQL 中模拟 MSSQL RANK() 或 ROW_NUMBER() 函数的方法多种多样,但到目前为止我尝试过的所有方法都很慢。
我有一个 table 看起来像这样:
CREATE TABLE ratings
(`id` int, `category` varchar(1), `rating` int)
;
INSERT INTO ratings
(`id`, `category`, `rating`)
VALUES
(3, '*', 54),
(4, '*', 45),
(1, '*', 43),
(2, '*', 24),
(2, 'A', 68),
(3, 'A', 43),
(1, 'A', 12),
(3, 'B', 22),
(4, 'B', 22),
(4, 'C', 44)
;
除了它有 220,000 条记录。大约有 90,000 个唯一 ID。
我想通过查看不是 *
的类别来将 id 排在第一位,其中较高的评级是较低的排名。
SELECT g1.id,
g1.category,
g1.rating,
Count(*) AS rank
FROM ratings AS g1
JOIN ratings AS g2 ON (g2.rating, g2.id) >= (g1.rating, g1.id)
AND g1.category = g2.category
WHERE g1.category != '*'
GROUP BY g1.id,
g1.category,
g1.rating
ORDER BY g1.category,
rank
输出:
id category rating rank
2 A 68 1
3 A 43 2
1 A 12 3
4 B 22 1
3 B 22 2
4 C 44 1
然后我想取一个 id 的最小排名,并将其与他们在 * 类别中的排名平均。给出总查询数:
SELECT X1.id,
(X1.rank + X2.minrank) / 2 AS OverallRank
FROM
(SELECT g1.id,
g1.category,
g1.rating,
Count(*) AS rank
FROM ratings AS g1
JOIN ratings AS g2 ON (g2.rating, g2.id) >= (g1.rating, g1.id)
AND g1.category = g2.category
WHERE g1.category = '*'
GROUP BY g1.id,
g1.category,
g1.rating
ORDER BY g1.category,
rank) X1
JOIN
(SELECT id,
Min(rank) AS MinRank
FROM
(SELECT g1.id,
g1.category,
g1.rating,
Count(*) AS rank
FROM ratings AS g1
JOIN ratings AS g2 ON (g2.rating, g2.id) >= (g1.rating, g1.id)
AND g1.category = g2.category
WHERE g1.category != '*'
GROUP BY g1.id,
g1.category,
g1.rating
ORDER BY g1.category,
rank) X
GROUP BY id) X2 ON X1.id = X2.id
ORDER BY overallrank
给我
id OverallRank
3 1.5000
4 1.5000
2 2.5000
1 3.0000
这个查询是正确的,也是我想要的输出,但它只是挂在我真实的 table 220,000 条记录上。我该如何优化它?我的真实 table 在 id,rating
和 category
以及 id,category
上有一个索引
编辑:
SHOW CREATE TABLE ratings
的结果:
CREATE TABLE `rating` (
`id` int(11) NOT NULL,
`category` varchar(255) NOT NULL,
`rating` int(11) NOT NULL DEFAULT '1500',
`rd` int(11) NOT NULL DEFAULT '350',
`vol` float NOT NULL DEFAULT '0.06',
`wins` int(11) NOT NULL,
`losses` int(11) NOT NULL,
`streak` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`streak`,`rd`,`id`,`category`),
UNIQUE KEY `id_category` (`id`,`category`),
KEY `rating` (`rating`,`rd`),
KEY `streak_idx` (`streak`),
KEY `category_idx` (`category`),
KEY `id_rating_idx` (`id`,`rating`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
PRIMARY KEY
是查询此 table 的最常见用例,这就是为什么它是聚簇键。值得注意的是,该服务器是 raid 10 的 SSD,具有 9GB/s FIO 随机读取。所以我不怀疑没有聚集的索引会影响很大。
(select count(distinct category) from ratings)
的输出是 50
考虑到这可能是数据的方式或对我的疏忽,我包括了整个 table 的导出。压缩后只有 200KB:https://www.dropbox.com/s/p3iv23zi0uzbekv/ratings.zip?dl=0
第一次查询用了27秒到运行
您可以使用带有 AUTO_INCREMENT 列的临时 table 来生成排名(行号)。
例如 - 为“*”类别生成排名:
drop temporary table if exists tmp_main_cat_rank;
create temporary table tmp_main_cat_rank (
rank int unsigned auto_increment primary key,
id int NOT NULL
) engine=memory
select null as rank, id
from ratings r
where r.category = '*'
order by r.category, r.rating desc, r.id desc;
这个 运行 大约需要 30 毫秒。虽然您使用 selfjoin 的方法在我的机器上需要 45 秒。即使在 (category, rating, id)
上有一个新索引,它仍然需要 14 秒才能到达 运行。
按组(按类别)生成排名有点复杂。我们仍然可以使用 AUTO_INCREMENT 列,但需要计算并减去每个类别的偏移量:
drop temporary table if exists tmp_pos;
create temporary table tmp_pos (
pos int unsigned auto_increment primary key,
category varchar(50) not null,
id int NOT NULL
) engine=memory
select null as pos, category, id
from ratings r
where r.category <> '*'
order by r.category, r.rating desc, r.id desc;
drop temporary table if exists tmp_cat_offset;
create temporary table tmp_cat_offset engine=memory
select category, min(pos) - 1 as `offset`
from tmp_pos
group by category;
select t.id, min(t.pos - o.offset) as min_rank
from tmp_pos t
join tmp_cat_offset o using(category)
group by t.id
这 运行 秒大约需要 220 毫秒。 selfjoin 解决方案使用新索引需要 42 秒或 13 秒。
现在您只需要将最后一个查询与第一个临时查询 table 结合起来即可获得最终结果:
select t1.id, (t1.min_rank + t2.rank) / 2 as OverallRank
from (
select t.id, min(t.pos - o.offset) as min_rank
from tmp_pos t
join tmp_cat_offset o using(category)
group by t.id
) t1
join tmp_main_cat_rank t2 using(id);
总的来说 运行时间是 ~280 毫秒没有额外的索引和 ~240 毫秒有索引在 (category, rating, id)
.
selfjoin 方法的注意事项:这是一个优雅的解决方案,并且在小组规模较小的情况下表现良好。它很快,平均组大小 <= 2。对于 10 人的组,它可以接受 table。但是你的平均组大小为 447 (count(*) / count(distinct category)
)。这意味着每一行都与其他 447 行(平均)相连。您可以通过删除 group by 子句来查看影响:
SELECT Count(*)
FROM ratings AS g1
JOIN ratings AS g2 ON (g2.rating, g2.id) >= (g1.rating, g1.id)
AND g1.category = g2.category
WHERE g1.category != '*'
结果超过 1000 万行。
但是 - 使用 (category, rating, id)
上的索引,您的查询 运行s 在我的机器上用了 33 秒。
人们尝试在 MySQL 中模拟 MSSQL RANK() 或 ROW_NUMBER() 函数的方法多种多样,但到目前为止我尝试过的所有方法都很慢。
我有一个 table 看起来像这样:
CREATE TABLE ratings
(`id` int, `category` varchar(1), `rating` int)
;
INSERT INTO ratings
(`id`, `category`, `rating`)
VALUES
(3, '*', 54),
(4, '*', 45),
(1, '*', 43),
(2, '*', 24),
(2, 'A', 68),
(3, 'A', 43),
(1, 'A', 12),
(3, 'B', 22),
(4, 'B', 22),
(4, 'C', 44)
;
除了它有 220,000 条记录。大约有 90,000 个唯一 ID。
我想通过查看不是 *
的类别来将 id 排在第一位,其中较高的评级是较低的排名。
SELECT g1.id,
g1.category,
g1.rating,
Count(*) AS rank
FROM ratings AS g1
JOIN ratings AS g2 ON (g2.rating, g2.id) >= (g1.rating, g1.id)
AND g1.category = g2.category
WHERE g1.category != '*'
GROUP BY g1.id,
g1.category,
g1.rating
ORDER BY g1.category,
rank
输出:
id category rating rank
2 A 68 1
3 A 43 2
1 A 12 3
4 B 22 1
3 B 22 2
4 C 44 1
然后我想取一个 id 的最小排名,并将其与他们在 * 类别中的排名平均。给出总查询数:
SELECT X1.id,
(X1.rank + X2.minrank) / 2 AS OverallRank
FROM
(SELECT g1.id,
g1.category,
g1.rating,
Count(*) AS rank
FROM ratings AS g1
JOIN ratings AS g2 ON (g2.rating, g2.id) >= (g1.rating, g1.id)
AND g1.category = g2.category
WHERE g1.category = '*'
GROUP BY g1.id,
g1.category,
g1.rating
ORDER BY g1.category,
rank) X1
JOIN
(SELECT id,
Min(rank) AS MinRank
FROM
(SELECT g1.id,
g1.category,
g1.rating,
Count(*) AS rank
FROM ratings AS g1
JOIN ratings AS g2 ON (g2.rating, g2.id) >= (g1.rating, g1.id)
AND g1.category = g2.category
WHERE g1.category != '*'
GROUP BY g1.id,
g1.category,
g1.rating
ORDER BY g1.category,
rank) X
GROUP BY id) X2 ON X1.id = X2.id
ORDER BY overallrank
给我
id OverallRank
3 1.5000
4 1.5000
2 2.5000
1 3.0000
这个查询是正确的,也是我想要的输出,但它只是挂在我真实的 table 220,000 条记录上。我该如何优化它?我的真实 table 在 id,rating
和 category
以及 id,category
编辑:
SHOW CREATE TABLE ratings
的结果:
CREATE TABLE `rating` (
`id` int(11) NOT NULL,
`category` varchar(255) NOT NULL,
`rating` int(11) NOT NULL DEFAULT '1500',
`rd` int(11) NOT NULL DEFAULT '350',
`vol` float NOT NULL DEFAULT '0.06',
`wins` int(11) NOT NULL,
`losses` int(11) NOT NULL,
`streak` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`streak`,`rd`,`id`,`category`),
UNIQUE KEY `id_category` (`id`,`category`),
KEY `rating` (`rating`,`rd`),
KEY `streak_idx` (`streak`),
KEY `category_idx` (`category`),
KEY `id_rating_idx` (`id`,`rating`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
PRIMARY KEY
是查询此 table 的最常见用例,这就是为什么它是聚簇键。值得注意的是,该服务器是 raid 10 的 SSD,具有 9GB/s FIO 随机读取。所以我不怀疑没有聚集的索引会影响很大。
(select count(distinct category) from ratings)
的输出是 50
考虑到这可能是数据的方式或对我的疏忽,我包括了整个 table 的导出。压缩后只有 200KB:https://www.dropbox.com/s/p3iv23zi0uzbekv/ratings.zip?dl=0
第一次查询用了27秒到运行
您可以使用带有 AUTO_INCREMENT 列的临时 table 来生成排名(行号)。
例如 - 为“*”类别生成排名:
drop temporary table if exists tmp_main_cat_rank;
create temporary table tmp_main_cat_rank (
rank int unsigned auto_increment primary key,
id int NOT NULL
) engine=memory
select null as rank, id
from ratings r
where r.category = '*'
order by r.category, r.rating desc, r.id desc;
这个 运行 大约需要 30 毫秒。虽然您使用 selfjoin 的方法在我的机器上需要 45 秒。即使在 (category, rating, id)
上有一个新索引,它仍然需要 14 秒才能到达 运行。
按组(按类别)生成排名有点复杂。我们仍然可以使用 AUTO_INCREMENT 列,但需要计算并减去每个类别的偏移量:
drop temporary table if exists tmp_pos;
create temporary table tmp_pos (
pos int unsigned auto_increment primary key,
category varchar(50) not null,
id int NOT NULL
) engine=memory
select null as pos, category, id
from ratings r
where r.category <> '*'
order by r.category, r.rating desc, r.id desc;
drop temporary table if exists tmp_cat_offset;
create temporary table tmp_cat_offset engine=memory
select category, min(pos) - 1 as `offset`
from tmp_pos
group by category;
select t.id, min(t.pos - o.offset) as min_rank
from tmp_pos t
join tmp_cat_offset o using(category)
group by t.id
这 运行 秒大约需要 220 毫秒。 selfjoin 解决方案使用新索引需要 42 秒或 13 秒。
现在您只需要将最后一个查询与第一个临时查询 table 结合起来即可获得最终结果:
select t1.id, (t1.min_rank + t2.rank) / 2 as OverallRank
from (
select t.id, min(t.pos - o.offset) as min_rank
from tmp_pos t
join tmp_cat_offset o using(category)
group by t.id
) t1
join tmp_main_cat_rank t2 using(id);
总的来说 运行时间是 ~280 毫秒没有额外的索引和 ~240 毫秒有索引在 (category, rating, id)
.
selfjoin 方法的注意事项:这是一个优雅的解决方案,并且在小组规模较小的情况下表现良好。它很快,平均组大小 <= 2。对于 10 人的组,它可以接受 table。但是你的平均组大小为 447 (count(*) / count(distinct category)
)。这意味着每一行都与其他 447 行(平均)相连。您可以通过删除 group by 子句来查看影响:
SELECT Count(*)
FROM ratings AS g1
JOIN ratings AS g2 ON (g2.rating, g2.id) >= (g1.rating, g1.id)
AND g1.category = g2.category
WHERE g1.category != '*'
结果超过 1000 万行。
但是 - 使用 (category, rating, id)
上的索引,您的查询 运行s 在我的机器上用了 33 秒。