如何用Django查询玩家前4高得分总和并生成排行榜?
问题
我有一个用于追踪玩家多场游戏得分的数据库,希望基于每位玩家的4场最高得分生成排行榜。
模型定义
class Event(models.Model): name = models.CharField(max_length=120) class Player(models.Model): name = models.CharField(max_length=45) class Game(models.Model): name = models.CharField(max_length=15) class Score(models.Model): player = models.ForeignKey(Player, on_delete=models.CASCADE) game = models.ForeignKey(Game, on_delete=models.CASCADE) event = models.ForeignKey(Event, on_delete=models.CASCADE) score = models.IntegerField() class Meta: unique_together = ["player", "event", "game"]
我尝试用子查询实现,但切片似乎被忽略,返回的总和是玩家的所有得分,而非仅前4高得分:
scores = Score.objects.filter(player=OuterRef('pk'), event=myEvent).order_by('-score')[:4] scores = scores.annotate(total=Func(F("score"), function="SUM")).values('total') leaders = Players.objects.annotate(total=Subquery(scores)).order_by('-total')
请问如何让该子查询生效?或者有没有更优的实现方案?
解决方案
修复子查询方案
你的问题核心是子查询里的求和操作在切片之前执行,导致计算了所有得分的总和,而非前4个最高分的和。调整子查询逻辑,先筛选前4个最高分再求和:
from django.db.models import OuterRef, Subquery, Sum, Func # 为每个玩家的得分按降序排名,只保留前4名 ranked_scores = Score.objects.filter( player=OuterRef('pk'), event=myEvent ).annotate( rank=Func( F('score'), function='RANK', template='%(function)s() OVER (PARTITION BY player_id ORDER BY %(expressions)s DESC)' ) ).filter(rank__lte=4) # 子查询计算前4个最高分的总和 total_score = ranked_scores.annotate( total=Sum('score') ).values('total') # 生成排行榜 leaders = Player.objects.annotate( total=Subquery(total_score) ).order_by('-total')
这里用RANK()窗口函数先给每个玩家的得分排序,筛选出前4名后再求和,就能得到正确结果。
更优的窗口函数方案
直接用窗口函数一次性完成排名和求和,数据库查询次数更少,效率更高:
from django.db.models import Window, Sum, F from django.db.models.functions import Rank # 第一步:筛选当前赛事的得分,为每个玩家的得分排名,取前4名并求和 leaderboard = Score.objects.filter(event=myEvent).annotate( player_rank=Window( expression=Rank(), partition_by=F('player'), order_by=F('score').desc() ) ).filter(player_rank__lte=4).values('player').annotate( total_score=Sum('score') ).order_by('-total_score') # 如果需要关联Player模型的其他字段(比如玩家名称),可以再关联查询 leaderboard_with_player = Player.objects.filter( pk__in=leaderboard.values('player') ).annotate( total_score=Subquery( leaderboard.filter(player=OuterRef('pk')).values('total_score') ) ).order_by('-total_score')
关键说明
- 原代码中,
annotate(total=Func(F("score"), function="SUM"))是对所有符合条件的得分求和,切片[:4]只是限制了返回的结果条数,并没有限制求和的范围,这才导致错误。 - 窗口函数
RANK()可以精准地为每个玩家的得分单独排序,确保我们只计算前4个最高分的总和。
内容的提问来源于stack exchange,提问作者desired login
相关产品推荐
相关产品推荐

