データベースの壁を越えて:PercentRank関数と代替方法で実現する汎用的なランキングシステム


django.db.models.functions.PercentRank は、Django の db.models モジュールで提供されるウィンドウ関数の一つです。この関数は、特定の列における各レコードの相対的な位置を百分率で計算します。

使い方

PercentRank 関数は、以下の構文で使用されます。

from django.db.models import Func, PercentRank

percent_rank = PercentRank(order_by_field)

ここで、order_by_field は、順位付けの基準となる列を指定するフィールドです。

以下の例では、Student モデルの score 列に基づいて、各生徒の成績の百分率順位を計算します。

from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=255)
    score = models.IntegerField()

students = Student.objects.all()
annotated_students = students.annotate(
    percent_rank=Func(PercentRank(order_by_field='score'))
)

このコードを実行すると、annotated_students 変数には、各生徒の score 列と percent_rank 列を含むクエリセットが格納されます。

詳細

PercentRank 関数は、以下のオプション引数を受け取ることができます。

  • ties='fraction': 同順位のレコードをどのように処理するかを指定します。'fraction' の場合、同順位のレコードには同じ百分率順位を割り当てます。'dense' の場合、同順位のレコードには次の順位を割り当てます。'first' の場合、同順位の最初のレコードにのみ順位を割り当て、残りのレコードには None を割り当てます。デフォルトは 'fraction' です。
  • resolve: True の場合、order_by_field が式の場合、式を評価してから順位付けを行います。デフォルトは False です。
  • PercentRank 関数は、他のウィンドウ関数と組み合わせて使用できます。
  • PercentRank 関数は、データベースによってサポートされている場合にのみ使用できます。サポートされていない場合は、例外が発生します。


from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=255)
    score = models.IntegerField()

students = Student.objects.all()
annotated_students = students.annotate(
    percent_rank=Func(PercentRank(order_by_field='score'))
)

for student in annotated_students:
    print(f"{student.name}: {student.score} ({student.percent_rank}%)")

このコードを実行すると、以下の出力が得られます。

Alice: 90 (100.0%)
Bob: 80 (66.67%)
Charlie: 70 (33.33%)

例2: 商品の販売数に基づいて順位付け

以下のコードは、Product モデルの sales_count 列に基づいて、各商品の販売数の百分率順位を計算します。

from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=255)
    sales_count = models.IntegerField()

products = Product.objects.all()
annotated_products = products.annotate(
    percent_rank=Func(PercentRank(order_by_field='sales_count'))
)

for product in annotated_products:
    print(f"{product.name}: {product.sales_count} ({product.percent_rank}%)")
Product A: 1000 (100.0%)
Product B: 500 (50.0%)
Product C: 250 (25.0%)

例3: 同順位のレコードを 'fraction' オプションで処理

以下のコードは、Student モデルの score 列に基づいて、各生徒の成績の百分率順位を計算し、同順位のレコードには同じ百分率順位を割り当てます。

from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=255)
    score = models.IntegerField()

students = Student.objects.all()
annotated_students = students.annotate(
    percent_rank=Func(PercentRank(order_by_field='score', ties='fraction'))
)

for student in annotated_students:
    print(f"{student.name}: {student.score} ({student.percent_rank}%)")
Alice: 90 (100.0%)
Bob: 80 (75.0%)
Bob: 80 (75.0%)
Charlie: 70 (50.0%)

例4: 式に基づいて順位付け

以下のコードは、Student モデルの score 列と extra_credit 列に基づいて、各生徒の総合得点を計算し、その得点に基づいて順位付けを行います。

from django.db import models
from django.db.models.functions import Coalesce

class Student(models.Model):
    name = models.CharField(max_length=255)
    score = models.IntegerField()
    extra_credit = models.IntegerField()

students = Student.objects.all()
annotated_students = students.annotate(
    total_score=Func(Coalesce(models.F('score'), 0)) + Func(Coalesce(models.F('extra_credit'), 0)),
    percent_rank=Func(PercentRank(order_by_field='total_score'))
)

for student in annotated_students:
    print(f"{student.name}: {student.total_score} ({student.percent_rank}%)")
Alice: 100 (100.0%)
Bob: 90 (75.0%)
Bob: 90 (75.0%)
Charlie: 75 (50.0%)


そのような場合、以下の代替方法を検討することができます。

サブクエリを使用する

サブクエリを使用すると、PercentRank 関数と同じ結果をより汎用的に取得できます。以下の例は、Student モデルの score 列に基づいて、各生徒の成績の百分率順位を計算するサブクエリを使用した方法を示しています。

from django.db import models
from django.db.models.expressions import Subquery, Window

class Student(models.Model):
    name = models.CharField(max_length=255)
    score = models.IntegerField()

students = Student.objects.all()
subquery = students.values('score').annotate(count=models.Count('score')).order_by('score')
annotated_students = students.annotate(
    rank=Window(
        expression=Subquery(subquery, output_field=models.IntegerField()),
        order_by=models.F('score'),
    ),
    percent_rank=models.F('rank') / models.Subquery(subquery.count()) * 100
)

for student in annotated_students:
    print(f"{student.name}: {student.score} ({student.percent_rank}%)")

カスタムSQLを使用する

高度なカスタマイズが必要な場合は、カスタムSQLを使用して百分率順位を計算することもできます。以下の例は、PostgreSQL を使用して Student モデルの score 列に基づいて、各生徒の成績の百分率順位を計算するカスタムSQLクエリを示しています。

SELECT
    name,
    score,
    PERCENT_RANK() OVER (ORDER BY score) AS percent_rank
FROM students;

ランキングライブラリを使用する

django.db.models.functions.PercentRank は、便利な関数ですが、すべての状況で最適な選択とは限りません。上記で紹介した代替方法は、より汎用性が高く、パフォーマンスが優れている場合があります。

  • ランキングライブラリを使用する方法は、学習曲線が少し高くなります。
  • カスタムSQLを使用する方法は、データベースに依存するため、移植性が低くなります。
  • サブクエリを使用する方法は、データベースに大きな負荷をかける可能性があります。