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_relatedprefetch_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

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