【问题标题】:Performant Way To Select Most Recent Item Per Group In Django / Postgres在 Django / Postgres 中选择每个组的最新项目的高性能方法
【发布时间】:2020-01-22 22:45:26
【问题描述】:

我想在 Django 中解决一个查询性能问题。

环境:

  • Django 2.2
  • Python 3.6
  • PostgreSQL 11

示例模型:

class Location(models.Model):
    name = models.CharField(max_length=256)
    # ...

class VendingMachine(models.Model):
    location = models.ForeignKey("MyApp.Location", on_delete=models.CASCADE)
    name = models.CharField(max_length=8)
    # ...

class Vend(models.Model):
    vending_machine = models.ForeignKey("MyApp.VendingMachine", on_delete=models.PROTECT)
    vend_start_time = models.DateTimeField(db_index=True)
    # ...

我正在尝试获取每台 VendingMachine 的最新 Vends 列表。

我采用了几种方法,但它们要么在我所拥有的设置和要求中不太适用,要么执行时间太长。

版本 1:

Vend.objects.filter(pk__in=Subquery(Vend.objects.order_by().values('vendingmachine__location__id', 'vendingmachine__id').annotate(max_id=Max('id')).values('max_id')))

这个版本超级快。但是,它仅在 Vend ID 按时间顺序排列时才有效。数据是按随机顺序插入到数据库中的,所以这行不通。

版本 2:

Vend.objects.all().order_by('vendingmachine_id', '-vend_start_time').distinct('vendingmachine_id')

这个版本执行需要 12-15 秒,由于是通过分页器运行的,所以查询执行了两次(一次用于计数,第二次用于获取对象和切片),因此页面需要加载大约需要 30 秒,这太长了。
这个版本的另一个问题是结果一旦返回就无法排序(Python 除外),因为它依赖于 order_by 排序 vend_start_time 来选择最后一个。

版本 3:

vend_sub_qs = Vend.objects.filter(vendingmachine_id=OuterRef("vendingmachine_id")).order_by("-vend_start_time").values_list("id", flat=True)[:1]
vend_qs = Vend.objects.filter(pk__in=Subquery(vend_sub_qs)).order_by("-vend_start_time")
vending_machines = VendingMachine.objects.prefetch_related(Prefetch("vend_set", queryset=vend_qs))

我在这里尝试了一种不同的方法,最终得到了一个自动售货机列表,其中预取了最新的自动售货机。这效果不好,因为我确实需要以 Vends 的 QuerySet 结尾。
这也非常慢,需要大约 45 秒才能执行。

总结:

重要的是我以 Vend 对象的 QuerySet 结尾,并且可以按 Vend 上的不同字段对其进行排序。

如果这可以在 5 秒或更短的时间内执行,那就太理想了。

可以使用 Postgres 特有的 Django 函数。
原始 SQL 也是一个选项,如果最后仍然可以获得 QuerySet。

【问题讨论】:

    标签: django postgresql django-queryset


    【解决方案1】:

    我能够使用自定义 SQL 解决这个问题。
    https://docs.djangoproject.com/en/dev/topics/db/sql/#executing-custom-sql-directly

    原始 SQL:

    vends = Vend.objects.raw('SELECT * FROM "myapp_vend" WHERE (vendingmachine_id, vend_start_time) IN (SELECT vendingmachine_id, max(vend_start_time) FROM "myapp_vend" GROUP BY vendingmachine_id)')    
    

    这在 2 秒内执行并正确地为我提供了 Vend 对象的 QuerySet。
    但是,它是一个不支持 order_by 和注释调用的 RawQuerySet。由于我将 QuerySet 传递给应用排序和注释以进行表格显示的库,因此我需要一个普通的 QuerySet。

    自定义 SQL:

    with connection.cursor() as cursor:
        cursor.execute('SELECT id FROM "myapp_vend" WHERE (vendingmachine_id, vend_start_time) IN (SELECT vendingmachine_id, max(vend_start_time) FROM "myapp_vend" GROUP BY vendingmachine_id)') 
        ids = [x[0] for x in cursor.fetchall()]
    vends = Vend.objects.filter(id__in=ids)   
    

    只有选择 id 才能让我执行一个普通的 Django 过滤语句,选择自定义 SQL 返回的 id。这给了我一个普通的 QuerySet,它可以传递给添加了 order_by 和注释的库,但确实需要运行两个查询。

    【讨论】:

      【解决方案2】:

      反转获取结果的方式。意味着,而不是查询 Vend 去获取 VendingMachines 并按时间订购它的相关商品

      class VendingMachine(models.Model):
          location = models.ForeignKey("MyApp.Location", on_delete=models.CASCADE)
          name = models.CharField(max_length=8)
          # ...
      
      # Note I added 'related_name' in here
      class Vend(models.Model):
          vending_machine = models.ForeignKey("MyApp.VendingMachine", on_delete=models.PROTECT, related_name='vends')
          vend_start_time = models.DateTimeField(db_index=True)
          # ...
      

      那么您的查询应如下所示:

      vending_machines = VendingMachine.objects.prefetch_related('vends').all()
      

      然后在VendingMachineVends 上为每台机器应用您喜欢的排序和过滤器。

      例如,如果您正在迭代您的自动售货机,您会执行类似的操作

      for machine in vending_machines:
          most_recent_vends = machine.vends.order_by('-vend_start_time')
      

      【讨论】:

        猜你喜欢
        • 2011-01-13
        • 1970-01-01
        • 2011-11-24
        • 2013-06-24
        • 2017-09-27
        • 2019-10-11
        • 1970-01-01
        • 2014-10-14
        相关资源
        最近更新 更多