
本文介绍在 Django 中模拟 SQL GROUP BY 的实用技巧,通过 annotate + Concat 构建复合分组键,对具有相同 address_from 和 address_to 的订单进行分组,并为每组统一关联新创建的 Bill 实例。
本文介绍在 django 中模拟 sql group by 的实用技巧,通过 annotate + concat 构建复合分组键,对具有相同 `address_from` 和 `address_to` 的订单进行分组,并为每组统一关联新创建的 bill 实例。
Django ORM 本身不支持原生 .group_by() 方法(如 SQL 中的 GROUP BY),也不允许直接对 QuerySet 调用类似 group_by('field1', 'field2') 的链式调用。但可通过 annotate() 配合数据库函数(如 Concat)构造唯一标识符,间接实现逻辑分组。
以下是一个典型场景:将所有 address_from 和 address_to 完全相同的订单归为一组,并为该组创建一个共享的 Bill 实例,再批量更新其 bill 外键字段。
✅ 正确实现方式(推荐)
from django.db.models import CharField, Value
from django.db.models.functions import Concat
# 步骤 1:为每个 Order 注入由 address_from_id 和 address_to_id 拼接而成的唯一键
orders = 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 并批量更新
for uk in unique_keys:
from_id, to_id = map(int, uk.split('-'))
# 创建新 Bill(需确保 Address 存在,生产环境建议加 try/except)
bill = Bill.objects.create(
address_from_id=from_id,
address_to_id=to_id,
# 其他必要字段(如 created_at、status 等)
)
# 批量更新当前分组内所有 Order 的 bill 字段(高效,仅一次 UPDATE SQL)
Order.objects.filter(addresses_uk=uk).update(bill=bill)
⚠️ 注意事项与最佳实践
- 性能优化:使用 .update() 而非 .save() 可避免逐条查询+更新,显著提升大批量操作效率;
- 原子性考虑:若业务要求强一致性(如 Bill 创建与 Order 关联必须同时成功),建议包裹在 transaction.atomic() 中;
- 空值处理:本例假设 address_from 和 address_to 均为非空(null=False)。若允许为空,需额外处理 NULL 值(例如用 Coalesce 替代 __pk,或过滤掉空地址);
- 索引建议:为 address_from_id 和 address_to_id 字段建立联合索引(db_index=True 或通过 Meta.indexes),加速 filter(address_from_id=x, address_to_id=y) 查询;
- 替代方案:若仅需统计或聚合(如每组订单数),可直接使用 values('address_from', 'address_to').annotate(count=Count('id')),无需拼接字符串键。
该方法兼顾可读性、可维护性与执行效率,是 Django 场景下实现“语义化分组”的成熟实践。











