【问题标题】:How to efficiently store a ranked list of objects in Django?如何在 Django 中有效地存储对象的排序列表?
【发布时间】:2015-11-09 03:03:05
【问题描述】:

我有一个 Django 模型:

from django.db import models

class Player(models.Model):
    name = models.CharField(max_length=254, null=True, blank=True,)
    score = models.IntegerField()

A = Player.create(name="A", score=99)
B = Player.create(name="B", score=66)
C = Player.create(name="C", score=66)
D = Player.create(name="D", score=55)
E = Player.create(name="E", score=44)

我想保持我所有球员的得分排名。如果玩家得分相同,他们将获得相同的排名。所以在这种情况下,排名将如下所示:

Name       Score     Rank
A          99        1
B          66        2
C          66        2
D          55        4
E          44        5

我想将此排名存储在数据库中,这样我就不必经常执行昂贵的排序查询。我需要高效地执行以下所有操作:

  1. 给定一个玩家,查找他们的排名
  2. 给定一个排名,找出哪些玩家拥有该排名
  3. 给定一个玩家,将其插入此排名
  4. 给定一个玩家,将其从该排名中删除
  5. 给定一名球员和一个分数,更新他们在此排名中的位置

我需要从这个列表中插入和删除玩家。每当我做#3、#4 或#5 时,我都需要相应地更新其他玩家的排名,以保持排名的完整性。

在 Django 中最有效的方法是什么?我如何构建我的模型,这样它才能有效地工作并且我的数据库操作将最小化?请告诉我任何新模型应该是什么样子。

【问题讨论】:

  • “每当我做#3、#4或#5时,我都需要更新其他玩家的排名……” - 这比计算更昂贵即时排名。如果每次插入/删除玩家时都必须更新排名,那么在数据库中存储排名似乎不是一个好主意。
  • 此外,如果所有内容都在内存中,查询会快很多。请参阅 MemcachedRedis 了解在内存中缓存数据库。
  • Xyres,我不能只使用数据库子句“order by”轻松计算这些排名,因为如您所见,我的排名函数有点复杂(即:可能有两个玩家排名第一)。此外,插入/删除播放器将比查找少得多。
  • 您不仅限于使用order_by。您可以编写自定义代码来计算排名。我写了一个即时计算排名的算法。请参阅下面的答案。
  • 查看 Redis SortedSets,它们非常适合您的问题:“Sorted Set 的每个成员都与 score 相关联,用于将有序集合从最小到最大排序. 虽然成员是独一无二的,但分数可能会重复。” redis.io/topics/data-types

标签: python mysql django database sorting


【解决方案1】:

您可以实现ranks 并在保存玩家时重新排序,注意并非所有时候您都需要在保存新分数时重新排序:

新模型:

class ScoreRank(models.Model):
    score = models.IntegerField(primary_key=True)
    rank = models.IntegerField()
    player_count = models.IntegerField()

保持排名排序:

from django.db.models.signals import pre_save
@receiver(pre_save, sender=Player)
def update_score_rank(sender, instance, **kwargs):
    #delete from previous rank
    if instance.pk:
       previous_score = ( Player.objects
                         .filter( id=instance.id )
                         .values_list( 'score', flat=True ).first() )
       sc = ScoreRank.objects.get( score = previous_score )
       if sc.player_count == 1:
           #new hole in ranks, add -1 to other ranks to remove it:
           _ = (ScoreRank.objects
               .filter( score__gt = previous_score )
               .update( rank=F('rank') - 1 ) )
       sc.delete()
    #insert in new rank
    sc, is_new = ( ScoreRank.objects
                  .get_or_create(score=instance.score,
                                 defaults={'rank': -1,'player_count': 1,}) )
    if not is_new:
        #this score is not new: add one to player_count
        _ = ( ScoreRank.objects
             .filter( score = instance.score )
             .update( rank=F('player_count') + 1 ) )
    else:
        #this score is not new: make hole for it
        rank = ( ScoreRank.objects
                .filter( score__gt = instance.score )
                .annotate( m=Min("score" ) ) )
        new_rank = rank["m"] if rank["m"] else 1
        _ = ( ScoreRank.objects
             .filter( score__lte = new_rank )
             .update( rank=F('rank') + 1 ) )
        _ = ( ScoreRank.objects
             .filter( sc = sc.score )
             .update( rank=new_rank ) )

不要忘记将数据库操作包含在单个事务中(可序列化事务)

免责声明:未经测试。

【讨论】:

    【解决方案2】:

    从技术上讲,查询集中的每个对象都是一个 Python 对象。这意味着您可以即时为它们分配属性。在我们的例子中,我们将为每个玩家分配一个rank 属性。

    所以我编写了一个小算法来即时计算排名。该算法的工作假设是查询集按降序排序。

    players = Player.objects.order_by('-score')
    
    current_rank = 1
    counter = 0
    
    for player in players:
        if counter < 1: # for first player
            player.rank = current_rank
        else: # for other players
            if player.score == players[counter - 1].score:
                # if player and previous player have same score,
                # give them the same rank
                player.rank = current_rank
            else:
                # first update the rank
                current_rank += 1
                # then assign new rank to player
                player.rank = current_rank
        counter += 1
    

    现在在您的模板中,您可以使用 {{ player.rank }} 访问排名。

    限制:

    1. 如果给出了排名,您将无法查找玩家。虽然您仍然可以查找,例如,前 10 名 玩家等。
    2. 计算单个等级可能会很慢。例如,要知道第 1000 位玩家的排名,您需要知道之前 999 位玩家的排名。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-02-01
      • 1970-01-01
      • 2021-09-24
      • 2023-03-20
      • 2011-02-24
      • 1970-01-01
      • 2015-02-21
      • 2017-02-20
      相关资源
      最近更新 更多