【问题标题】:Django: Understanding QuerySet APIDjango:了解 QuerySet API
【发布时间】:2020-04-16 16:26:00
【问题描述】:

我是 Django 的新手,我有一些关于通过 QuerySet API 进行查询的问题。

例如,我有用户、他的订单及其状态

class User(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    is_active = models.BooleanField()

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

class Order(models.Model):
    number = models.CharField(max_length=10)
    amount = models.DecimalField(max_digits=19, decimal_places=2)
    user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="orders")
    order_status = models.ForeignKey(OrderStatus, on_delete=models.PROTECT)
    creation_datetime = models.DateTimeField(auto_now_add=True)

    # Some filtering field
    filtering_field = models.IntegerField()

我将我所有的问题合并到这个查询中:

为每个用户获取一些额外数据的活跃用户:

  1. 按“filtering_field”过滤并按最小值和最大值聚合的订单的“金额”
  2. “filtering_field”过滤的第一个订单的“Number”和“Amount”
  3. 按“filtering_field”过滤、按“计数”汇总并按“订单状态”分组的订单计数。这种分组意味着来自查询 #1 和 #2 的数据可以重复,这没关系。

我可以在 T-SQL 中通过 3 个带有自己的分组、过滤、排序的独立子查询来进行此查询:

SELECT
    u.id,
    u.first_name,
    u.last_name,

    ts.min_amount,
    ts.max_amount,

    first_order.number as first_order_number,
    first_order.amount as first_order_amount,

    cnt.order_status_id,
    cnt.cnt
FROM
    [User] u

    -- 1. 'Amount' of the Orders filtered by 'filtering_field' and aggregated by Min and Max
    LEFT OUTER JOIN (
        SELECT 
            [user_id],
            MIN(amount) min_amount,
            MAX(amount) max_amount
        FROM
            [Order]
        WHERE
            filtering_field = 1
        GROUP BY
            [user_id]
    ) ts ON u.id = ts.[user_id]

    -- 2. 'Number' and 'Amount' of the first Order filtered by 'filtering_field'
    OUTER APPLY (
        SELECT TOP 1
            o.number,
            o.amount
        FROM
            [Order] o
        WHERE
            u.id = o.[user_id] AND
            o.filtering_field = 2
        ORDER BY
            o.creation_datetime

    ) first_order

    -- 3. Count of the Orders filtered by 'filtering_field', aggregated by Count and grouped by 'Order Status'.
    LEFT OUTER JOIN (
        SELECT 
            [user_id],
            order_status_id,
            COUNT(*) cnt
        FROM
            [Order]
        WHERE
            filtering_field = 3
        GROUP BY
            [user_id],
            order_status_id
     ) cnt ON u.id = cnt.[user_id]
 WHERE
    u.is_active = 1

我如何通过 QuerySet API 做同样的事情?

查询 #1 我可以在 Annotate 中做 Min 和 Max。

    data = User.objects.filter(
        Q(is_active=True)
    ).values(
        'id',
        'first_name',
        'last_name',
    ).annotate(

        min_amount=Min(
            'orders__amount', 
            filter=Q(orders__filtering_field=1)
        ),

        max_amount=Max(
            'orders__amount', 
            filter=Q(orders__filtering_field=1)
        )

    )

但是查询 #2 和 #3 呢?

我考虑过 Subquery(),但它只支持一个输出值。 我的意思是,如果你想从 1 个查询集中获取 5 个字段,sql server 会运行 5 个查询。我认为这对性能不利。

如何加入第一个订单一次以使用其字段以及如何使用 Count() 按子模型的过滤行进行分组?

我想使用 .prefetch_related() 代替 T-SQL 中的子查询,如下所示:

    Prefetch(
        'orders',
        queryset=Order.objects.filter(filtering_field=1)..., #staff with .values(), annotate(Min(), Max()) and etc.
        to_attr='pf_query_1'
    )

然后在 User.objects...values()...annotate() 中使用 'pf_query_1' 之类的 'orders__pf_query_1__amount'。

但我不能在 Prefetch 中使用 .values() 以及 'pf_query_1' 作为模型字段。

那么,通过 QuerySet API 进行这一查询的最佳实践是什么? 我想查看整个 QuerySet API 查询,就像 T-SQL 查询一样

