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, который записывает запросы, сделанные с текущим подключением. Ниже приведен код декоратора отладчика запросов для получения количества запросов в функции — вы можете использовать его, чтобы проверить эффективность вашего запроса. Есть также очень хороший пакет 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().