【问题标题】:Most efficient way to build list of highest prices from queryset?从查询集中构建最高价格列表的最有效方法?
【发布时间】:2017-04-12 12:13:54
【问题描述】:

在我的应用程序的一个页面中,我试图显示每家公司最昂贵的汽车。我的模型大致如下:

class Company(models.Model):
    id = models.IntegerField(primary_key=True)
    company = models.CharField(max_length=100)
    headcount = models.IntegerField(null=False)
    info = models.CharField(max_length=100)

class Car(models.Model):
    id = models.IntegerField(primary_key=True)
    company_unique = models.ForeignKey(Company)
    company = models.CharField(max_length=50)
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=9, decimal_places=2, default=0.00)

所以,我想建立一个包含每家公司最昂贵的汽车对象的列表。

我是这样处理问题的:

company_list = Company.objects.all()
most_expensive = []
for company in company_list:
    most_expensive.append(Car.objects.filter(company_unique=company.id).order_by("-price")[0])

但是,这似乎是一种非常低效的方法。我可以通过 Django 调试工具栏看到这段代码产生了太多的 mysql 查询。

有人可以提出一个更好的方法来构建这个可能只影响 MySQL 一次或两次的列表吗?

【问题讨论】:

标签: python mysql django django-queryset django-orm


【解决方案1】:

虽然您所处理的情况很常见,但似乎缺乏明显的解决方案。

解决方案 1,位于 this article。您可能可以尝试以下方式:

companies = Company.objects.annotate(max_price=Max('car__price'))
values = tuple((company.id, company.max_price) for company in companies)

expensive_cars = Car.objects.extra(where=['(company_unique_id, price) IN %s' % (values,)])

不能说我喜欢这个解决方案 - 应该避免使用 .extra - 但我想不出更好的方法。我也不完全确定这是否会起作用。

解决方案 2,次优。您可以使用custom Prefetch object

prefetch = Prefetch('cars', queryset=Car.objects.order_by('-price'), to_attr='cars_by_price')
companies = Company.objects.prefetch_related(prefetch)

most_expensive_cars = []
for company in companies:
    most_expensive_cars.append(list(company.cars_by_price.all())[0])

这绝对可以工作并在两个查询中获取所有内容,但是非常浪费,因为它将与给定的Companies 集合相关的所有Cars 加载到内存中。请注意list() 部分不是可选的:无论您采用切片或索引,都会复制查询集并生成单独的数据库查询,因此否定预取,而实例化列表将使用所述预取的结果。

如果您之后需要访问公司,例如 Car.company,请不要回避使用 select_related,正如 Erik 在 cmets 中所建议的那样。

【讨论】:

  • 我认为解决方案 1 是我要走的路。就我而言,我发现我必须将这两个值转换为字符串,例如:values = tuple(( str(company.id), str(company.max_price)) for company in companies) ...否则我会收到 MySQL 错误。
【解决方案2】:

向公司添加一个名为“priciest_car”的字段并覆盖保存,这样每次保存公司时,您都会遍历它的相关汽车并将最昂贵的汽车设置为 priciest_car。然后,当您需要为每家公司调用最昂贵的汽车时,您可以遍历每家公司并将company.priciest_car 添加到列表中。这是一个循环,每次迭代一个 sql 调用。唯一的额外工作是当你拯救一家公司时,但它会是每家公司,所以它不应该花太长时间。如果是这样,请找到一种方法,使其仅在您知道已更改时才设置“priciest_car”字段。

【讨论】:

    【解决方案3】:

    我发誓这就是我能够处理它的方式,但看来我一定是弄错了。

    我认为 Aggregation 可以做到这一点:

    most_expensive = Car.objects.values('company_unique').annotate(Max('price'))
    

    以下是原始SQL,有它的好处,但我觉得可能有更干净的方式:

    from django.db import connection
    
    cursor = connection.cursor()
    cursor.execute("SELECT Max(price), company_unique FROM Car GROUP BY company_unique");
    price_company = cursor.fetchall()
    
    # This still does one query per car, only it fetches one item at a time.
    most_expensive = [Cars.objects.get(price=pc[0],company_unique=pc[1]) 
                        for pc in price_company]
    

    如果您真的想将其限制为一个查询,那么您可以利用raw

    most_expensive = Cars.objects.raw("""
       SELECT * FROM Cars 
         INNER JOIN 
           (SELECT Max(price) as price, company_unique FROM Car GROUP BY company_unique) m 
           ON m.price = Cars.price, m.company_unique = Cars.company_unique
    """)
    

    使用raw 的问题在于它与数据库无关,因此任何重构都需要重新编写此查询。 (例如,Oracle 有不同的辅助查询语法)。

    我觉得我应该指出SELECT Max(price) as price, company_unique FROM Car GROUP BY company_unique 查询无论如何都会被执行——如果您使用的是更原生的 Django 解决方案,它会在幕后发生。

    【讨论】:

    • 该查询集似乎不再由 Car 对象组成。至少,它不再具有 Car 对象的所有字段,例如“名称”。当我将它传递到我的模板中时,会使用正确的行数构建一个表格,但每个单元格都是空的。
    • 这是一个不同的问题。您的模板中可能有错误(未包含在问题中,因此此处不相关)这确实回答了问题
    • 我看不出这如何以任何方式回答这个问题。如果您阅读 OP 给出的示例,很明显他希望获得 Car 实例,而不仅仅是最高价格。
    猜你喜欢
    • 2019-08-25
    • 2012-10-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-06
    • 1970-01-01
    • 1970-01-01
    • 2013-07-21
    相关资源
    最近更新 更多