Django select_related - когда его использовать

Я пытаюсь оптимизировать свои запросы ORM в Django. Я использую connection.queries для просмотра запросов, сгенерированных для меня django.

Предполагая, что у меня есть эти модели:

class Book(models.Model):
    name   = models.CharField(max_length=50)
    author = models.ForeignKey(Author)

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

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

Так что я должен использовать

Book.objects.all().select_related("author")

Что приведет к запросу JOIN. Даже если я сделаю строку раньше:

Author.objects.all()

Очевидно, в шаблоне я напишу что-то вроде {{book.author.name}}.
Таким образом, вопрос в том, когда я получаю доступ к значению внешнего ключа (автор), если у django уже есть этот объект из другого запроса, это все равно приведет к дополнительному запросу (для каждой книги)? Если нет, то в таком случае, действительно ли использование select_related приводит к снижению производительности?

Ответ 1

Вы на самом деле задаете два разных вопроса:

1. действительно ли использование select_related приводит к снижению производительности?

Вы должны увидеть документацию о Django Query Cache:

Understand QuerySet evaluation

Чтобы избежать проблем с производительностью, важно понимать:

  • что QuerySets ленивы.

  • когда они оцениваются.

  • как данные хранятся в памяти.

Итак, в заключение, Django кэширует результаты памяти, оцениваемые в одном и том же объекте QuerySet, то есть если вы делаете что-то подобное:

books = Book.objects.all().select_related("author")
for book in books:
    print(book.author.name)  # Evaluates the query set, caches in memory results
first_book = books[1]  # Does not hit db
print(first_book.author.name)  # Does not hit db  

Ударит только db один раз, когда вы предварительно выбираете авторов в select_related, все это приведет к одному запросу к базе данных с INNER JOIN.

НО это не будет делать кеш между наборами запросов и даже с одним и тем же запросом:

books = Book.objects.all().select_related("author")
books2 = Book.objects.all().select_related("author")
first_book = books[1]  # Does hit db
first_book = books2[1]  # Does hit db

На самом деле это указано в документах:

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

2. если у django уже есть этот объект из другого запроса, приведет ли это к дополнительному запросу (для каждой книги)?

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

Ответ не Django, официально не поддерживается, но неофициально, да, через сторонние приложения. Наиболее подходящие сторонние приложения, поддерживающие этот тип кэширования:

  1. Джонни-Кэш (старше, не поддерживает django> 1.6)
  2. Django-Cachalot (новее, поддерживает 1.6, 1.7 и все еще в dev 1.8)
  3. Django-Cacheops (новее, поддерживает Python 2.7 или 3. 3+, Django 1. 8+ и Redis 2. 6+ (4. 0+ рекомендуется))

Посмотрите эти, если вы ищете для кэширования запросов и помните, сначала профиль, найти узкие места, и если они вызывают проблемы, то оптимизируйте.

Настоящая проблема заключается в том, что программисты слишком много времени уделяют заботам об эффективности в неправильных местах и в неподходящее время; преждевременная оптимизация - корень всего зла (или, по крайней мере, большей его части) в программировании. Дональд Кнут.

Ответ 2

Django не знает о других запросах! Author.objects.all() и Book.objects.all() - это совершенно разные запросы. Так что, если они есть и в вашем представлении, и передают их в контекст шаблона, но в вашем шаблоне вы делаете что-то вроде:

{% for book in books %}
  {{ book.author.name }}
{% endfor %}

и имеют N книги, это приведет к дополнительным запросам базы данных N (помимо запросов для получения всех книг и авторов)!

Если вместо этого вы выполнили Book.objects.all().select_related("author"), в приведенном выше фрагменте шаблона никаких дополнительных запросов не будет.

Теперь select_related(), конечно, добавляет некоторые служебные запросы. Случается, что при выполнении Book.objects.all() django вернет результат SELECT * FROM BOOKS. Если вместо этого вы выполните Book.objects.all().select_related("author"), django вернет результат SELECT * FROM BOOKS B LEFT JOIN AUTHORS A ON B.AUTHOR_ID = A.ID. Поэтому для каждой книги он будет возвращать как столбцы книги, так и ее автора. Тем не менее, эти накладные расходы значительно меньше по сравнению с накладными расходами на базу данных N раз (как объяснялось ранее).

Итак, несмотря на то, что select_related создает небольшую служебную нагрузку (каждый запрос возвращает больше полей из базы данных), на самом деле будет полезно использовать его, кроме случаев, когда вы полностью уверены, что вам понадобятся только столбцы конкретных которую вы запрашиваете.

Наконец, отличный способ действительно увидеть, сколько запросов (и которые именно) являются excultuted в вашей базе данных, - использовать django-debug-tooblar (https://github.com/django-debug-toolbar/django-debug-toolbar).

Ответ 3

Book.objects.select_related("author")

достаточно хорош. Нет необходимости в Author.objects.all()

{{ book.author.name }}

не попадет в базу данных, потому что book.author уже предварительно заполнен.