优化Django查询:减少循环并实现历史记录前置状态判定
优化思路与解决方案
核心逻辑拆解
原代码的目标是:筛选**DataActionItem的历史记录中状态为'resolved'、时间在最近7天,且该历史记录的上一条记录状态不为'resolved'**的条目,最终统计这些条目对应日期的出现次数。你当前的ORM查询卡在了前置记录状态的校验环节,这里可以用Django的窗口函数直接在数据库层面完成过滤,彻底消除Python循环的开销。
完整优化代码
from django.db.models import Count, F from django.db.models.functions import TruncDate, Lag from django.utils import timezone # 定义时间范围(用timezone处理时区更严谨) today = timezone.now().date() seven_days_ago = today - timezone.timedelta(days=7) # 单次数据库查询完成所有逻辑 result = ( DataActionItem.history .filter( site_id=settings.SITE_ID, status='resolved', history_date__date__range=(seven_days_ago, today) ) # 按原DataActionItem分组,按历史时间排序,获取前一条记录的状态 .annotate( prev_status=Lag('status', partition_by=F('object_id'), order_by=F('history_date')) ) # 过滤前一条状态不是resolved的记录(第一条记录prev_status为None,也符合条件) .filter(prev_status__ne='resolved') # 按日期截断并统计每日数量 .annotate(day=TruncDate('history_date')) .values('day') .annotate(count=Count('id')) .order_by('day') )
关键逻辑说明
Lag('status', partition_by=F('object_id'), order_by=F('history_date')):通过窗口函数Lag,按每个原DataActionItem(object_id对应原item的ID)分组,按历史记录的时间顺序排序,自动获取每条记录的前一条记录的status。filter(prev_status__ne='resolved'):直接筛选出符合要求的记录——前一条状态不是'resolved',包含每个item的第一条resolved记录(此时prev_status为None,也满足过滤条件)。- 后续的
TruncDate和Count操作,直接完成原代码中取最晚日期、统计每日次数的逻辑,所有计算都在数据库层面完成,避免了Python循环和N+1查询的性能问题。
性能优势
- 原代码存在N+1查询问题:先查所有resolved的item,再循环每个item查历史记录;优化后的查询是单次数据库请求,性能大幅提升。
- 消除了Python与数据库之间的大量数据传输开销,所有过滤、统计逻辑都在数据库端执行。
内容的提问来源于stack exchange,提问作者NimrodMcTurner
相关产品推荐
相关产品推荐

