优化 Django的查询

好吧,必须先说。 Django 是您可以使用的最漂亮的框架之一。 它提供了引擎盖下的一切,但正如本叔叔常说的:

不正确或不正确地使用事物可能会导致很多问题。 所以让我们继续看看一些 Django 查询以及它在不同条件下的执行情况。

下面讨论的所有 Django 查询都应用于没有索引的基准表。 这是它的结构。

万一,如果你想在桌子上玩,你可以在这里克隆 Django 应用程序

在触发查询之前,我已经在数据库表中插入了 200 万条记录。 您可以在此处下载数据库转储。 要插入更多行,您可以使用上面提到的 git repo 中的脚本。

1.only、defer、values和values_list的区别
让我们用一个客观的查询来理解这四件事。 我们必须从我们的表中获取所有 Unique databook_id。 而已。 这就是我们所要做的。

考虑一个包含大量列的胖模型,获取整个对象将是一个巨大的开销,而不是获取列的子集。

我们获取了所有对象,但仅使用“databook_id”列。 这比获取整个对象并迭代它以获得“databook_id”要好得多。

现在让我们仔细看看“try-and-except”块。 在这里,我试图访问我没有在 .only() 函数中作为参数添加的“数据”列。

如果您认为 Django 会为此抛出异常,请不要担心。 你不是一个人。 但有趣的是,会再触发一个查询,并从表中获取数据列,因此不会出现任何异常。 很奇怪,对吧?

现在来到时间部分,我的机器运行这个块花了 23.72 秒。 昂贵。 我知道。

与 .only() 完全相反。 它获取除 .defer() 中指定的内容之外的所有内容

这里没什么特别的,和上面一样,取一个没有数据列的对象,它是一个 JSON 字段,并迭代它以获得 databook_id。 再次,仔细查看“try-and-except”块语句,如果您至少在这里认为 Django 会抛出异常。

Django 将再触发一个查询并获取数据列。 这个块花费了 41 秒的时间来执行。 这是可以理解的,考虑到它正在获取除数据之外的所有其他列。

如果检查 .only() 和 .defer(),可以看到返回类型是 Object。 这就是我们将其作为 object.column_name 访问的原因。 .values() 将返回字典列表而不是模型对象。 它还允许我们只选择我们需要从模型中获取的列集。

字典列表将存储在 benchmark_objects 中,我们正在遍历列表以获取 databook_id 这个块需要 10.5 秒来执行。 这比前两个要好得多。

很酷的方法。 只是将“databook_id”作为平面列表返回。 删除 flat=True 会给出一个元组列表。

它在 6.72 秒内被执行。

好的,好的,我听到了。 这也很慢。 但是你有没有看到什么东西,这在上面的所有四个块中都很常见?

我们从表中取出所有对象并将它们添加到一个集合中。 如果数据库做那件事而不是 Python,那么性能改进会是什么?

刚刚将 .distinct() 添加到查询中。 所以表做了唯一的过滤。 有趣的是,执行该块只需要 0.8 秒。

如果您已经阅读到现在,谢谢。 你有一个很好的注意力跨度!

回来,记住我们是如何在 40 秒或 30 秒内完成相同的操作的。 现在我们在不到 1 秒的时间内做了同样的事情。

注意事项:

只从桌子上拿走你需要的东西。 不要贪心。
如果可以,请在表格中执行,如果没有办法,请使用 Python vanilla 函数。

2. 不要在枪战中带刀。

假设您必须编写一个仅在表中有特定条目可用时执行的代码块,我们可以通过多种方式执行此操作。 喜欢,

使用我们的条件过滤掉所有条目并对其应用 len() 函数。
过滤并在查询上执行 .count() ,它返回符合我们条件的条目数。
作为一个聪明的人,您遵循了第二个选项,记住了我们之前讨论过的内容,即,如果可以的话,在数据库中执行。

执行此块花费了 0.3 秒。 还不错吧? 可是等等。 这里 count 是一个多余的部分,我们想知道是否只有一个实例满足我们的条件。 这就是 .exist() 方法有帮助的地方。

 

