按组中的前 n(最少)项计数和分组

Posted

技术标签:

【中文标题】按组中的前 n(最少)项计数和分组【英文标题】:count and group by first n(minimum) items in group 【发布时间】:2013-12-02 16:30:46 【问题描述】:

我已经经历了几个“n from M”类型的解决方案,但无法接近我所追求的,尽管这个问题之前可能已经以其他格式提出过。

我已经尝试了来自 mysql Group By with top N number of each kind 和 http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/ 的示例,这些示例似乎都不适用于我正在尝试做的事情。

我正在尝试做的是在跑步比赛中确定最好的团队,个人跑步者不是问题,性别、年龄类别都可以考虑。团队奖励的规则基于俱乐部的会员资格。

    俱乐部必须至少有 3 名参赛者才有资格参加团体赛。 只有每个俱乐部的前 3 名参赛者才计入比赛。 团队名次由符合条件的参赛者的总和决定,因此获得第 2 名、第 9 名和第 10 名的俱乐部 A 的参赛者获得 21 分,获得第 4 名、第 5 名和第 6 名的俱乐部 B 的参赛者获得 15 分,等等。

我有一个包含以下字段的表格:

+---------------+-------------+------+-----+---------+----------------+
| Field         | Type        | Null | Key | Default | Extra          |
+---------------+-------------+------+-----+---------+----------------+
| id            | int(11)     | NO   | PRI | NULL    | auto_increment |
| runner_id     | int(11)     | YES  |     | NULL    |                |
| club_id       | int(11)     | YES  |     | NULL    |                |
| race_id       | int(11)     | YES  |     | NULL    |                |
| race_number   | int(11)     | YES  |     | NULL    |                |
| category      | varchar(20) | YES  |     | NULL    |                |
| finish_time   | int(11)     | YES  |     | NULL    |                |
| race_position | int(11)     | YES  |     | NULL    |                |
+---------------+-------------+------+-----+---------+----------------+

只有 club_id 和 race_position 与查询相关。 runner_id、club_id 和 race_id 是外键,我需要能够在创建结果时从这些表中提取数据(given_name、family_name、age、club_name 等)。

这是典型数据:

+----+-----------+---------+---------+-------------+-----------+-------------+---------------+
| id | runner_id | club_id | race_id | race_number | category  | finish_time | race_position |
+----+-----------+---------+---------+-------------+-----------+-------------+---------------+
| 53 |        26 |       1 |      85 |          17 | Msenior   |        1666 |            11 |
| 35 |        39 |       1 |      85 |           4 | Munder_18 |        1503 |             4 |
| 63 |        61 |       2 |      85 |          27 | Mvet_50   |        1610 |             9 |
| 42 |        46 |       2 |      85 |          11 | Lvet_40   |        1773 |            14 |
| 38 |        42 |       2 |      85 |           7 | Lunder_18 |        1793 |            17 |
| 56 |        36 |       9 |      85 |          20 | Msenior   |        1561 |             6 |
| 44 |        48 |       9 |      85 |          13 | Msenior   |        1667 |            12 |
| 64 |        62 |       9 |      85 |          28 | Msenior   |        1660 |            10 |
| 49 |        52 |       9 |      85 |          18 | Msenior   |        1432 |             1 |
| 47 |        51 |      10 |      85 |          16 | Msenior   |        1779 |            15 |
| 61 |        59 |      11 |      85 |          25 | Mvet_50   |        1502 |             3 |
| 33 |        38 |      11 |      85 |           2 | Munder_18 |        1440 |             2 |
| 65 |        63 |      11 |      85 |          29 | Mvet_40   |        1566 |             8 |
| 54 |        54 |      12 |      85 |          19 | Msenior   |        1785 |            16 |
| 58 |        56 |      12 |      85 |          23 | Msenior   |        1546 |             5 |
| 37 |        41 |      12 |      85 |           6 | Munder_18 |        1668 |            13 |
| 45 |        49 |      14 |      85 |          14 | Mvet_50   |        1565 |             7 |
+----+-----------+---------+---------+-------------+-----------+-------------+---------------+

我想要的结果是这样的:

+----+-----------+---------+---------+-------------+-----------+-------------+---------------+
| id | runner_id | club_id | race_id | race_number | category  | finish_time | race_position |
+----+-----------+---------+---------+-------------+-----------+-------------+---------------+
| 33 |        38 |      11 |      85 |           2 | Munder_18 |        1440 |             2 |
| 61 |        59 |      11 |      85 |          25 | Mvet_50   |        1502 |             3 |
| 65 |        63 |      11 |      85 |          29 | Mvet_40   |        1566 |             8 |
| 49 |        52 |       9 |      85 |          18 | Msenior   |        1432 |             1 |
| 56 |        36 |       9 |      85 |          20 | Msenior   |        1561 |             6 |
| 64 |        62 |       9 |      85 |          28 | Msenior   |        1660 |            10 |
| 58 |        56 |      12 |      85 |          23 | Msenior   |        1546 |             5 |
| 37 |        41 |      12 |      85 |           6 | Munder_18 |        1668 |            13 |
| 54 |        54 |      12 |      85 |          19 | Msenior   |        1785 |            16 |
| 63 |        61 |       2 |      85 |          27 | Mvet_50   |        1610 |             9 |
| 42 |        46 |       2 |      85 |          11 | Lvet_40   |        1773 |            14 |
| 38 |        42 |       2 |      85 |           7 | Lunder_18 |        1793 |            17 |
+----+-----------+---------+---------+-------------+-----------+-------------+---------------+

