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

使用Django Q查询筛选首次预订客户ID失败求助

问题:Django Q查询筛选指定年月首次完成预订客户ID失败

我尝试用Django的Q查询筛选指定年月内首次完成预订的客户ID,编写了如下函数:

def get_first_time_customer_ids(year: int, month: int) -> QuerySet:
    return Customer.objects.filter(
        Q(bookings__status=Booking.STATUS.completed, bookings__pickup_time__year=year, bookings__pickup_time__month=month) &
        ~Q(bookings__status=Booking.STATUS.completed, bookings__pickup_time__lt=date(year, month, 1))
        ).distinct().values_list('id', flat=True)

但测试用例执行失败:

def test_get_first_time_customer_ids(self) -> None:
        customer_1 = Customer.objects.create(name="Customer 1")
        customer_2 = Customer.objects.create(name="Customer 2")
        Booking.objects.bulk_create([
            Booking(number="A", customer=customer_1, price=100_000, status=Booking.STATUS.completed,
                    pickup_time=dt(2023, 2, 4, 12), route_id=1, vehicle_category_id=1),
            Booking(number="B", customer=customer_1, price=100_000, status=Booking.STATUS.completed,
                    pickup_time=dt(2023, 1, 5, 12), route_id=1, vehicle_category_id=1),
            Booking(number="E", customer=customer_2, price=100_000, status=Booking.STATUS.completed,
                    pickup_time=dt(2023, 2, 10, 12), route_id=1, vehicle_category_id=1)
        ])        
        ids = get_first_time_customer_ids(2023, 2)
      
        self.assertTrue(customer_2.id in ids)
        self.assertFalse(customer_1.id in ids)

预期customer_1不应出现在2023年2月的首次预订客户列表中,但实际被包含,请求排查问题。


问题原因

你的Q查询逻辑存在漏洞:当客户同时拥有指定年月的完成预订和更早的完成预订时,Django的关联查询会将客户匹配到第一个Q条件(存在2月的完成预订),而第二个~Q条件的判断逻辑并不是“该客户没有任何更早的完成预订”——实际上,这个写法会生成两次独立的关联查询(JOIN),只要存在一个预订满足第一个Q,且存在一个预订不满足第二个Q的条件(比如2月的预订本身就不早于2月1日),就会保留该客户,这完全不符合“首次预订在指定年月”的需求。


解决方案

正确的思路是:先为每个客户计算其最早的完成预订时间,再筛选这个最早时间落在指定年月内的客户。可以通过annotate结合Min聚合函数实现:

from django.db.models import Min, Q
from datetime import date

def get_first_time_customer_ids(year: int, month: int) -> QuerySet:
    # 计算指定年月的起始和结束日期
    start_date = date(year, month, 1)
    # 获取下一个月的第一天,作为结束边界(不包含)
    if month == 12:
        end_date = date(year + 1, 1, 1)
    else:
        end_date = date(year, month + 1, 1)
    
    return Customer.objects.annotate(
        first_completed_booking=Min(
            'bookings__pickup_time',
            filter=Q(bookings__status=Booking.STATUS.completed)
        )
    ).filter(
        first_completed_booking__gte=start_date,
        first_completed_booking__lt=end_date
    ).values_list('id', flat=True)

逻辑说明:

  1. annotate(first_completed_booking=Min(...)):为每个客户计算其所有完成状态预订的最早取车时间。
  2. filter条件确保这个最早时间落在指定年月的区间内(start_date <= 时间 < end_date),这样就能精准筛选出首次完成预订在该年月的客户。
  3. 这种写法避免了多关联查询的逻辑漏洞,结果准确且性能更优。

内容的提问来源于stack exchange,提问作者Andi Fathul Mukminin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 17:35:14