Робота з реляційними базами даних — це часто балансування на межі між зручністю програмування та продуктивністю. Припустимо, у нас є дві моделі: Author та Book. Один автор може написати багато книг. Якби ми просто завантажували об'єкти без оптимізації, кожне звернення до пов'язаного об'єкта могло б створювати окремий запит до бази даних. Це не проблема, якщо у нас пара записів. Але спробуйте завантажити одразу тисячу книг з їхніми авторами, і ви почуєте, як ваша база даних жалібно стогне.
1. Оптимізація запитів за допомогою select_related()
select_related() використовується для оптимізації SQL-запитів при роботі з полями ForeignKey або OneToOne. Цей метод дозволяє об'єднати дві таблиці в один SQL-запит за допомогою операції JOIN, тим самим скорочуючи кількість запитів до бази.
Звучить складно? Давай розберемо на прикладі.
Приклад без select_related()
# models.py
class Author(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
# views.py
books = Book.objects.all()
for book in books:
print(book.title, book.author.name)
У цьому прикладі для кожного об'єкта book Django відправляє окремий запит до бази даних, щоб отримати дані про автора. Якщо у нас 100 книг, це означає 101 запит (1 для книг + 100 для кожного автора).
Приклад з select_related()
books = Book.objects.select_related('author')
for book in books:
print(book.title, book.author.name)
Тепер Django виконає один запит з використанням SQL JOIN, витягуючи дані про книги та їх авторів одночасно. Виглядає це приблизно так:
SELECT book.id, book.title, author.id, author.name
FROM book
INNER JOIN author ON book.author_id = author.id;
Коли використовувати select_related()?
- Якщо у вас зв'язок
ForeignKeyабоOneToOne. - Якщо ви багаторазово звертаєтесь до пов'язаних об'єктів у циклі або інших операціях.
- Якщо ваші дані завжди потрібні разом (наприклад, книга завжди відображається зі своїм автором).
2. Робота з prefetch_related()
Якщо select_related() зручний для зв’язків один-до-багатьох ForeignKey і OneToOne, то для зв’язків багато-до-багатьох (ManyToMany) або складних запитів на множину пов’язаних об’єктів краще підходить prefetch_related().
Цей метод виконує два окремих запити, але кешує результат, щоб Django могла зв’язати об’єкти на рівні Python. Це дозволяє уникнути надмірної кількості запитів при складних зв’язках.
Приклад без prefetch_related()
class Tag(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
tags = models.ManyToManyField(Tag)
# views.py
books = Book.objects.all()
for book in books:
print(book.title, [tag.name for tag in book.tags.all()])
Для кожної книги Django виконає окремий запит, щоб завантажити пов’язані теги. Якщо у нас 50 книг і кожна має 5 тегів, ми отримаємо 51 запит (1 для книг + 50 для тегів).
Приклад з prefetch_related()
books = Book.objects.prefetch_related('tags')
for book in books:
print(book.title, [tag.name for tag in book.tags.all()])
Тепер виконується лише два запити: один для завантаження книг, інший для завантаження всіх тегів, пов’язаних із цими книгами.
Приклад SQL-запитів:
- Завантаження книг:
SELECT * FROM book;
- Завантаження тегів:
SELECT * FROM tag INNER JOIN book_tags ON tag.id = book_tags.tag_id WHERE book_tags.book_id IN (1, 2, 3, ..., 50);
Коли використовувати prefetch_related()?
- Якщо у вас зв’язок
ManyToManyабо зворотний зв’язокForeignKey. - Якщо ви хочете уникнути великої кількості "дрібних" запитів.
- Якщо вам потрібно об’єднати дані з пов’язаних таблиць на рівні Python.
3. Порівняння select_related() і prefetch_related()
| Метод | Тип зв'язку | Кількість запитів | Переваги | Недоліки |
|---|---|---|---|---|
select_related() |
ForeignKey, OneToOne |
1 запит | Об'єднує дані на рівні бази даних за допомогою SQL JOIN |
Не підтримує ManyToMany |
prefetch_related()| ManyToMany, зворотний ForeignKey |
2 запити (мінімум) | Завантажує дані через окремі запити і зв'язує їх на рівні Python | Виконує більше запитів, ніж select_related() |
Використовуйте select_related(), якщо вам потрібно просто підтягнути дані для полів ForeignKey або OneToOne, а prefetch_related() — для більш складних сценаріїв, пов'язаних з ManyToMany або зворотним зв'язком.
4. Комбінування select_related() та prefetch_related()
Іноді одне без іншого не обходиться. Наприклад, якщо у нас є зв'язок виду:
- Книга (
Book) -> Автор (Author) черезForeignKey. - Книга також може мати теги (
ManyToMany).
Ми можемо використовувати обидва методи:
books = Book.objects.select_related('author').prefetch_related('tags')
for book in books:
print(f"Книга: {book.title}, Автор: {book.author.name}, Теги: {[tag.name for tag in book.tags.all()]}")
5. Практика
Давай створимо невеликий приклад. У нас є три моделі: Author, Book та Tag.
Моделі
class Author(models.Model):
name = models.CharField(max_length=255)
class Tag(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag)
Завантаження даних без оптимізації
books = Book.objects.all()
for book in books:
print(book.title, book.author.name, [tag.name for tag in book.tags.all()])
Результат: величезна кількість запитів (один для книг, по одному для кожного автора і для тегів).
Оптимізований варіант
books = Book.objects.select_related('author').prefetch_related('tags')
for book in books:
print(book.title, book.author.name, [tag.name for tag in book.tags.all()])
Результат: всього три запити. Дуже елегантно і швидко!
Підсумки
Методи select_related() та prefetch_related() — це ключові інструменти в оптимізації запитів Django ORM. Вони допоможуть вам уникнути типових помилок початківців розробників, таких як "N+1 запит". Використовуйте їх правильно, і ваш сервер (а, можливо, і фронтенд) буде вам вдячний.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