Советы по оптимизации работы с базой данных в Django
В этой части я расскажу о нескольких ключевых советах по оптимизации, а не о каждом из них. Пожалуйста, ознакомьтесь с официальной документацией Django для получения полной информации.
На протяжении всего произведения мы будем использовать следующие модели:
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, который записывает запросы, сделанные в текущем соединении. Есть также декоратор отладчика запросов для получения количества запросов в функции - вы можете использовать его для проверки эффективности вашего запроса.
Используйте итератор, когда это возможно
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()
и exist()
.
# Плохо
count = len(Entry.objects.all()) # Вычисляет весь набор запросов
# Хорошо
count = Entry.objects.count() # Выполняет более эффективный SQL для определения количества
# Плохо
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()
.