Select Django / Postgres 中每组最新项目的高效方法

Performant Way To Select Most Recent Item Per Group In Django / Postgres

我有一个查询性能问题,我正试图在 Django 中解决。

环境:

示例模型:

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 的自动售货机列表。这不是很好,因为我确实需要以 Vends 的 QuerySet 结束。
这也非常慢,大约需要 45 秒才能执行。

总结:

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

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

可以使用 Postgres 特定的 Django 函数。
RawSQL也是一个选项,如果最后还能得到一个QuerySet

我能够使用自定义解决这个问题 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 传递给一个为 table 显示应用排序和注释的库,因此我需要一个普通的 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 和注释的库,但确实需要两个查询 运行.

颠倒获取结果的方式。意思是,与其查询 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()

然后对每台机器的 VendingMachineVend 应用您的首选排序和过滤器。

例如,如果您要遍历自动售货机,您会做类似的事情

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