mysql - 快速组 rank() 函数

标签 mysql performance grouping rank row-number

人们尝试在 MySQL 中模拟 MSSQL RANK() 或 ROW_NUMBER() 函数的方法多种多样,但到目前为止我尝试过的所有方法都很慢。

我有一个看起来像这样的表:

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

这个查询是正确的,也是我想要的输出,但它只是卡在我的 220,000 条记录的真实表上。我该如何优化它?我的真实表在 id,ratingcategoryid,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 是查询此表的最常见用例,这就是为什么它是聚簇键。值得注意的是,该服务器是 raid 10 的 SSD,具有 9GB/s FIO 随机读取。所以我不怀疑没有聚集的索引会影响很大。

(select count(distinct category) from ratings) 的输出是 50

为了这可能是数据的方式或对我的疏忽,我包括了整个表格的导出。压缩后只有 200KB:https://www.dropbox.com/s/p3iv23zi0uzbekv/ratings.zip?dl=0

第一个查询需要 27 秒才能运行

最佳答案

您可以使用带有 AUTO_INCREMENT 列的临时表来生成排名(行号)。

例如 - 为“*”类别生成排名:

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 秒。

现在您只需将最后一个查询与第一个临时表结合起来,即可获得最终结果:

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 人的组大小。但是你的平均组大小为 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) 上的索引,您的查询在我的机器上运行 33 秒。

关于mysql - 快速组 rank() 函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42990419/

相关文章:

html - 如何设置每个 tr 组第一行的样式?

c# - 如何在 C# (linq) 上创建困难组?

mysql - 如何使用 GROUP BY AND ROLLUP 对 MySQL 查询的输出重新排序?

php - 在php中访问mysql OUT变量时出错

javascript - 存储带有多个彼此相邻的撇号的字符串

c++ - 有没有办法从此 sql 语句和逻辑中删除 select count(*)

performance - STM 性能不佳/锁定

mysql - 删除数据库时出错(无法 rmdir './someDB/' ,错误号 : 17)

android - 使用 StaggeredGridLayoutManager 预加载 Android Recyclerview 项目

Pandas - 分为 24 小时区 block ,但不是午夜到午夜