В этой статье мы поделимся с вами несколькими советами по оптимизации запросов к базе данных.
Для работы мы будем использовать следующие модели:
class Author(models.Model): name = models.CharField(max_length=200) email = models.EmailField() def __str__(self): return self.name class Blog(models.Model): name = models.CharField(max_length=100) tagline = models.TextField() def __str__(self): return self.name class Entry(models.Model): blog = models.ForeignKey(Blog, on_delete=models.CASCADE) headline = models.CharField(max_length=255) body_text = models.TextField(blank=True) likes = models.IntegerField(blank=True, default=0) authors = models.ManyToManyField(Author, blank=True) class Meta: default_related_name = 'entries' def __str__(self): return self.headline
Разберитесь с оценкой и кешированием QuerySet
Разработчик Django должен разбираться в оценке и кэшировании QuerySet для оптимизации обращений к базе данных. Более подробно можете почитать об этом здесь.
Используйте get() с умом
Используйте get()
, если вы знаете, что существует только один объект, соответствующий вашему запросу. Если ни один элемент не соответствует запросу, то get()
вызовет исключение DoesNotExist
. Если запросу соответствует несколько объектов, то get()
вызовет исключение MultipleObjectsReturned
. Используйте get()
следующим образом:
try: one_entry = Entry.objects.get(blog=2000) except Entry.DoesNotExist: # запросу не соответствует ни один элемент. pass except Entry.MultipleObjectsReturned: # запросу соответствует несколько элементов. pass else: # запросу соответствует только один элемент. print(one_entry)
Используйте доступные инструменты отладки
Используйте django-debug-toolbar и QuerySet.explain(), чтобы определить эффективность вашего кода. Разберитесь с django.db.connection
, который записывает запросы, сделанные с текущим подключением. Ниже приведен код декоратора отладчика запросов для получения количества запросов в функции — вы можете использовать его, чтобы проверить эффективность вашего запроса. Есть также очень хороший пакет django-silk, которым тоже можно пользоваться.
from django.db import connection, reset_queries import time import functools def query_debugger(func): @functools.wraps(func) def inner_func(*args, **kwargs): reset_queries() start_queries = len(connection.queries) start = time.perf_counter() result = func(*args, **kwargs) end = time.perf_counter() end_queries = len(connection.queries) print(f"Function : {func.__name__}") print(f"Number of Queries : {end_queries - start_queries}") print(f"Finished in : {(end - start):.2f}s") return result return inner_func
По возможности используйте итератор
QuerySet обычно кеширует свои результаты, когда происходит оценка, и для любых дальнейших операций с этим QuerySet он сначала проверяет кешированные результаты. Но когда вы используете iterator()
, он не проверяет кеш и считывает результаты непосредственно из базы данных. Результаты не сохраняются в QuerySet.
Для QuerySet, возвращающего большое количество объектов, которые требуют много памяти для кеширования и к которому вам нужно получить доступ только один раз, вы можете использовать iterator()
.
В следующем коде мы извлечем все записи из базы данных и загрузим в память, а затем проитерируемся по ним.
q = Entry.objects.all() for each in q: do_something(each)
Когда мы используем iterator()
, Django будет удерживать SQL-соединение открытым, читать каждую строку и вызывать do_something()
перед чтением следующей строки.
q = Entry.objects.all().iterator() for each in q: do_something(each)
Используйте постоянные соединения с базой данных
Django при каждом запросе открывает новое соединение с базой данных и закрывает его, когда его запрос выполнен. За это поведение отвечает настройка CONN_MAX_AGE
— значение по умолчанию равно 0. Но сколько секунд должно быть установлено соединение? Это зависит от трафика вашего сайта — чем больше трафика, тем больше секунд требуется для сохранения соединения. Мы бы рекомендовали установить относительно небольшое значение, например 60.
Используйте select_related() и prefetch_related()
В Django select_related()
и prefetch_related()
предназначены для снижения количества запросов к базе данных. Более подробно о них вы можете прочитать здесь.
Используйте F-выражения
# Так делать не стоит. for entry in Entry.objects.all(): entry.likes += 1 entry.save() # Лучше так. Entry.objects.update(likes=F('likes') + 1)
Используйте агрегацию
# Так делать не стоит. most_liked = 0 for entry in Entry.objects.all(): if entry.likes > most_liked: most_liked = entry.likes # Лучше так. most_liked = Entry.objects.all().aggregate(Max('likes'))['likes__max']
Используйте значения внешних ключей напрямую
Django ORM автоматически извлекает и кеширует внешние ключи, поэтому используйте их вместо ненужных запросов к базе данных.
# Так делать не стоит. Требуется обращение к базе данных. blog_id = Entry.objects.get(id=200).blog.id # Лучше так. Внешний ключ уже кеширован, поэтому обращение к базе данных не требуется. blog_id = Entry.objects.get(id=200).blog_id # Или так. Обращение к базе данных так же не требуется. blog_id = Entry.objects.select_related('blog').get(id=200).blog.id
Не сортируйте результаты, если вам это не нужно
Сортировка не бесплатная — каждое поле, которое должно быть отсортировано — это операция, которую должна выполнить база данных. Если у модели есть сортировка по умолчанию (Meta.ordering
) и она вам не нужна, удалите ее в QuerySet, вызвав order_by()
без параметров. Добавление индекса в вашу базу данных может повысить производительность сортировки.
Используйте count() и exists()
Если вам не нужно содержимое QuerySet, используйте count()
и exists()
.
# Так делать не стоит. count = len(Entry.objects.all()) # Лучше так. count = Entry.objects.count() # Так делать не стоит. qs = Entry.objects.all() if qs: pass # Лучше так. qs = Entry.objects.exists() if qs: pass
Используйте массовое добавление add(*objs) для ManyToManyField полей
author1 = Author(name='author1') author2 = Author(name='author2') author3 = Author(name='author3') entry = Entry.objects.get(id=1) # Так делать не стоит. entry.authors.add(author1) entry.authors.add(author2) entry.authors.add(author3) # Лучше так. entry.authors.add(author1, author2, author3)
Используйте delete() и update() для массовых операций
Если вы хотите удалить или обновить сразу несколько экземпляров модели, используйте соответственно delete()
и update()
.
# Так делать не стоит. Элементы удаляются по одному. for entry in Entry.objects.all(): entry.delete() # Лучше так. Удаляются все сразу. Entry.objects.all().delete() # Так делать не стоит. for entry in Entry.objects.all(): entry.likes += 1 entry.save() # Лучше так. Entry.objects.update(likes=F('likes')+1)
Используйте bulk_create()
# Так делать не стоит. for i in range(20): Blog.objects.create(name="blog"+str(i), headline='tagline'+str(i)) # Лучше так. blogs = [] for i in range(20): blogs.append(Blog(name="blog"+str(i), headline='tagline'+str(i))) Blog.objects.bulk_create(blogs)
Используйте values(), values_list(), defer(), only()
Если вам нужны определенные поля в результатах QuerySet и вы хотите получить результаты в списке, кортеже или словарях, используйте values()
и values_list()
.
Если вам нужны определенные поля в результатах QuerySet вы хотите получить объекты модели в QuerySet вместо списка, кортежа или словарей, используйте defer()
и only()
.