
本文介绍如何在 django orm 中模拟 sql 的 group by 行为,通过 annotate() + concat 构建唯一键对订单按收发地址分组,并为每组统一关联账单(bill),避免 n+1 查询和手动循环判断。
本文介绍如何在 django orm 中模拟 sql 的 group by 行为,通过 annotate() + concat 构建唯一键对订单按收发地址分组,并为每组统一关联账单(bill),避免 n+1 查询和手动循环判断。
在 Django ORM 中,原生不支持类似 SQL 的 .group_by() 链式调用(如 queryset.group_by('field1', 'field2')),但可通过组合 annotate()、聚合函数与字符串拼接等技巧,实现逻辑上的“分组”效果。核心思路是:构造一个能唯一标识目标分组的计算字段(如 address_from_id-address_to_id),再基于该字段去重、遍历并批量操作。
以下是以 Order 模型为例的完整实践方案:
from django.db.models import CharField, Value
from django.db.models.functions import Concat
from myapp import models
# 步骤 1:为每个 Order 注入合成的唯一分组键(格式:'from_id-to_id')
orders = models.Order.objects.annotate(
addresses_uk=Concat(
'address_from__pk',
Value('-'),
'address_to__pk',
output_field=CharField()
)
)
# 步骤 2:提取所有唯一分组键
unique_keys = set(orders.values_list('addresses_uk', flat=True))
# 步骤 3:对每个分组创建对应 Bill,并批量更新 Order 关联
for uk in unique_keys:
from_id, to_id = map(int, uk.split('-'))
# 创建新 Bill(根据业务逻辑,此处可替换为 get_or_create)
bill = models.Bill.objects.create(
address_from_id=from_id,
address_to_id=to_id,
# 其他必要字段(如 created_at、status 等)
)
# 批量更新该分组内所有 Order 的 bill 字段(高效,仅 1 次 UPDATE SQL)
models.Order.objects.filter(addresses_uk=uk).update(bill=bill)
✅ 关键优势说明:
- 无 N+1 查询:values_list(..., flat=True) 和 filter(...).update(...) 均为纯数据库操作,全程不实例化模型对象;
- 原子性保障:每个 update() 是独立事务安全的批量更新;
- 可扩展性强:若需按更多字段分组(如加 service_type),只需在 Concat 中追加字段即可。
⚠️ 注意事项:
- 确保 address_from 和 address_to 字段非空(或在 annotate 前用 filter(address_from__isnull=False, address_to__isnull=False) 排除异常数据);
- 若 pk 类型非整数(如 UUID),应改用 Cast 或 ToString 函数转换,或直接拼接 address_from__id 字符串值(需确保数据库支持);
- 生产环境建议添加事务包裹(transaction.atomic()),尤其当 Bill 创建与 Order 更新需强一致性时;
- 避免在循环中调用 models.Address.objects.get(...) —— 示例中已优化为直接使用 address_from_id 赋值,大幅提升性能。
该方法本质是「以空间换逻辑清晰度」:用轻量级字符串键替代复杂嵌套查询,兼顾可读性、性能与 Django ORM 最佳实践。











