Советы по оптимизации работы с базой данных в 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().

Вернуться на верх