![]()
去年有个做健身App的朋友跟我吐槽,说他们 leaderboard 功能上线第一天就崩了。用户量才5万,查询排名平均耗时800毫秒,高峰期直接超时。问题出在哪?他们用了一个"看起来对"的方案——先查用户分数,再数有多少人比他高。这在测试环境跑10个用户当然没问题,但生产环境就是另一回事了。
这种场景太常见了。排行榜、积分系统、竞技匹配,几乎每个产品都会遇到"算排名"的需求。Django ORM 给了你好几种实现方式,但它们的性能差距能达到100倍以上。今天把三种主流方案拆开看,包括一个很多人没注意到的 Django 2.0 隐藏武器。
方案一:窗口函数,Django 2.0 埋了3年的大招
这是唯一能在单条 SQL 里搞定全表排名的方案。
Django 2.0 引入的 Window 函数,本质上是在 ORM 层封装了 SQL 的窗口函数。写法很干净:
from django.db.models import F, Window
from django.db.models.functions import Rank
users_with_rank = User.objects.annotate(
rank=Window(
expression=Rank(),
order_by=F('points').desc()
)
)
Window 类告诉 Django 生成 OVER 子句,Rank() 处理排名逻辑,F('points').desc() 确保分数高的排前面。整条查询只访问一次数据库,返回结果里每个用户都带上了 rank 字段。
Rank() 的计分规则跟标准 SQL 一致:并列分数共享同一名次,下一名次跳过。比如 Alice 和 Bob 都是200分,并列第1,Carol 180分直接跳到第3。如果你不希望跳号,把 Rank() 换成 DenseRank(),Carol 就会变成第2名。
![]()
窗口函数在 PostgreSQL、MySQL 8.0+、SQLite 3.25+ 都支持,但有个坑:旧版 MySQL 5.7 会报错。如果你的项目还在用古董数据库,这方案直接排除。
方案二:计数法,新手陷阱
这个方案的思路很直观:数一下有多少人分数比你高,再加1就是你的排名。
specific_user = User.objects.get(id=2)
user_rank = User.objects.filter(points__gt=specific_user.points).count() + 1
代码确实好懂,不需要 import 额外的东西。但代价是两次数据库往返——先取用户,再计数。查一个人的排名还好,如果要在 leaderboard 页面展示前50名,你得循环查50次,变成51条查询。
更隐蔽的问题是并列处理。假设 Alice 和 Bob 同分,先查 Alice 的排名时,系统认为有0人比她高,排第1;查 Bob 时,Alice 已经被算进去了,Bob 变成第2。同一个分数出现两个排名,用户看到会懵。
我那个做健身App的朋友用的就是这个方案。5万用户时,单条排名查询要扫描半张表,索引都救不了。
方案三:子查询,折中但啰嗦
用子查询可以在单条 SQL 里算出单个用户的排名,比计数法少一次往返:
![]()
from django.db.models import OuterRef, Subquery, Count
user_rank = User.objects.filter(
points__gt=OuterRef('points')
).values('points').annotate(cnt=Count('pk')).values('cnt')
User.objects.annotate(rank=user_rank + 1)
但子查询的写法明显更绕,而且数据库优化器不一定会把它处理得高效。PostgreSQL 通常能优化好,MySQL 的表现在版本差异很大。窗口函数能做的事,没必要用子查询硬凑。
生产环境怎么选
直接给结论:
• 数据库支持窗口函数 → 无脑用方案一,Rank() 或 DenseRank() 看业务需求
• 困在 MySQL 5.7 或更老的版本 → 考虑升级数据库,比改代码划算
• 实在动不了数据库 → 方案二加缓存,但得接受并列排名的混乱
有个细节很多人忽略:窗口函数返回的是查询集,你可以继续链式操作。比如只取前100名、按地区过滤、或者把排名和用户信息一起序列化,都不会触发额外查询。
Django 官方文档对 Window 函数的示例很少,Stack Overflow 上相关讨论也不多。这功能从2.0到现在已经6年了,但据我观察,实际项目中使用率不到三成。可能是名字起得太技术,很多人看到 Window 就跳过了。
你现在的项目里,排名功能用的是哪种方案?如果今天重写,你会换吗?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.