Протягом останніх лекцій ми познайомилися з основами агрегацій у Django ORM: навчилися використовувати функції aggregate() для підрахунку, знаходження середнього значення та інших операцій, а також заглибилися в роботу з анотаціями через annotate() для додавання обчислюваних полів до результатів запитів. Ми також розглянули фільтрацію даних з анотаціями, освоїли комбінацію Q-об'єктів для складних логічних умов та F-об'єктів для роботи з полями моделі всередині одного запиту. Це були важливі кроки, які дозволили поглянути на ORM-запити з гнучкістю та потужністю аналітичних інструментів.
Сьогодні ми завершуємо цю тему, зосереджуючись на оптимізації запитів з агрегаціями. Адже неважливо, наскільки гарний чи складний ваш запит — якщо він гальмує додаток, користувачі навряд чи будуть у захваті.
Основи оптимізації запитів
1. Уникайте зайвих запитів
У Django ORM часто виникають ситуації, коли одна дія породжує багато окремих запитів до бази. Це називається "N+1 проблема". Приклад:
# Приклад: отримання всіх пов'язаних книг для авторів
authors = Author.objects.all()
for author in authors:
books = author.book_set.all() # Кожен виклик створює окремий запит!
print(f"{author.name} написав {books.count()} книг")
Цей код приведе до (1 + N) запитів, де N — кількість авторів. Оптимізація полягає у використанні select_related() та prefetch_related().
# Оптимізований запит з мінімальною кількістю звернень до бази
authors = Author.objects.prefetch_related('book_set').all()
for author in authors:
books = author.book_set.all() # Дані вже завантажені
print(f"{author.name} написав {books.count()} книг")
У цьому випадку ми зменшуємо навантаження на базу даних.
2. Використовуйте агрегатні функції з розумом
Агрегації можуть бути дуже затратними, особливо якщо працюють з великими обсягами даних. Наприклад:
# Підрахунок загальної кількості книг у всій базі
total_books = Book.objects.aggregate(total=Count('id'))
Важливо розуміти, що таке агрегатні запити і як вони виконуються "під капотом". Кожного разу, коли ви викликаєте aggregate(), Django відправляє один SQL-запит, а результат приходить у вигляді словника. Це набагато краще, ніж тягнути всі рядки в пам'ять, а потім рахувати їх вручну.
Якщо вам потрібна додаткова фільтрація перед агрегацією, додавайте її у QuerySet:
# Підрахунок книг, опублікованих лише у 2023 році
books_2023 = Book.objects.filter(published_year=2023).aggregate(total=Count('id'))
Однак пам'ятайте: якщо дані чутливі до обсягу, краще працювати з преагрегованими таблицями або кешованими даними.
3. Профілюйте запити
Перш ніж оптимізувати запити, потрібно визначити, які з них "гальмують". Django надає інструмент django-debug-toolbar, який можна підключити до вашого проєкту. З його допомогою ви побачите повний список запитів і їх час виконання.
Встановлення та налаштування:
Встановіть бібліотеку:
pip install django-debug-toolbarДодайте у
INSTALLED_APPS:INSTALLED_APPS += ['debug_toolbar']Налаштуйте
MIDDLEWAREі шляхи:MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']У
urls.py:from django.conf import settings from django.urls import include, path if settings.DEBUG: import debug_toolbar urlpatterns = [path('__debug__/', include(debug_toolbar.urls))] + urlpatterns
Після цього ви зможете бачити SQL-запити і одразу оцінювати їх складність.
4. Уникайте "лінивої" завантаження QuerySet
Django ORM використовує "ліниву" завантаження запитів. Це означає, що фактично запит відправляється лише тоді, коли ви намагаєтеся використовувати дані. Іноді це може обернутися великою кількістю окремих запитів.
Приклад:
qs = Book.objects.all() # Запит ще не відправлений
for book in qs: # Запит відправляється тут!
print(book.title)
Щоб одразу отримати всі дані, використовуйте list():
qs = list(Book.objects.all()) # Примусове завантаження даних
for book in qs:
print(book.title)
5. Кешуйте результати, де це можливо
Якщо ви виконуєте один і той самий запит кілька разів, має сенс закешувати його результати. Наприклад:
from django.core.cache import cache
# Зберігаємо результат запиту у кеші
top_authors = cache.get('top_authors')
if not top_authors:
top_authors = Author.objects.annotate(book_count=Count('book_set')).order_by('-book_count')[:10]
cache.set('top_authors', top_authors, 60 * 15) # Кеш на 15 хвилин
Кешування запитів зменшує навантаження на базу даних і прискорює додаток.
6. Використовуйте індексування
Індекси відіграють ключову роль у прискоренні складних запитів. Наприклад, якщо ви часто фільтруєте книги за роком публікації, має сенс створити індекс:
# models.py
class Book(models.Model):
title = models.CharField(max_length=100)
published_year = models.IntegerField(db_index=True)
Індекси дозволяють швидше знаходити потрібні дані. Зверніть увагу, що індекси краще додавати одразу при створенні моделі, оскільки додавання індексу до існуючої таблиці може бути затратним.
7. Не зловживайте анотаціями
Хоча анотації є потужним інструментом, вони можуть суттєво уповільнити виконання запитів, якщо додаються складні обчислення. Наприклад:
# Створення анотації середньої оцінки для книг
books = Book.objects.annotate(average_rating=Avg('ratings__value'))
Це чудово працює для невеликого набору даних, але зі збільшенням кількості записів продуктивність може знижуватися. У таких випадках краще використовувати окремі тимчасові таблиці або кешування.
8. Оптимізуйте JOIN-операції
Коли ви використовуєте анотації або агрегації через зв'язки, Django генерує SQL-запити з JOIN-операціями. Ці операції можуть бути важкими для бази даних. Наприклад:
authors = Author.objects.annotate(total_books=Count('book_set'))
Ви можете зменшити складність, якщо використовувати select_related() або prefetch_related() і проводити агрегації на вже підготовлених даних.
Практика оптимізації запитів
Спробуємо зібрати все докупи. Припустимо, ми хочемо створити сторінку статистики, де відображається:
- Загальна кількість книг.
- Середня оцінка для кожної книги.
- 10 найактивніших авторів (за кількістю опублікованих книг).
Реалізація:
from django.db.models import Count, Avg
from django.core.cache import cache
def get_statistics():
# Кешуємо запит на загальну кількість книг
total_books = cache.get('total_books')
if not total_books:
total_books = Book.objects.aggregate(total=Count('id'))['total']
cache.set('total_books', total_books, 60 * 15)
# Отримуємо середню оцінку для кожної книги
books = Book.objects.annotate(average_rating=Avg('ratings__value'))
# 10 найактивніших авторів
top_authors = Author.objects.annotate(book_count=Count('book_set')).order_by('-book_count')[:10]
return total_books, books, top_authors
Цей підхід мінімізує кількість запитів, використовує кешування та запобігає дублюванню операцій.
Таким чином, оптимізація запитів в Django ORM – це не тільки покращення продуктивності, але й мистецтво грамотного використання інструментів, які надає Django. Чим більше ви практикуєтесь, тим краще ваші додатки працюють під навантаженням.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