按组中的前 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(最少)项计数和分组的主要内容,如果未能解决你的问题,请参考以下文章