Django ORM 对 QuerySet 的迭代真的很慢

Posted

技术标签:

【中文标题】Django ORM 对 QuerySet 的迭代真的很慢【英文标题】:Django ORM really slow iterating over QuerySet 【发布时间】:2019-11-24 04:26:26 【问题描述】:

我正在处理一个新项目,并且必须非常快速地构建几页的大纲。

我导入了一个包含 280k 产品的目录,我想通过这些产品进行搜索。我选择了 Whoosh 和 Haystack 来提供搜索,因为我在以前的项目中使用它们。 我为索引添加了定义并开始了该过程。但是,似乎 Django 对 QuerySet 进行迭代真的非常非常非常慢。 最初,我认为索引需要超过 24 小时 - 这似乎很荒谬,所以我测试了其他一些东西。我现在可以确认,遍历 QuerySet 需要很多小时。

也许在 Django 2.2 中有一些我不习惯的东西?我以前用过 1.11,但我想我现在用的是更新的版本。

我正在尝试迭代的模型:

class SupplierSkus(models.Model):
    sku = models.CharField(max_length=20)
    link = models.CharField(max_length=4096)
    price = models.FloatField()
    last_updated = models.DateTimeField("Date Updated", null=True, auto_now=True)
    status = models.ForeignKey(Status, on_delete=models.PROTECT, default=1)
    category = models.CharField(max_length=1024)
    family = models.CharField(max_length=20)
    family_desc = models.TextField(null=True)
    family_name = models.CharField(max_length=250)
    product_name = models.CharField(max_length=250)
    was_price = models.FloatField(null=True)
    vat_rate = models.FloatField(null=True)
    lead_from = models.IntegerField(null=True)
    lead_to = models.IntegerField(null=True)
    deliv_cost = models.FloatField(null=True)
    prod_desc = models.TextField(null=True)
    attributes = models.TextField(null=True)
    brand = models.TextField(null=True)
    mpn = models.CharField(max_length=50, null=True)
    ean = models.CharField(max_length=15, null=True)
    supplier = models.ForeignKey(Suppliers, on_delete=models.PROTECT)

而且,正如我所提到的,该表中大约有 280k 行。

当我做一些简单的事情时:

from products.models import SupplierSkus
sku_list = SupplierSkus.objects.all()
len(sku_list)

该过程将很快消耗大部分 CPU 资源并且无法完成。同样,我无法对其进行迭代:

for i in sku_list:
    print(i.sku)

也只需要几个小时而不打印一行。但是,我可以使用以下方法对其进行迭代:

for i in sku_list.iterator():
    print(i.sku)

这对我帮助不大,因为我仍然需要通过 Haystack 进行索引,我相信这些问题是相关的。

在我参与过的一些早期项目中,情况并非如此。即使是更大的列表(3-5m 行)也会很快迭代。查询列表长度需要一点时间,但会以秒而不是小时返回结果。

所以,我想知道,发生了什么事? 这是别人遇到过的吗?

【问题讨论】:

Python 不擅长迭代 100k+ 次。通常 100k+ 意味着处理时间将以秒为单位。然而,打印 all 280k+ sku 的可能性很小。通常你应该能够过滤? 所以,我使用的是 Python3 - 忘了提。我实际上并不介意“秒”,我介意“天”。这里有些不对劲。我刚刚在 Django 1.9.2 中再次尝试了同样的方法,结果大不相同。有了更长的 QuerySet,Python 几乎立即开始迭代。但在 2.2 或 2.0 中根本没有迭代。 您是否通过过滤相关模型加入查询集中?如果最后加上.distinct()呢? 没有@WillemVanOnsem。我绝对没有过滤。正如您在我的示例中看到的那样,没有加入,没有相关数据。我也不认为迭代超过 280k 行有那么多工作要做。我经常迭代更大的查询集。我也非常喜欢做类似的事情: list(SupplierSkus.objects.all()) 立即将 QS 变成一个列表。这在现在看来是不可能的。我的 16 GB RAM,2.9Ghz i5 仍然占用 len(sku_list) 几个小时,而我的 4GB RAM,1.3 Ghz i5 在不到 5 分钟的时间内就已经超过了一个更大的集合。我不确定它是否与 Django 1.9 vs 2.2 相关? 您可能希望为此使用类似于 Paginator 的东西。我相信与 Django 一起打包的那个很可能足以满足您的需求,但是如果这不起作用,只需获取总数并将其分成几块,就可以很容易地自己创建一个几千。如果您需要一些代码,请告诉我。 EDIT: Just found something that would help! 【参考方案1】:

好的,我发现问题出在 Python mysql 驱动程序上。如果不使用.iterator() 方法,for 循环将卡在 QuerySet 中的最后一个元素上。我在an expanded question here 上发布了更详细的答案。

我没有使用 Django 推荐的 mysqlclient。我正在使用 由 Oracle/MySQL 创建的。似乎有一个错误导致 迭代器“卡”在 for 中 QuerySet 的最后一个元素上 循环并在某些情况下陷入无限循环。

仔细想想,这很可能是 MySQL 驱动的一个设计特性。我记得以前这个驱动程序的 Java 版本也有类似的问题。也许我应该放弃 MySQL 并转向 PostgreSQL?

无论如何,我都会尝试向 Oracle 提出一个错误。

【讨论】:

以上是关于Django ORM 对 QuerySet 的迭代真的很慢的主要内容,如果未能解决你的问题,请参考以下文章

Django框架详细介绍---ORM相关操作---select_related和prefetch_related函数对 QuerySet 查询的优化

Django ORM之QuerySet方法大全

Django - 原始 SQL 查询或 Django QuerySet ORM

Python - Django - ORM QuerySet 方法补充

Django ORM查询总结

Django ORM queryset object 解释(子查询和join连表查询的结果)