You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django QuerySet注解ended字段后过滤结果异常,如何排查?

Django annotate关联查询导致的计数异常问题分析与解决

问题场景

假设Django项目中有如下模型结构:

from django.db import models

class Object(models.Model):
    name = models.CharField(max_length=100)


class ObjectEvent(models.Model):
    class EventTypes(models.IntegerChoices):
        CREATED = 1, "Created"
        SCHEDULED = 2, "Scheduled"
        COMPLETED = 3, "Completed"
        CANCELED = 4, "Canceled"

    event_type = models.IntegerField(choices=EventTypes.choices)
    object = models.ForeignKey(Object, on_delete=models.CASCADE, related_name="events")

为了实现通过ended属性(定义为拥有COMPLETED或CANCELED类型事件的对象)过滤,在自定义ObjectManager中使用annotate实现:

class ObjectManager(models.Manager):
        def get_queryset(self):
            qs = super().get_queryset()
            qs = qs.annotate(
                ended=models.Case(
                    models.When(
                        events__event_type__in=(
                            ObjectEvent.EventTypes.COMPLETED,
                            ObjectEvent.EventTypes.CANCELED,
                        ),
                        then=models.Value(True),
                    ),
                    default=models.Value(False),
                    output_field=models.BooleanField(
                        verbose_name="Object ended",
                    ),
                )
            )
            return qs
            
class Object(models.Model):
    objects = ObjectManager()
    name = models.CharField(max_length=100)

使用测试数据fake_data.json(包含4个Object和9个关联Event)测试时,出现异常结果:

>>> ended_objects = Object.objects.filter(ended=True)
>>> ended_objects.count()
2 # 符合预期,对应task1和task3
>>> not_ended_objects = Object.objects.filter(ended=False)
>>> not_ended_objects.count()
7 # 不符合预期,预期应该是2(task2和task4)
>>> not_ended_objects.distinct().count()
4 # 依然不符合预期

原因分析

  1. 关联查询的左连接行为:当使用events__event_type__in作为When的条件时,Django会自动执行左连接,将每个Object与它的所有ObjectEvent关联,生成一对多的结果行。比如task1有3个Event,就会生成3条关联记录。
  2. Case语句的逐行判断逻辑:当前的annotate逻辑是针对每一条关联记录设置ended值:如果当前关联的Event是COMPLETED/CANCELED,则ended=True,否则ended=False。这意味着:
    • task1有2个非结束Event(CREATED、SCHEDULED),对应的2条关联记录ended=False;1个结束Event(CANCELED),对应1条记录ended=True。
    • task3有2个非结束Event,对应的2条记录ended=False;1个结束Event,对应1条记录ended=True。
    • task2和task4的所有Event都是非结束类型,对应的关联记录ended全为False。
  3. 过滤计数的偏差:
    • 过滤ended=False时,会筛选出所有ended为False的关联行,总数是2(task1)+1(task2)+2(task3)+2(task4)=7,这就是计数为7的原因。
    • 使用distinct()后,会对Object去重,得到4个Object(因为这4个Object都存在至少一条ended=False的关联记录),但这并不是我们需要的“没有任何结束事件的Object”。

解决方案

需要修改annotate的逻辑,让ended属性针对Object本身是否存在至少一个结束事件进行判断,而非逐行判断。以下是两种可行方案:

方案1:使用Exists子查询

利用Exists子查询直接判断当前Object是否存在符合条件的Event,返回的是针对Object的布尔值:

class ObjectManager(models.Manager):
    def get_queryset(self):
        qs = super().get_queryset()
        qs = qs.annotate(
            ended=models.Exists(
                ObjectEvent.objects.filter(
                    object=models.OuterRef('pk'),
                    event_type__in=(
                        ObjectEvent.EventTypes.COMPLETED,
                        ObjectEvent.EventTypes.CANCELED,
                    )
                )
            )
        )
        return qs

方案2:使用Count过滤统计

通过统计符合条件的Event数量,判断是否大于0来标记ended:

class ObjectManager(models.Manager):
    def get_queryset(self):
        qs = super().get_queryset()
        qs = qs.annotate(
            ended=models.Case(
                models.When(
                    models.Count(
                        'events',
                        filter=models.Q(
                            events__event_type__in=(
                                ObjectEvent.EventTypes.COMPLETED,
                                ObjectEvent.EventTypes.CANCELED,
                            )
                        )
                    ) > 0,
                    then=models.Value(True),
                ),
                default=models.Value(False),
                output_field=models.BooleanField(verbose_name="Object ended"),
            )
        )
        return qs

修改后重新测试,过滤ended=False时会得到预期的2个结果(task2和task4),计数和去重后的结果一致。

内容的提问来源于stack exchange,提问作者Zeno Dalla Valle

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 21:42:02