因此,即使 runner_id 的 52 赢得了比赛,他也不在获胜的队伍中。

我在 Codeigniter/Datamapper ORM 下运行所有​​这些,但我可以通过这一层向下传递完整的 SQL 查询字符串。

我希望这一切都有意义。

【问题讨论】:

【参考方案1】:

MySQL 缺少解决此问题的重要功能(CTE、窗口函数),但您可以使用一些用户定义的变量并支付性能成本来解决它们:

SELECT s1.id, s1.runner_id, s1.club_id, s1.race_id, s1.race_number, s1.category,
  s1.finish_time, s1.race_position
FROM (
  SELECT t1.*,
    @club_rank := if(@prev_club = t1.club_id, @club_rank + 1, 1) club_rank,
    @prev_club := t1.club_id
  FROM t t1
  CROSS JOIN (SELECT @prev_club := NULL, @club_rank := 1) init
  ORDER BY t1.club_id, t1.race_position
) s1
JOIN (
  SELECT club_id, count(*) teamSize, sum(race_position) teamPosition FROM t
  GROUP BY club_id
) s2 ON s1.club_id = s2.club_id
WHERE club_rank <= 3 AND teamSize >= 3
ORDER BY teamPosition, race_position

输出:

| ID | RUNNER_ID | CLUB_ID | RACE_ID | RACE_NUMBER |  CATEGORY | FINISH_TIME | RACE_POSITION |
|----|-----------|---------|---------|-------------|-----------|-------------|---------------|
| 33 |        38 |      11 |      85 |           2 | Munder_18 |        1440 |             2 |
| 61 |        59 |      11 |      85 |          25 |   Mvet_50 |        1502 |             3 |
| 65 |        63 |      11 |      85 |          29 |   Mvet_40 |        1566 |             8 |
| 49 |        52 |       9 |      85 |          18 |   Msenior |        1432 |             1 |
| 56 |        36 |       9 |      85 |          20 |   Msenior |        1561 |             6 |
| 64 |        62 |       9 |      85 |          28 |   Msenior |        1660 |            10 |
| 58 |        56 |      12 |      85 |          23 |   Msenior |        1546 |             5 |
| 37 |        41 |      12 |      85 |           6 | Munder_18 |        1668 |            13 |
| 54 |        54 |      12 |      85 |          19 |   Msenior |        1785 |            16 |
| 63 |        61 |       2 |      85 |          27 |   Mvet_50 |        1610 |             9 |
| 42 |        46 |       2 |      85 |          11 |   Lvet_40 |        1773 |            14 |
| 38 |        42 |       2 |      85 |           7 | Lunder_18 |        1793 |            17 |

小提琴here.

【讨论】:

哇!谢谢。性能可能不是问题(除非它很慢),因为结果表最多只能保存几千行 - 每场比赛最多有大约 250 名跑步者 - 并且只有一个人运行此查询一次每场比赛的结束。 此查询存在问题。它实际上是对整个团队的价值进行求和,而不仅仅是前 3 名的值。如果不复制排名,我找不到一个体面的方法来解决这个问题。最有可能使用临时表是最好的解决方案:/ 你能做到吗 - code@tmp_rank := club_rank; @club_rank := if(@prev_club = t1.club_id AND @tmp_rank code ? 是的,但它不会改变任何东西。问题在于分组依据的派生表中。它总结了一切。为了仅对排名前 3 位求和,那么您必须再次计算其中的排名......如果您创建一个带有排名的临时表,那么您计算一次然后使用它两次。【参考方案2】:

有点晚了,因为我身体不适。

我想出了一个不优雅的解决方案。我在表中添加了一个 club_total 列。然后,我通过一个查询循环遍历表,每个俱乐部获得前 N 名跑步者,查询如下:

select * from entries where race_id=? and club_id=? LIMIT ? order by race_position;

然后我忽略那些完赛人数少于 N 人的俱乐部,并将其他俱乐部的比赛位置相加,并将这个值写回表中。

最后我运行另一个查询来只提取那些包含俱乐部总数的行:

select * from entries where club_total > 0 and race_id=? order by club_total, race_position;

就像我说的那样,它并不优雅,当然也不快(我没有计时),但它每年只会在一台机器上运行几次,而且记录集是几百行一个最大值。对于小数据集,它并不比通过 AJAX 显示数据的简单查询慢。在这种情况下完成工作比速度更重要。我不会在性能有问题的任何情况下使用这种方法

【讨论】:

以上是关于按组中的前 n(最少)项计数和分组的主要内容,如果未能解决你的问题,请参考以下文章

如何按 RDD 中的选定字段数进行分组,以查找基于这些字段的重复项

查找与排序:计数排序

按组的每个出现值构建计数列

如何从MYSQL中的组中查找前N个记录的查询结果

按组将唯一/不同值的计数添加到原始数据

使用 pandas 在数据帧上执行 groupby,按计数排序并获取 python 中的前 2 个计数