# Django ORM: оптимізація запитів для максимальної продуктивності > Повний посібник з оптимізації запитів Django ORM. select_related, prefetch_related, індекси, аналіз проблеми N+1 та просунуті техніки для високопродуктивних застосунків. - Published: 2026-02-15 - Updated: 2026-04-28 - Author: SharpSkill - Tags: django, django orm, python, оптимізація бази даних, продуктивність - Reading time: 14 min --- Django ORM пропонує елегантну абстракцію для роботи з базою даних, але саме ця простота може приховувати критичні проблеми з продуктивністю. Погано оптимізований Django-застосунок здатний згенерувати сотні запитів там, де вистачило б одного. Цей посібник розкриває основні техніки виявлення та усунення таких проблем. > **Золоте правило оптимізації** > > Перш ніж оптимізувати, потрібно виміряти. Використання django-debug-toolbar у середовищі розробки дає змогу побачити кожен згенерований SQL-запит і швидко виявити вузькі місця. ## Розуміння проблеми N+1 Проблема N+1 — найпоширеніша пастка під час роботи з ORM. Вона виникає, коли початковий запит отримує N об'єктів, а потім виконується ще N запитів для доступу до зв'язків кожного з них. Така мультиплікація запитів суттєво знижує продуктивність. ```python # 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. ```python # 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. ```python # 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, щоб отримати авторів разом із книгами. ```python # Ланцюжок 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-ів. ```python # 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` для ефективної оптимізації. ```python # 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. > **select_related vs prefetch_related** > > Для ForeignKey та OneToOne варто використовувати `select_related` (SQL JOIN). Для ManyToMany та зворотних зв'язків — `prefetch_related` (окремі запити). Обидва метода можна поєднувати в одному QuerySet. ## Налаштування попереднього завантаження за допомогою об'єктів Prefetch Об'єкт `Prefetch` забезпечує тонкий контроль над попередньо завантаженими даними: фільтрування, сортування і навіть обмеження кількості результатів. ```python # 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 замість звичайного менеджера. ```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}) ``` Такий підхід суттєво зменшує кількість запитів для складних структур даних. ## Обмеження стовпців через only() та defer() За замовчуванням Django отримує всі стовпці таблиці. У моделях з великою кількістю полів або з масивними полями обмеження стовпців підвищує продуктивність. ```python # 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()` створює «відкладений» об'єкт, який вантажить лише вказані стовпці. ```python # 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()` повертають полегшені словники чи кортежі. ```python # 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}) ``` Ці методи дозволяють уникнути створення об'єктів моделі та зменшують споживання пам'яті. ```python # Поєднання з агрегаціями 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 дозволяє визначати їх безпосередньо в моделях. ```python # 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-запити дають повний контроль. ```python # 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. ```python # 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', # Кеш ] ``` Таке налаштування активує найкорисніші панелі для оптимізації. ```python # Логування SQL-запитів у середовищі розробки # settings.py LOGGING = { 'version': 1, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db.backends': { 'level': 'DEBUG', 'handlers': ['console'], }, }, } ``` Таке логування виводить кожен SQL-запит у консоль, що допомагає виявляти проблеми N+1. ## Кешування QuerySet'ів Для даних, до яких часто звертаються та які рідко змінюються, кеш дозволяє уникнути повторюваних запитів. ```python # 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 залишається потужним інструментом, коли його тонкощі засвоєні. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/django/django-orm-optimizing-queries