好吧,必须先说。 Django 是您可以使用的最漂亮的框架之一。 它提供了引擎盖下的一切,但正如本叔叔常说的:
1 |
权力越大,责任越大 |
不正确或不正确地使用事物可能会导致很多问题。 所以让我们继续看看一些 Django 查询以及它在不同条件下的执行情况。
下面讨论的所有 Django 查询都应用于没有索引的基准表。 这是它的结构。
1 2 3 4 5 6 7 |
class Benchmark(models.Model): knowledge_begin_date = models.DateTimeField(null=False) knowledge_end_date = models.DateTimeField(null=True) client_id = models.IntegerField(null=False) databook_id = models.UUIDField(null=False) datasheet_id = models.UUIDField(null=False) data = JSONField(null=True, encoder=DjangoJSONEncoder) |
万一,如果你想在桌子上玩,你可以在这里克隆 Django 应用程序
在触发查询之前,我已经在数据库表中插入了 200 万条记录。 您可以在此处下载数据库转储。 要插入更多行,您可以使用上面提到的 git repo 中的脚本。
1.only、defer、values和values_list的区别
让我们用一个客观的查询来理解这四件事。 我们必须从我们的表中获取所有 Unique databook_id。 而已。 这就是我们所要做的。
Using .only
考虑一个包含大量列的胖模型,获取整个对象将是一个巨大的开销,而不是获取列的子集。
1 2 3 4 5 6 7 8 9 10 |
benchmark_objects = Benchmark.objects.only('databook_id') unique_databook_ids = set() for each_object in benchmark_objects: unique_databook_ids.add(each_object.databook_id) try: print(benchmark_objects[0].data) except: print('Not able to access data') |
我们获取了所有对象,但仅使用“databook_id”列。 这比获取整个对象并迭代它以获得“databook_id”要好得多。
现在让我们仔细看看“try-and-except”块。 在这里,我试图访问我没有在 .only() 函数中作为参数添加的“数据”列。
如果您认为 Django 会为此抛出异常,请不要担心。 你不是一个人。 但有趣的是,会再触发一个查询,并从表中获取数据列,因此不会出现任何异常。 很奇怪,对吧?
现在来到时间部分,我的机器运行这个块花了 23.72 秒。 昂贵。 我知道。
Using .defer()
与 .only() 完全相反。 它获取除 .defer() 中指定的内容之外的所有内容
1 2 3 4 5 6 7 8 9 10 |
benchmark_objects = Benchmark.objects.defer('data') unique_databook_ids = set() for each_object in benchmark_objects: unique_databook_ids.add(each_object.databook_id) try: print(benchmark_objects[0].data) except: print('Not able to access data') |
这里没什么特别的,和上面一样,取一个没有数据列的对象,它是一个 JSON 字段,并迭代它以获得 databook_id。 再次,仔细查看“try-and-except”块语句,如果您至少在这里认为 Django 会抛出异常。
Django 将再触发一个查询并获取数据列。 这个块花费了 41 秒的时间来执行。 这是可以理解的,考虑到它正在获取除数据之外的所有其他列。
using .values()
如果检查 .only() 和 .defer(),可以看到返回类型是 Object。 这就是我们将其作为 object.column_name 访问的原因。 .values() 将返回字典列表而不是模型对象。 它还允许我们只选择我们需要从模型中获取的列集。
1 2 3 4 5 6 7 8 9 10 |
benchmark_objects = list(Benchmark.objects.values('databook_id')) unique_databook_ids = set() for each_object in benchmark_objects: unique_databook_ids.add(each_object['databook_id']) try: print(benchmark_objects[0]['data']) except: print('Exception: Data is not accessible here') |
字典列表将存储在 benchmark_objects 中,我们正在遍历列表以获取 databook_id 这个块需要 10.5 秒来执行。 这比前两个要好得多。
using .values_list(flat=True)
很酷的方法。 只是将“databook_id”作为平面列表返回。 删除 flat=True 会给出一个元组列表。
1 2 3 |
unique_databook_ids = set( Benchmark.objects.values_list("databook_id", flat=True) ) |
它在 6.72 秒内被执行。
好的,好的,我听到了。 这也很慢。 但是你有没有看到什么东西,这在上面的所有四个块中都很常见?
我们从表中取出所有对象并将它们添加到一个集合中。 如果数据库做那件事而不是 Python,那么性能改进会是什么?
1 2 3 |
unique_databook_ids = set( Benchmark.objects.values_list("databook_id", flat=True).distinct() ) |
刚刚将 .distinct() 添加到查询中。 所以表做了唯一的过滤。 有趣的是,执行该块只需要 0.8 秒。
如果您已经阅读到现在,谢谢。 你有一个很好的注意力跨度!
回来,记住我们是如何在 40 秒或 30 秒内完成相同的操作的。 现在我们在不到 1 秒的时间内做了同样的事情。
注意事项:
只从桌子上拿走你需要的东西。 不要贪心。
如果可以,请在表格中执行,如果没有办法,请使用 Python vanilla 函数。
2. 不要在枪战中带刀。
假设您必须编写一个仅在表中有特定条目可用时执行的代码块,我们可以通过多种方式执行此操作。 喜欢,
使用我们的条件过滤掉所有条目并对其应用 len() 函数。
过滤并在查询上执行 .count() ,它返回符合我们条件的条目数。
作为一个聪明的人,您遵循了第二个选项,记住了我们之前讨论过的内容,即,如果可以的话,在数据库中执行。
1 2 3 4 5 6 |
db_ds_objects = Benchmark.objects.filter( databook_id="61722a62-fe71-44df-86a5-477bcdfbd91c" ).count() if db_ds_objects > 0: print("Yes, one object with this condition exist") |
执行此块花费了 0.3 秒。 还不错吧? 可是等等。 这里 count 是一个多余的部分,我们想知道是否只有一个实例满足我们的条件。 这就是 .exist() 方法有帮助的地方。
1 2 3 4 5 6 |
db_ds_objects = Benchmark.objects.filter( databook_id="61722a62-fe71-44df-86a5-477bcdfbd91c" ).exists() if db_ds_objects: print("Yes, this object exist") |
以 0.03 秒为代价执行与前一个块相同的操作。 运行时间减少了惊人的 100% 或 1000%。 我知道我数学不好😕
您可能会问“我拥有现代机器,我为什么还要关心这个兄弟?”。 问题是,一旦数据增长,时间差就会呈指数级增长。
注意事项:
这不仅是在数据库上做,而且要以最好和正确的方式做。
阅读文档。
3.遇到麻烦时调用Q()。
比方说,我们想获得满足以下条件的对象的数量。
1 |
(databook_id='A' OR datasheet_id='B') AND (NOT the records which has databook_id='A' and datasheet_id='B') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
databook_condition_objs = Benchmark.objects.filter(databook_id="A") datasheet_condition_objs = Benchmark.objects.filter(datasheet_id="B") combined_objects_before_check = [] for each_object in databook_condition_objs: combined_objects_before_check.append(each_object) for each_object in datasheet_condition_objs: combined_objects_before_check.append(each_object) combined_objects_after_check = [] for each_object in combined_objects_before_check: if not (each_object.databook_id == "A" and each_object.datasheet_id == "B"): combined_objects_after_check.append(each_object) print(len(combined_objects_after_check)) |
双重查询和过滤:我们可以分别做两个查询,遍历对象并过滤 NOT 条件。 但这是超级昂贵且非常低效的。
即使在最黑暗的时期也可以找到性能,当人们只记得阅读文档时。 – 不是邓布利多。
这就是 Q() 来救援的时候。 它的作用是让我们轻松地进行复杂的查询。 Q() 表达式也可以与 filter() 函数结合使用。
&、|、~ 符号分别用于 AND、OR、NOT。
1 2 3 4 5 6 7 8 9 10 |
databook_condition = Q(databook_id="A") datasheet_condition = Q(datasheet_id="B") both_condition = Q(databook_id="A") & Q( datasheet_id="B" ) combined_objects_after_check = Benchmark.objects.filter( (databook_condition | datasheet_condition) & ~both_condition ).count() print(combined_objects_after_check) |
我们从 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() 方法。 下面是它的实现。
1 2 3 4 5 6 |
objects = Benchmark.objects.filter(datasheet_id="A") for each_object in objects: each_object.knowledge_end_date = ( each_object.knowledge_begin_date + timedelta(days=10) ) each_object.save() |
这会触发 N+1 个查询来更新 N 个对象。 是的,我听到了,我们可以做 bulk_update() 但是如果我告诉你有更好的方法。
1 2 3 |
Benchmark.objects.filter( datasheet_id="A", ).update(knowledge_end_date=F("knowledge_begin_date") + timedelta(days=10)) |
这段代码做同样的事情但更好。 这里发生的是 F() 表达式将在表中进行修改而不获取它。 请注意,我们只能添加两个相同类型的东西。 Date + Date 或 Int + Int 等等。 如果您尝试使用日期或字符串添加 Int。 F() 表达式将引发异常。
注意事项:
同样,让数据库而不是 Python 来做艰苦的工作。
F() 减少了查询次数,避免了在迭代和保存的情况下出现的竞争条件问题。
5. 迭代、迭代、迭代……
假设您的桌子变得非常大。 您遇到了需要从表中获取几乎 90% 的数据的情况。 接下来,您使用该条件触发一个查询并炸毁您的记忆。
1 2 3 4 |
databook_id = set() benchmark_objects = Benchmark.objects.values("databook_id") for obj in benchmark_objects: databook_ids.add(obj["databook_id"]) |
酷,现在我们已经用上面的查询炸毁了我们的内存。 让我们去 .iterator()
迭代器是在任何时间点只给你一个即时对象的东西。 您无法重新访问以前看到的对象。
Django 支持 .iterator() ,它打开一个数据库连接一次,而不是一次获取所有内容,而是逐块获取对象。 您消耗第一块对象,然后移动到下一个块。
是的,我们在这里增加了查询的数量,以便以最少的内存做事。
1 2 3 4 5 6 |
databook_ids = set() benchmark_objects = ( Benchmark.objects.all().values("databook_id").iterator(chunk_size=2000) ) for obj in benchmark_objects: databook_ids.add(obj["databook_id"]) |
假设您的表中有 5420 个对象,上述实现将触发 3 个查询。
chunk_size 参数帮助我们配置,我们需要为查询选择多少对象。
注意事项:
.iterator() 增加了您将进行的查询数量,但大大减少了内存。
如果往返(连接到远程数据库、获取数据并将其返回给您所需的时间)很高,请考虑增加 chunk_size。
6.索引好。 扫描不好。
1 2 3 4 5 6 7 8 9 |
indexes = [ models.Index( fields=[ "client_id", "-knowledge_end_date", ] ), models.Index(fields=["client_id", "databook_id", "datasheet_id"]), ] |
让我们看看哪些查询会进行索引扫描,哪些查询不会。
1 2 3 4 5 6 |
.filter(client_id='1') # Index Scan .filter(client_id='1', databook_id='A') # Index Scan .filter(databook_id='A') # Nopee, Sorry. .filter(datasheet_id='B') # Nopee, Sorry. .filter(client_id='1', databook_id='A',datasheet_id='B') # Index Scan .filter(client_id='1', datasheet_id='B') # Nope, Sorry. |
注意事项:
尝试以与索引相同的顺序在过滤器中使用条件。
阅读 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