Search code examples
djangoorm

How to add global ranking annotation to subsequent Django queries?


I have a Django model that looks like this:

class LeaderboardScore(models.Model):
  score = models.FloatField()
  player_account = models.ForeignKey(PlayerAccount, on_delete=models.CASCADE)
  timestamp = models.DateTimeField()

This represents top scores in a game. The scores are ranked by score descending and timestamp ascending, so if two scores are the same, the earliest score is ranked higher.

Every time I do a query on the leaderboards, I'd like to include the global ranking of each instance in the query result. I can do that successfully when I'm querying all the scores like so:

from django.db.models import F, Window
from django.db.models.functions import DenseRank

lq = LeaderboardScore.objects.filter(leaderboard__id=1).annotate(
  rank=Window(
    expression=DenseRank(),
    order_by=[F('score').desc(), F('timestamp').asc()]
  )
)

This will correctly at a rank field with the ranking. But if I take the above query and filter it by player_account like so:

# rank will be 1, but should be the global ranking
lq.filter(player_account__id=123)[0].rank

then the ranking resets so that the first item in the query has the rank of 1, even though its global rank should be different. How do I preserve the global ranking?


Solution

  • I couldn't figure out how to do this Django's ORM, but I was able to use SQL as temporary solution:

    WITH ranked_leaderboard AS (
      SELECT
        id,
        score,
        timestamp,
        player_account_id,
        DENSE_RANK() OVER (ORDER BY score DESC, timestamp ASC) AS rank
      FROM
        leaderboards_leaderboardscore
      WHERE
        leaderboard_id = 1
    )
    SELECT
      id,
      score,
      timestamp,
      rank
    FROM
      ranked_leaderboard
    WHERE
      player_account_id = 123
    ORDER BY
      score DESC, timestamp ASC
    

    This isn't an ideal solution as it only works when I'm searching for a particular player -- I have other queries that might be composed of an arbitrary list of players, which I had use a non-optimal solution for.