【问题讨论】:

    标签: python django tsql


    【解决方案1】:

    您是否考虑过 docs 中描述的 Django Subquery

    关于您的第三个问题,我想到的唯一方法是动态创建注释。

    这是使用您的模型的(经过测试的)代码示例:

    def test_query(self):
    
            # 1st question
            min_order = Order.objects.filter(user=OuterRef('pk'), filtering_field=1)\
                .order_by().values('user').annotate(min=Min('amount')).values('min')
            max_order = Order.objects.filter(user=OuterRef('pk'), filtering_field=1)\
                .order_by().values('user').annotate(max=Max('amount')).values('max')
            # 2nd question
            first_number = Order.objects.filter(user=OuterRef('pk'), filtering_field=1)\
                .order_by().values('user').annotate(fnumber=F('number')).values('fnumber')
            first_amount = Order.objects.filter(user=OuterRef('pk'), filtering_field=1)\
                .order_by().values('user').annotate(fnumber=F('amount')).values('amount')
    
            kwargs = {
                'min': Subquery(min_order, output_field=DecimalField()),
                'max': Subquery(max_order, output_field=DecimalField()),
                'first_n': Subquery(first_number, output_field=CharField()),
                'first_a': Subquery(first_amount, output_field=DecimalField())
            }
    
            # 3rd question
            for o in OrderStatus.objects.all():
                kwargs['%s_count' % o.name] = \
                    Subquery(Order.objects.filter(user=OuterRef('pk'), filtering_field=1, order_status=o)\
                        .order_by().values('user').annotate(c=Count('pk')).values('c'), output_field=IntegerField())
    
            # Putting it all together
            qs2 = User.objects.annotate(**kwargs)
    
            # Testing the results
            for user in qs2:
                v = Order.objects.filter(user=user, filtering_field=1).aggregate(Min('amount'), Max('amount'))
                self.assertEqual(v['amount__min'], user.min)
                self.assertEqual(v['amount__max'], user.max)
                v = Order.objects.filter(user=user, filtering_field=1).first()
                self.assertEqual(v.number, user.first_n)
                self.assertEqual(v.amount, user.first_a)
                for o in OrderStatus.objects.all():
                    v = Order.objects.filter(user=user, filtering_field=1, order_status=o).count()
                    if v == 0:
                        v = None
                    k = '%s_count' % o.name
                    v1 = getattr(user, k)
                    self.assertEqual(v, v1)
    
            # The sql
            print(qs2.query)
    

    请注意:

    1. 该代码是 TestCase 的一部分,我将其放入其中以检查它是否有效 不出所料
    2. 我知道查询的某些部分可以在没有的情况下生成 使用聚合函数的过滤器属性的子查询。作为 此过滤器属性仅在 Django 2.0 中引入,而不是 在 LTS 版本 1.11 中支持我没用过。

    编辑:这是我想出的另一种方法,从“基本查询集”开始并对其进行注释:

    def test_query2(self):
    
            qs = Order.objects.filter(filtering_field=1).values('user', 'order_status').distinct()
    
            # 1st question
            min_order = Order.objects.filter(user=OuterRef('user'), filtering_field=1)\
                .order_by().values('user').annotate(min=Min('amount')).values('min')
            max_order = Order.objects.filter(user=OuterRef('user'), filtering_field=1)\
                .order_by().values('user').annotate(max=Max('amount')).values('max')
    
            # 2nd question
            first_number = Order.objects.filter(user=OuterRef('user'), filtering_field=1)\
                .order_by().values('user').annotate(fnumber=F('number')).values('fnumber')
            first_amount = Order.objects.filter(user=OuterRef('user'), filtering_field=1)\
                .order_by().values('user').annotate(fnumber=F('amount')).values('amount')
    
            # 3rd question
            total_count = Order.objects.filter(user=OuterRef('user'), filtering_field=1, order_status=OuterRef('order_status'))\
                .order_by().values('user').annotate(c=Count('pk')).values('c')
    
            qs2 = qs.annotate(
                min = Subquery(min_order, output_field=DecimalField()),
                max = Subquery(max_order, output_field=DecimalField()),
                first_n = Subquery(first_number, output_field=CharField()),
                first_a = Subquery(first_amount, output_field=CharField()),
                c = Subquery(total_count, output_field=IntegerField())
            )
    
    
            # Testing the results
            for d in qs2:
                v = Order.objects.filter(user=d['user'], filtering_field=1).aggregate(Min('amount'), Max('amount'))
                self.assertEqual(v['amount__min'], d['min'])
                self.assertEqual(v['amount__max'], d['max'])
                v = Order.objects.filter(user=d['user'], filtering_field=1).first()
                self.assertEqual(v.number, d['first_n'])
                self.assertEqual(v.amount, d['first_a'])
                v = Order.objects.filter(user=d['user'], filtering_field=1, order_status=d['order_status']).count()
                self.assertEqual(v, d['c'])
    
            print(qs2.query)
    

    【讨论】:

    • 感谢详细回复!我已经考虑过“子查询”,但我认为在 SELECT 区域中放置一个带有分组、过滤和聚合的繁重查询是一个坏主意,因为这个查询将为每一行运行。但似乎我弄错了。 MS SQL Server 只运行一次这样的查询。
    • 关于第三个查询:看起来不是我的意思。我不需要将每个 OrderStatus 作为单个列。我只需要两列:每个用户的“订单状态名称”和“具有此状态名称的订单数量”。因此,根据这个数据库架构,如果用户有不同状态的订单,则用户 id、first_name、last_name、min_amount、first_order_number 等将重复。像这样: [ { "user_id": 1, ... "order_status_name: "pending", "order_cnt": 2 }, { "user_id": 1, ... "order_status_name: "approved", "order_cnt": 4 } ]
    • 我已经检查了实际数据库中的性能 - 实际上 SELECT 区域中的子查询比 FROM 区域中的子查询慢(使用 LEFT JOIN)。它会影响您何时需要子查询中的几列。例如,如果您只需要 Count(),就可以了。但是如果你需要 Count、Max、Min、Avg,MS SQL Server 运行 4 个查询而不是一个
    • 任何想法如何将这两个带有 Min() 和 Max() 的查询从 SELECT 区域移动到 FROM 区域中的 1 个查询,就像在 T-SQL 中一样?
    • 我添加了一种替代方法,可以提供您期望的结果 qs。我留给你调查性能方面
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-03-30
    • 1970-01-01
    • 2014-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多