aggregate()返回字典并立即执行SQL,annotate()返回可链式操作的QuerySet;需防Count重复计数、空值返回None及字段名冲突。

用 aggregate() 计算单个或多个聚合值
直接调用 aggregate() 是最常用方式,它返回一个字典,键是你指定的别名,值是计算结果。注意它不返回 QuerySet,而是立即执行 SQL 并返回 Python 值。
常见错误是误以为 aggregate() 会链式调用(比如后面接 filter()),其实它一调就执行了,不能再接其他 QuerySet 方法。
- 支持标准聚合函数:
Count、Sum、Avg、Min、Max,需从django.db.models导入 - 别名不能和模型字段名冲突,否则可能被静默覆盖(尤其用
*导入时) - 对空 QuerySet,默认返回
None(如Avg),不是 0;可用Coalesce包裹处理
from django.db.models import Sum, Avg, Count
Order.objects.aggregate(
total_amount=Sum('amount'),
avg_amount=Avg('amount'),
order_count=Count('id')
)
# → {'total_amount': 1250.0, 'avg_amount': 250.0, 'order_count': 5}用 annotate() 为每条记录添加聚合字段
annotate() 和 aggregate() 完全不同:它在 QuerySet 每行上“加一列”,返回的是可继续操作的 QuerySet,适合分组统计或关联数据聚合。
典型陷阱是忘记加 group_by 就直接 annotate() —— 如果没显式分组,Django 默认按所有 SELECT 字段(含主键)分组,结果往往比预期多出大量重复行。
立即学习“Python免费学习笔记(深入)”;
- 必须配合
values()或values_list()显式指定分组维度,否则行为不可控 - 跨关系聚合(如统计用户订单数)直接写
user__order__id即可,Django 自动 JOIN - 聚合字段名若与原有字段同名,会覆盖原字段值(不是报错)
# 每个用户的订单总数和总金额
User.objects.values('id', 'name').annotate(
order_count=Count('order__id'),
total_spent=Sum('order__amount')
)避免 Count 重复计数导致的数值膨胀
当一次查询中对多个外键做 Count(比如同时统计用户订单数和收藏数),Django 默认会生成笛卡尔积 JOIN,造成计数值虚高——这是最常被忽略的性能与逻辑双坑。
根本原因是 SQL 中多个 LEFT JOIN 导致行数倍增,而 Count 统计的是行数,不是真实关联对象数。
- 解决方案:对每个
Count加distinct=True,例如Count('order', distinct=True) - 更高效的做法是拆成子查询(
Subquery+OuterRef),尤其当聚合逻辑复杂时 - 用
prefetch_related+ Python 端统计仅适用于小数据集,大数据量下 N+1 和内存开销明显
聚合结果为空时的默认值处理
Django 的聚合函数遇到空数据时行为不一致:Count 返回 0,但 Sum、Avg、Min、Max 都返回 None。这在模板或 API 返回中容易引发 TypeError。
不能靠 Python 层 or 0 简单兜底,因为数据库层面 NULL 参与运算仍得 NULL(如 Sum + 100 = NULL)。
- 统一用
Coalesce包裹聚合表达式,例如Coalesce(Sum('amount'), 0) -
Coalesce第二个参数必须是同类型常量(0对应数字,""对应字符串) - 在
annotate()中使用Coalesce不影响分组逻辑,是最安全的兜底方式
from django.db.models import Coalesce, Sum, IntegerField
User.objects.annotate(
total=Coalesce(Sum('order__amount'), 0, output_field=IntegerField())
)聚合本身不难,难的是搞清 aggregate 和 annotate 的语义边界、JOIN 引发的计数失真,以及空值在数据库和 Python 层的双重表现。这些地方一旦写错,结果可能完全偏离预期,且不易通过单元测试覆盖到。


















