Почему проблема N+1 в Django ORM так часто упоминается?

Используя Django ORM, вы часто слышите термин "проблема N+1". Однако, если вы не сталкивались с ней напрямую, сложно ощутить, насколько серьезной является эта проблема.

Говоря простыми словами, это "проблема, когда запрос, который должен быть выполнен только один раз, выполняется гораздо чаще, чем ожидалось". В результате, скорость загрузки страниц может значительно замедлиться, и чем больше данных, тем более выражено снижение производительности. Особенно это часто происходит, когда вы запрашиваете данные, связанные с моделями, поэтому это обязательно нужно понимать, когда вы используете ORM.

💡 Давайте легко поймем проблему N+1

Предположим, мы создаем блоговую систему. Каждый автор (Author) может писать несколько постов (Post). Если представить это с помощью модели Django, она будет выглядеть следующим образом.

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

Так как каждый автор (Author) может писать несколько постов (Post), это отношение 1:N.

Теперь предположим, что мы реализуем функцию, которая выводит имена всех авторов и заголовки их постов. Как правило, это может быть написано следующим образом.

⚠️ Код, вызывающий проблему N+1

authors = Author.objects.all()  # (1) Первый запрос (поиск в таблице Author)

for author in authors:
    print(author.post_set.all())  # (N) Поиск постов каждого автора по отдельности

Если запустить этот код, возможно, это займет больше времени, чем ожидалось. Если данных не так много, это может и не быть заметным, но если авторов 100, а постов более 500? Проблема начнет проявляться.

🧐 SQL-запросы, которые фактически выполняются

SELECT * FROM author;  -- Выполняется 1 раз
SELECT * FROM post WHERE author_id = 1;  -- Выполняется N раз
SELECT * FROM post WHERE author_id = 2;
SELECT * FROM post WHERE author_id = 3;
...

Таким образом, чем больше авторов, тем больше становится количество запросов. Вот в чем заключается проблема N+1.

🚀 В каких ситуациях часто возникает проблема N+1?

На самом деле эта проблема возникает независимо от того, хорошо вы используете Django ORM или нет, при запросе данных, связанных с двумя или более моделями, вам почти всегда нужно быть внимательным. Особенно это случается в следующих случаях.

1️⃣ Когда в шаблоне вы ссылаетесь на данные внутри цикла

{% for author in authors %}
    

{{ author.name }}

    {% for post in author.post_set.all %}
  • {{ post.title }}
  • {% endfor %}
{% endfor %}

Когда вы обращаетесь к данным в шаблоне через цикл, каждый объект вызывает дополнительные запросы, что снижает производительность.

2️⃣ Когда в функции представления вы ссылаетесь на связанные модели в цикле

def blog_view(request):
    authors = Author.objects.all()  # Первый запрос (поиск авторов)
    
    author_list = []
    for author in authors:
        author_list.append({
            'name': author.name,
            'posts': author.post_set.all()  # Происходит N дополнительных запросов
        })
    
    return render(request, 'blog.html', {'authors': author_list})

Если данные обрабатываются в функции представления, такая же проблема также может возникнуть.

💡 Мой личный опыт с проблемой N+1

Ранее я создавал пользовательский метод get_absolute_url(). Это была функция, которая генерировала URL, объединяя несколько полей модели, и я нашел причину медленной работы только после того, как столкнулся с проблемой производительности.

class Post(models.Model):
    title = models.CharField(max_length=255)
    slug = models.SlugField(unique=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

    def get_absolute_url(self):
        return f"/author/{self.author.name}/{self.slug}/"

Каждый раз, когда вы вызываете этот метод, self.author.name выполняется, вызывая N дополнительных запросов. Решение этой проблемы заняло у меня довольно много времени.

✅ Резюме: Почему важно понимать проблему N+1

  • Если вы используете Django ORM, эта проблема может возникнуть в любое время.
  • Если скорость страницы замедляется, это может означать, что ORM выполняет больше запросов, чем ожидалось.
  • Проблема может возникнуть в любом месте: в шаблонах, функциях представления и методах модели.
  • С увеличением объема данных производительность может сильно ухудшиться.
  • Поскольку ORM не проводит автоматическую оптимизацию, вам нужно проверить выполняемые запросы и оптимизировать их.

В следующей статье мы обсудим, как решить проблему N+1 с помощью select_related и prefetch_related.

Проблема N+1 в Django ORM