以 0.03 秒为代价执行与前一个块相同的操作。 运行时间减少了惊人的 100% 或 1000%。 我知道我数学不好😕

您可能会问“我拥有现代机器,我为什么还要关心这个兄弟?”。 问题是,一旦数据增长,时间差就会呈指数级增长。

注意事项:

这不仅是在数据库上做,而且要以最好和正确的方式做。
阅读文档。

3.遇到麻烦时调用Q()。

比方说,我们想获得满足以下条件的对象的数量。

双重查询和过滤:我们可以分别做两个查询,遍历对象并过滤 NOT 条件。 但这是超级昂贵且非常低效的。

即使在最黑暗的时期也可以找到性能,当人们只记得阅读文档时。 – 不是邓布利多。

这就是 Q() 来救援的时候。 它的作用是让我们轻松地进行复杂的查询。 Q() 表达式也可以与 filter() 函数结合使用。

&、|、~ 符号分别用于 AND、OR、NOT。

我们从 Q() 中得到了什么?

更好的性能。
更好的代码可读性
一些幸福。

4. F() 是什么。

现在,假设您必须更新所有具有 databook_id = ‘A’ 的行的“knowledge_end_date”。 你必须将它更新为“knowlege_begin_date”+ 12 天让我举个例子:

knowledge_begin_date knowledge_end_date databook_id
2022-05-13 NULL A
2021-01-01 NULL B
2022-04-05 NULL A

更新后:

knowledge_begin_date knowledge_end_date databook_id
2022-05-13 2022-05-25 A
2021-01-01 NULL B
2022-04-05 2022-04-17 A

 

布鲁? 你读了这么久? 该死的你的注意力。

想到的一件事是获取所有具有 databook_id=’A’ 的对象,遍历它们,更新 Knowledge_end_date 并在对象上调用 .save() 方法。 下面是它的实现。

这会触发 N+1 个查询来更新 N 个对象。 是的,我听到了,我们可以做 bulk_update() 但是如果我告诉你有更好的方法。

这段代码做同样的事情但更好。 这里发生的是 F() 表达式将在表中进行修改而不获取它。 请注意,我们只能添加两个相同类型的东西。 Date + Date 或 Int + Int 等等。 如果您尝试使用日期或字符串添加 Int。 F() 表达式将引发异常。

注意事项:

同样,让数据库而不是 Python 来做艰苦的工作。
F() 减少了查询次数,避免了在迭代和保存的情况下出现的竞争条件问题。

5. 迭代、迭代、迭代……
假设您的桌子变得非常大。 您遇到了需要从表中获取几乎 90% 的数据的情况。 接下来,您使用该条件触发一个查询并炸毁您的记忆。

酷,现在我们已经用上面的查询炸毁了我们的内存。 让我们去 .iterator()

迭代器是在任何时间点只给你一个即时对象的东西。 您无法重新访问以前看到的对象。

Django 支持 .iterator() ,它打开一个数据库连接一次,而不是一次获取所有内容,而是逐块获取对象。 您消耗第一块对象,然后移动到下一个块。

是的,我们在这里增加了查询的数量,以便以最少的内存做事。

假设您的表中有 5420 个对象,上述实现将触发 3 个查询。

chunk_size 参数帮助我们配置,我们需要为查询选择多少对象。

注意事项:

.iterator() 增加了您将进行的查询数量,但大大减少了内存。
如果往返(连接到远程数据库、获取数据并将其返回给您所需的时间)很高,请考虑增加 chunk_size。

6.索引好。 扫描不好。

让我们看看哪些查询会进行索引扫描,哪些查询不会。

注意事项:

尝试以与索引相同的顺序在过滤器中使用条件。
阅读 https://use-the-index-luke.com/。 Internet 上没有比这更好的站点解释这样的索引了。

就是这样,伙计们。 感谢您阅读到最后!

原文:https://delliganesh.dev/tech/the-one-with-better-django-queries/

https://github.com/yuvanist/django-app-to-test-queries