Django ORM: оптимізація запитів для максимальної продуктивності
Повний посібник з оптимізації запитів Django ORM. select_related, prefetch_related, індекси, аналіз проблеми N+1 та просунуті техніки для високопродуктивних застосунків.

Django ORM пропонує елегантну абстракцію для роботи з базою даних, але саме ця простота може приховувати критичні проблеми з продуктивністю. Погано оптимізований Django-застосунок здатний згенерувати сотні запитів там, де вистачило б одного. Цей посібник розкриває основні техніки виявлення та усунення таких проблем.
Перш ніж оптимізувати, потрібно виміряти. Використання django-debug-toolbar у середовищі розробки дає змогу побачити кожен згенерований SQL-запит і швидко виявити вузькі місця.
Розуміння проблеми N+1
Проблема N+1 — найпоширеніша пастка під час роботи з ORM. Вона виникає, коли початковий запит отримує N об'єктів, а потім виконується ще N запитів для доступу до зв'язків кожного з них. Така мультиплікація запитів суттєво знижує продуктивність.
# models.py
from django.db import models
class Author(models.Model):
"""Модель, що представляє автора книги."""
name = models.CharField(max_length=200)
email = models.EmailField(unique=True)
bio = models.TextField(blank=True)
def __str__(self):
return self.name
class Book(models.Model):
"""Модель книги разом з її автором."""
title = models.CharField(max_length=300)
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
related_name='books'
)
published_date = models.DateField()
isbn = models.CharField(max_length=13, unique=True)
def __str__(self):
return self.titleЦі прості моделі дають змогу наочно проілюструвати проблему N+1.
# views.py - Проблемний приклад
def list_books_bad(request):
"""❌ Ця в'юшка генерує N+1 запитів."""
books = Book.objects.all() # 1 запит за книгами
for book in books:
# Кожен доступ до book.author виконує SQL-запит
print(f"{book.title}, автор {book.author.name}")
# Зі 100 книгами = 101 SQL-запит!
return render(request, 'books/list.html', {'books': books})Код виглядає безневинним, але на кожну книгу генерує запит для отримання її автора.
Розв'язання N+1 за допомогою select_related
Метод select_related виконує SQL JOIN і отримує пов'язані дані одним запитом. Він працює для зв'язків ForeignKey та OneToOneField.
# views.py - Оптимізоване рішення з select_related
def list_books_optimized(request):
"""✅ Ця в'юшка генерує єдиний запит з JOIN."""
# select_related виконує SQL JOIN
books = Book.objects.select_related('author').all()
for book in books:
# Без додаткових запитів: автор уже завантажений
print(f"{book.title}, автор {book.author.name}")
# Загалом: 1 SQL-запит, незалежно від кількості книг
return render(request, 'books/list.html', {'books': books})Згенерований SQL-запит використовує LEFT OUTER JOIN, щоб отримати авторів разом із книгами.
# Ланцюжок select_related для вкладених зв'язків
# models.py
class Publisher(models.Model):
name = models.CharField(max_length=200)
country = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
# views.py
def list_books_with_details(request):
"""Отримує книги, авторів та видавництва одним запитом."""
books = Book.objects.select_related(
'author', # ForeignKey до Author
'publisher' # ForeignKey до Publisher
).all()
return render(request, 'books/list.html', {'books': books})Кілька зв'язків можна оптимізувати одночасно, перерахувавши їх у select_related.
Оптимізація зв'язків ManyToMany через prefetch_related
Для ManyToMany-зв'язків чи зворотних зв'язків (ForeignKey з протилежного боку) prefetch_related виконує окремі, але оптимізовані запити, уникаючи громіздких JOIN-ів.
# models.py
class Tag(models.Model):
"""Теги для категоризації книг."""
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag, related_name='books')ManyToMany-зв'язок між Book і Tag вимагає prefetch_related для ефективної оптимізації.
# views.py - Оптимізація ManyToMany
def list_books_with_tags(request):
"""✅ Отримує книги та їхні теги ефективно."""
books = Book.objects.prefetch_related('tags').all()
for book in books:
# Теги попередньо завантажені, без додаткових запитів
tag_names = [tag.name for tag in book.tags.all()]
print(f"{book.title}: {', '.join(tag_names)}")
# Загалом: 2 запити (книги + теги) незалежно від обсягу
return render(request, 'books/list.html', {'books': books})prefetch_related виконує окремий запит за тегами, а потім поєднує дані вже у Python.
Для ForeignKey та OneToOne варто використовувати select_related (SQL JOIN). Для ManyToMany та зворотних зв'язків — prefetch_related (окремі запити). Обидва метода можна поєднувати в одному QuerySet.
Налаштування попереднього завантаження за допомогою об'єктів Prefetch
Об'єкт Prefetch забезпечує тонкий контроль над попередньо завантаженими даними: фільтрування, сортування і навіть обмеження кількості результатів.
# views.py
from django.db.models import Prefetch
def list_authors_with_recent_books(request):
"""Отримує авторів лише з їхніми свіжими книгами."""
# Спеціальний Prefetch: лише книги з 2025 року
recent_books_prefetch = Prefetch(
'books',
queryset=Book.objects.filter(
published_date__year__gte=2025
).order_by('-published_date'),
to_attr='recent_books' # Зберігається в окремому атрибуті
)
authors = Author.objects.prefetch_related(
recent_books_prefetch
).all()
for author in authors:
# Доступ через спеціальний атрибут
for book in author.recent_books:
print(f"{author.name}: {book.title}")
return render(request, 'authors/list.html', {'authors': authors})Атрибут to_attr зберігає результати у списку Python замість звичайного менеджера.
# Поглиблене поєднання: select_related + Prefetch
def list_authors_complete(request):
"""Повний приклад багаторівневої оптимізації."""
authors = Author.objects.prefetch_related(
Prefetch(
'books',
queryset=Book.objects.select_related(
'publisher' # Оптимізує також видавництво кожної книги
).prefetch_related(
'tags' # І теги кожної книги
).filter(published_date__year=2026)
)
).all()
return render(request, 'authors/complete.html', {'authors': authors})Такий підхід суттєво зменшує кількість запитів для складних структур даних.
Готовий до співбесід з Django?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Обмеження стовпців через only() та defer()
За замовчуванням Django отримує всі стовпці таблиці. У моделях з великою кількістю полів або з масивними полями обмеження стовпців підвищує продуктивність.
# views.py
def list_books_minimal(request):
"""Отримує лише потрібні стовпці."""
# only() задає список потрібних стовпців
books = Book.objects.only(
'id',
'title',
'published_date'
).select_related('author')
# Увага: доступ до пропущеного поля викличе запит
for book in books:
print(book.title) # OK, входить у вибірку
# print(book.isbn) # Спричинить додатковий запит
return render(request, 'books/list.html', {'books': books})Метод only() створює «відкладений» об'єкт, який вантажить лише вказані стовпці.
# defer() для виключення певних стовпців
def list_authors_without_bio(request):
"""Виключає великі поля, що рідко використовуються."""
# defer() виключає вказані стовпці
authors = Author.objects.defer(
'bio' # TextField не завантажується
).all()
for author in authors:
print(author.name) # OK
print(author.email) # OK
# author.bio підвантажить поле за потреби
return render(request, 'authors/list.html', {'authors': authors})defer() — протилежність only(): перелічені стовпці не завантажуються одразу.
Оптимізація з values() та values_list()
Коли потрібні лише окремі значення без повних об'єктів моделі, values() і values_list() повертають полегшені словники чи кортежі.
# views.py
def get_book_titles(request):
"""Отримує лише назви у вигляді списку."""
# values_list повертає кортежі
titles = Book.objects.values_list('title', flat=True)
# Результат: ['Книга 1', 'Книга 2', ...]
# values повертає словники
book_data = Book.objects.values('title', 'published_date')
# Результат: [{'title': 'Книга 1', 'published_date': ...}, ...]
return render(request, 'books/titles.html', {'titles': titles})Ці методи дозволяють уникнути створення об'єктів моделі та зменшують споживання пам'яті.
# Поєднання з агрегаціями
from django.db.models import Count, Avg
def get_author_statistics(request):
"""Статистика по авторах без завантаження об'єктів."""
stats = Author.objects.values('name').annotate(
book_count=Count('books'),
avg_year=Avg('books__published_date__year')
).order_by('-book_count')
# Результат: [{'name': 'Автор', 'book_count': 5, 'avg_year': 2024}, ...]
return render(request, 'authors/stats.html', {'stats': stats})Анотація дозволяє виконувати обчислення безпосередньо в базі даних.
Створення індексів для прискорення запитів
Індекси бази даних суттєво прискорюють пошук. Django дозволяє визначати їх безпосередньо в моделях.
# models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=300, db_index=True)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
published_date = models.DateField()
isbn = models.CharField(max_length=13, unique=True)
status = models.CharField(max_length=20, default='available')
class Meta:
# Складені індекси для частих запитів
indexes = [
# Індекс за датою публікації (часто використовується для сортування)
models.Index(
fields=['published_date'],
name='book_pub_date_idx'
),
# Складений індекс для фільтрації за автором + статусом
models.Index(
fields=['author', 'status'],
name='book_author_status_idx'
),
# Частковий індекс: лише доступні книги
models.Index(
fields=['published_date'],
name='book_available_idx',
condition=models.Q(status='available')
),
]
# Сортування за замовчуванням, що використовує індекс
ordering = ['-published_date']Ці індекси покращують продуктивність запитів, що фільтрують за відповідними стовпцями.
Індекси прискорюють читання, але дещо сповільнюють операції запису (INSERT, UPDATE). Створювати їх варто лише на стовпцях, які часто використовуються в WHERE, ORDER BY або JOIN.
Звернення до сирих SQL-запитів за потреби
Для складних запитів або оптимізацій, специфічних для конкретної СКБД, сирі SQL-запити дають повний контроль.
# views.py
from django.db import connection
def get_books_with_raw_sql(request):
"""Сирий запит для особливих випадків."""
# Спосіб 1: raw() для отримання об'єктів моделі
books = Book.objects.raw('''
SELECT b.*, a.name as author_name
FROM library_book b
INNER JOIN library_author a ON b.author_id = a.id
WHERE b.published_date > %s
ORDER BY b.published_date DESC
''', ['2025-01-01'])
return render(request, 'books/list.html', {'books': books})
def execute_custom_query(request):
"""Пряме виконання для запитів, які не є SELECT."""
with connection.cursor() as cursor:
# Запит зі складною агрегацією
cursor.execute('''
SELECT
a.name,
COUNT(b.id) as book_count,
AVG(EXTRACT(YEAR FROM b.published_date)) as avg_year
FROM library_author a
LEFT JOIN library_book b ON b.author_id = a.id
GROUP BY a.id, a.name
HAVING COUNT(b.id) > 2
ORDER BY book_count DESC
''')
results = cursor.fetchall()
return render(request, 'stats.html', {'results': results})Сирі запити обходять ORM, але втрачається переносимість між СКБД.
Аналіз запитів за допомогою django-debug-toolbar
Інструмент django-debug-toolbar дозволяє побачити всі SQL-запити, які генерує в'юшка Django.
# settings.py - Налаштування debug toolbar
INSTALLED_APPS = [
# ... інші застосунки
'debug_toolbar',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
# ... інші middleware
]
# Показувати toolbar для локальних запитів
INTERNAL_IPS = ['127.0.0.1']
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.sql.SQLPanel', # SQL-запити
'debug_toolbar.panels.timer.TimerPanel', # Час виконання
'debug_toolbar.panels.cache.CachePanel', # Кеш
]Таке налаштування активує найкорисніші панелі для оптимізації.
# Логування SQL-запитів у середовищі розробки
# settings.py
LOGGING = {
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
}Таке логування виводить кожен SQL-запит у консоль, що допомагає виявляти проблеми N+1.
Кешування QuerySet'ів
Для даних, до яких часто звертаються та які рідко змінюються, кеш дозволяє уникнути повторюваних запитів.
# views.py
from django.core.cache import cache
from django.views.decorators.cache import cache_page
def list_featured_books(request):
"""Отримує рекомендовані книги з кешем."""
cache_key = 'featured_books_list'
# Спроба отримати з кешу
books = cache.get(cache_key)
if books is None:
# Cache miss: виконати запит
books = list(
Book.objects.select_related('author')
.filter(featured=True)
.order_by('-published_date')[:10]
)
# Зберегти у кеші на 5 хвилин
cache.set(cache_key, books, timeout=300)
return render(request, 'books/featured.html', {'books': books})
# Декоратор для кешування всієї в'юшки
@cache_page(60 * 15) # Кеш 15 хвилин
def list_all_tags(request):
"""Перелік усіх тегів (рідко змінювані дані)."""
tags = Tag.objects.annotate(
book_count=Count('books')
).order_by('-book_count')
return render(request, 'tags/list.html', {'tags': tags})Кеш зменшує навантаження на базу даних для статичних даних.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Висновок
Оптимізація запитів Django ORM ґрунтується на кількох ключових принципах:
✅ Виміряти перед оптимізацією: використовувати django-debug-toolbar для виявлення реальних проблем
✅ Усунути проблему N+1: select_related для ForeignKey, prefetch_related для ManyToMany
✅ Обмежити дані: only(), defer(), values() завантажують лише необхідне
✅ Розумно індексувати: створювати індекси на стовпцях, що часто фільтруються або сортуються
✅ Стратегічне кешування: рідко змінювані дані виграють від кешу Django
✅ Сирі запити: як крайній засіб для оптимізацій, специфічних для бази
При методичному застосуванні ці техніки перетворюють повільний Django-застосунок на ефективну систему, здатну обробляти великі обсяги даних. Django ORM залишається потужним інструментом, коли його тонкощі засвоєні.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

Django 5.2: Власна Middleware та Обробка Сигналів для Технічних Співбесід
Повний посібник з Django 5.2: побудова власної middleware, використання сигналів post_save та pre_save, асинхронна middleware, користувацькі сигнали та типові питання для технічних співбесід з прикладами коду.

Питання на співбесіді з Django та Python: Топ 25 у 2026 році
25 найпоширеніших питань на співбесіді з Django та Python. ORM, представлення, middleware, DRF, сигнали та оптимізація з детальними відповідями та прикладами коду.

Django 5: Створення REST API з Django REST Framework
Повний посібник зі створення професійного REST API з Django 5 та DRF. Серіалізатори, ViewSet'и, автентифікація JWT та найкращі практики.