select_related и prefetch_related в Django
В Django select_related
и prefetch_related
предназначены для остановки потока запросов к базе данных, вызванных доступом к связанным объектам.
Я в основном пытался выяснить, как и сколько запросов он сокращает, и в этой статье я опишу свои выводы.
В статье мы будем использовать следующие модели:
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=300)
def __str__(self):
return self.name
class Book(models.Model):
name = models.CharField(max_length=300)
price = models.IntegerField(default=0)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
class Meta:
default_related_name = 'books'
def __str__(self):
return self.name
class Store(models.Model):
name = models.CharField(max_length=300)
books = models.ManyToManyField(Book)
class Meta:
default_related_name = 'stores'
def __str__(self):
return self.name
Чтобы протестировать нашу функцию, нам нужно вставить данные в наши модели.
По этой причине я написал команду управления, которая вставляет пять издателей, 100 книг (по 20 книг для каждого издателя) и 10 магазинов (по 10 книг в каждом магазине). Просто запустите python manage.py load_items
.
import random
from django.core.management.base import BaseCommand
from apps.bookstore.models import Publisher, Store, Book
class Command(BaseCommand):
"""
Эта команда предназначена для вставки издателя, книги, магазина в базу данных.
Добавляет 5 издателей, 100 книг, 10 магазинов.
"""
def handle(self, *args, **options):
Publisher.objects.all().delete()
Book.objects.all().delete()
Store.objects.all().delete()
# создать 5 издателей
publishers = [Publisher(name=f"Publisher{index}") for index in range(1, 6)]
Publisher.objects.bulk_create(publishers)
# создать по 20 книг для каждого издателя
counter = 0
books = []
for publisher in Publisher.objects.all():
for i in range(20):
counter = counter + 1
books.append(Book(name=f"Book{counter}", price=random.randint(50, 300), publisher=publisher))
Book.objects.bulk_create(books)
# создать 10 магазинов и вставить по 10 книг в каждый магазин
books = list(Book.objects.all())
for i in range(10):
temp_books = [books.pop(0) for i in range(10)]
store = Store.objects.create(name=f"Store{i+1}")
store.books.set(temp_books)
store.save()
Я написал декоратор для измерения времени выполнения и количества запросов, выполняемых в функции.
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
select_related
Мы используем select_related
, когда объект, который вы собираетесь выбрать, является одним объектом, что означает пересылку ForeignKey
, OneToOne
и обратный OneToOne.
select_related
работает путем создания соединения SQL и включения полей связанного объекта в оператор SELECT
. По этой причине select_related
получает связанные объекты в том же запросе к базе данных.
Давайте разберемся с этим на примере.
@query_debugger
def book_list():
queryset = Book.objects.all()
books = []
for book in queryset:
books.append({'id': book.id, 'name': book.name, 'publisher': book.publisher.name})
return books
После запуска этой функции вывод показывает:
Function : book_list Number of Queries : 101 Finished in : 0.08s
Один запрос для заполнения всех книг и, выполняя итерацию каждый раз, мы получаем доступ к издателю, который выполняет другой отдельный запрос.
Давайте изменим запрос с помощью select_related
следующим образом и посмотрим, что произойдет.
@query_debugger
def book_list_select_related():
queryset = Book.objects.select_related('publisher').all()
books = []
for book in queryset:
books.append({'id': book.id, 'name': book.name, 'publisher': book.publisher.name})
return books
После запуска этой функции вывод показывает:
Function : book_list_select_related Number of Queries : 1 Finished in : 0.02s
Разве это не потрясающе? Этот запрос уменьшил 101 до 1. Это то, что делает select_related
.
prefetch_related
Мы используем prefetch_related
, когда собираемся получить набор вещей.
Это означает обработку ManyToMany
и обратных ManyToMany
, ForeignKey
. prefetch_related
выполняет отдельный поиск для каждой связи и выполняет «объединение» в Python.
Он отличается от select_related
. prefetch_related
выполнял JOIN
с использованием Python, а не в базе данных.
Давайте разберемся с этим на примере.
@query_debugger
def store_list():
queryset = Store.objects.all()
stores = []
for store in queryset:
books = [book.name for book in store.books.all()]
stores.append({'id': store.id, 'name': store.name, 'books': books})
return stores
После запуска этой функции вывод показывает:
Function : store_list Number of Queries : 11 Finished in : 0.02s
У нас в базе 10 магазинов и в каждом магазине по 10 книг. Здесь происходит один запрос для выборки всех хранилищ, и во время итерации по каждому хранилищу выполняется другой запрос, когда мы получаем доступ к полю books
ManyToMany
.
Давайте уменьшим количество запросов с помощью prefetch_related
.
@query_debugger
def store_list_prefetch_related():
queryset = Store.objects.prefetch_related('books')
stores = []
for store in queryset:
books = [book.name for book in store.books.all()]
stores.append({'id': store.id, 'name': store.name, 'books': books})
return stores
После запуска этой функции вывод показывает:
Function : store_list_prefetch_related Number of Queries : 2 Finished in : 0.01s
Здесь производительность запросов улучшилась, с 11 до 2 запросов. Я хочу, чтобы вы поняли, что здесь делает prefetch_related
.
Возьмем еще один пример для prefetch_related
.
В коде команды управления я произвольно устанавливаю цену книги от 50 до 300. Теперь мы найдем дорогие книги (цена от 250 до 300) в каждом магазине.
@query_debugger
def store_list_expensive_books_prefetch_related():
queryset = Store.objects.prefetch_related('books')
stores = []
for store in queryset:
books = [book.name for book in store.books.filter(price__range=(250, 300))]
stores.append({'id': store.id, 'name': store.name, 'books': books})
return stores
После запуска этой функции вывод показывает:
Function : store_list_expensive_books_prefetch_related Number of Queries : 12 Finished in : 0.05s
Несмотря на то, что мы используем prefetch_related
, наши запросы скорее увеличились, чем уменьшились. Но почему?
Используя предварительную выборку, мы говорим Django предоставить все результаты для JOIN, но когда мы используем фильтр (price__range = (250, 300))
, мы меняем основной запрос, и тогда Django не присоединяется к правильным результатам для нас.
По этой причине у нас есть 12 запросов, 11 запросов, повторяющихся по хранилищам, и один запрос для получения всех результатов в режиме предварительной выборки.
Решим проблему с Prefetch
.
@query_debugger
def store_list_expensive_books_prefetch_related_efficient():
queryset = Store.objects.prefetch_related(
Prefetch('books', queryset=Book.objects.filter(price__range=(250, 300))))
stores = []
for store in queryset:
books = [book.name for book in store.books.all()]
stores.append({'id': store.id, 'name': store.name, 'books': books})
return stores
После запуска этой функции вывод показывает:
Function : store_list_expensive_books_prefetch_related_efficient Number of Queries : 2 Finished in : 0.03s
Миссия выполнена успешно! Два запроса, а не 12.
Перевод https://medium.com/better-programming/django-select-related-and-prefetch-related-f23043fd635d