Django 数据库查询优化:在 filter() 中对同一 QuerySet 赋值给不同变量

0 投票
1 回答
35 浏览
提问于 2025-04-12 20:25

我有一个基础的 queryset,我的目标是利用这个基础的 queryset 来构建一个仪表盘统计数据,但需要根据每个变量的不同值进行过滤。

为了更好地理解,我简化了代码:

class Transaction(models.Model):
    """ Invoice model.
     Represents a basic income/outcome transaction. """
    
    user = models.ForeignKey(CustomUser, related_name="transactions", on_delete=models.CASCADE)
    title = models.CharField(max_length=32, verbose_name="Title")
    category = models.ForeignKey(Category, related_name="transactions", on_delete=models.CASCADE, null=True, blank=True)
    operation = models.CharField(max_length=8, choices=OPERATION_TYPE, verbose_name="operation")
    value = models.DecimalField(max_digits=14, decimal_places=2, verbose_name="value")
    date_created = models.DateTimeField(auto_now_add=True, blank=True, null=True) 

class Category(MPTTModel):
    """ Category model. 
    Represents a category where money have been spent/earned."""

    name = models.CharField(max_length=54, unique=True)
    parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name='children')

在上面的模型基础上,我创建了一个 ClassBasedView(ListView)

class DashboardView(ListView):
    """ View implements dashboard functionality. """
    model = Transaction
    template_name = "invoices/dashboard.html"
    ordering = "-date_created"


    def get_queryset(self) -> QuerySet[Any]:
        queryset = super().get_queryset()
        queryset = queryset.filter(user=self.request.user).select_related("category__parent")
        return queryset

    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
        data = super().get_context_data(**kwargs)

        # retrieving all transactons
        transactions_all = self.get_queryset()

        # retrieving incomes/expenses summary for current month
        incomes_this_month = transactions_all.filter(transaction_filter.transaction_date_filter(month="current"), operation="incomes")
        incomes_this_month_sum = incomes_this_month.aggregate(Sum("value")).get("value__sum")
        
        expenses_this_month = transactions_all.filter(transaction_filter.transaction_date_filter(month="current"), operation="expenses")                          
        expenses_this_month_sum = expenses_this_month.aggregate(Sum("value")).get("value__sum")
        
        # retrieving incomes/expenses summary for previous month
        transactions_prev_month = transactions_all.filter(transaction_filter.transaction_date_filter(month="previous"))
   
        incomes_previous_month = transactions_prev_month.filter(operation="incomes")
        incomes_previous_month_sum = incomes_previous_month.aggregate(Sum("value")).get("value__sum")

        expenses_previous_month = transactions_prev_month.filter(operation="expenses")
        expenses_previous_month_sum = expenses_previous_month.aggregate(Sum("value")).get("value__sum")

你可以看到 transaction_date_filterfilter() 管理器中。它只是用于日期过滤的 Q() 对象。

通过使用 select_related,我成功去掉了重复的数据,但仍然有一些“相似”的查询,因为 Django ORM 会单独请求查询来过滤 incomes_this_monthexpenses_this_monthincomes_previous_monthexpenses_previous_month,这些都是根据 date=operation= 字段进行的。我知道 filter() 管理器会“重置”缓存的对象,所以我在尝试寻找一种更高效的方法来获取这些数据,而不需要通过额外的查询去访问数据库。我附上了 DjDT 查询计数器的截图,希望能让问题更清楚。

非常感谢对此主题的任何建议。

在这里输入图片描述

1 个回答

0

为了把这个问题简化成一个查询,我们可以使用SQL中的GROUP_BY,这个功能可以让我们查询这个月和上个月的所有收入和支出,然后把它们分成我们想要的不同组。在Django中,我们可以通过在查询集上调用.values()来实现。传给.values()的参数是我们想用来分组的属性。在你的情况下,我们想根据交易的月份和操作类型来分组数据。

transactions_all.values("month", "operation")

不过在这个查询能够正常工作之前,我们需要对查询集进行.annotate(),这样“月份”这个属性才能在每笔交易中存在。我不太清楚你的transaction_filter.transaction_date_filter是怎么工作的,所以我会写一个自己的注解。你可能可以调整你现有的日期过滤器来适应这个需求。我会使用ExtractMonth来从date_created字段中提取出月份的值。

from django.db.models.functions import ExtractMonth

transactions_all.annotate(month=ExtractMonth("date_created"))

现在,带注解的查询集有一个问题,就是交易的年份根本没有考虑进去,所以我们会得到这个月的所有交易,但也包括去年和前年同一个月的交易(每年的三月,而不仅仅是今年的三月)。我们可以通过在查询集中添加一个过滤器来解决这个问题,以确保我们只查询最近两个月的数据。

transactions_all.filter(date_created__range=[beginning_of_last_month, end_of_this_month])

最后要添加到查询集的是一个最终的.annotate(),它会对每个组的交易值进行求和。这个.annotate()替代了你在查询中使用的.aggregate(),但它的作用是一样的,都是用来求和交易的值。我们需要这样做,因为我们的结果有多个对象,所以我们使用.annotate()为查询集中的每个对象添加一个属性。使用.aggregate()会把查询集的结果压缩成一个单一对象,这样就会破坏我们之前的分组。

transactions_all.filter(
        date_created__range=[beginning_of_last_month, end_of_this_month]
    ).annotate(
        month=ExtractMonth("date_created")
    ).values(
        "month", "operation"
    ).annotate(
        value_sum=Sum("value")
    )

这样就能在一个SQL查询中获取到我们需要的所有数据,输出结果看起来像这样:

<QuerySet [
    {'operation': 'expenses', 'month': 2, 'value_sum': Decimal('750')},
    {'operation': 'expenses', 'month': 3, 'value_sum': Decimal('750')},
    {'operation': 'incomes', 'month': 2, 'value_sum': Decimal('240')},
    {'operation': 'incomes', 'month': 3, 'value_sum': Decimal('350')}
]>

撰写回答