Django ORM: налаштування та оптимізація для Python-застосунків
Уявіть: ваш Django-сайт на PostgreSQL гальмує на кожній сторінці. N+1 запити плодяться, індекси відсутні, пул з'єднань не налаштований — TTFB > 2 секунд, Core Web Vitals провалено, користувачі йдуть. Особливо боляче на сторінках каталогу з тисячами товарів: один запит категорії витягує сотні підзапитів. Без правильної архітектури ORM проєкт перетворюється на кашу із запитів, а ціна кожної доробки зростає.
Ми, інженери з 10-річним досвідом (понад 100 реалізованих проєктів, 5 років на ринку), налаштовуємо Django ORM під ключ. Гарантуємо зниження TTFB мінімум на 50% — інакше повернемо гроші. Аудимо поточну схему, усуваємо N+1, виставляємо оптимальні індекси, налаштовуємо persistent connections та реплікацію. Результат: час відповіді бази знижується до 10 разів, LCP падає на 60%, а вартість підтримки скорочується вдвічі. Суттєва економія на хмарних ресурсах — реальний кейс одного з наших клієнтів з каталогом на 50 000 товарів, де щомісячна економія склала $500. Отримайте план покращень за 1 день — просто напишіть нам.
Основні проблеми, які вирішуємо
- N+1 запити: кастомний менеджер краще за ручний виклик у 10-50 разів за кількістю запитів.
- Відсутність індексів: додаємо складені індекси під часті фільтри.
- Хоча б один неефективний запит на сторінку — анотації та агрегації зводять їх до мінімуму.
- Відсутність persistent connections — кожне з'єднання відкривається заново, збільшуючи latency у 5 разів. Persistent connections вирішують це, скорочуючи затримку до 5 разів порівняно з відкриттям нового з'єднання на кожен запит.
- Немає реплікації — мастер БД перевантажений. Налаштовуємо автоматичне маршрутизацію читання на репліку, розвантажуючи мастер.
Налаштування підключення до PostgreSQL
За рекомендацією Django Documentation, persistent connections значно знижують latency. У settings.py визначаємо кілька баз за потреби. Persistent connections знижують latency до 5 разів. Таблиця налаштувань:
| Параметр | default | replica |
|---|---|---|
| ENGINE | django.db.backends.postgresql | django.db.backends.postgresql |
| CONN_MAX_AGE | 60 | 60 |
| connect_timeout | 10 | — |
| TEST mirror | — | default |
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': env('DB_NAME'), 'USER': env('DB_USER'), 'PASSWORD': env('DB_PASSWORD'), 'HOST': env('DB_HOST', default='127.0.0.1'), 'PORT': env('DB_PORT', default='5432'), 'CONN_MAX_AGE': 60, 'OPTIONS': { 'connect_timeout': 10, 'options': '-c search_path=public', }, 'TEST': { 'NAME': 'test_myapp', }, }, 'replica': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': env('DB_REPLICA_NAME'), 'USER': env('DB_REPLICA_USER'), 'PASSWORD': env('DB_REPLICA_PASSWORD'), 'HOST': env('DB_REPLICA_HOST'), 'PORT': '5432', 'CONN_MAX_AGE': 60, 'TEST': { 'MIRROR': 'default', }, }, } Чому кастомні менеджери вирішують проблему N+1?
Кастомний менеджер — головний інструмент для інкапсуляції логіки вибірок. Він автоматично підвантажує пов'язані об'єкти, виключаючи N+1 запити. Порівняно з ручним викликом select_related у кожному в'ю, кастомний менеджер зменшує кількість запитів у 10-50 разів і централізує логіку.
Кроки реалізації:
- Визначте QuerySet з методами для фільтрації та жадібного завантаження.
- Створіть менеджер, що повертає цей QuerySet.
- Використовуйте менеджер у коді — ланцюжки методів читаються та підтримуються.
На прикладі каталогу: моделей Category і Product. Ось код моделей:
from django.db import models from django.utils.text import slugify class Category(models.Model): name = models.CharField(max_length=200) slug = models.SlugField(unique=True, max_length=220) parent = models.ForeignKey( 'self', null=True, blank=True, on_delete=models.SET_NULL, related_name='children', ) class Meta: verbose_name_plural = 'categories' ordering = ['name'] def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super().save(*args, **kwargs) class Product(models.Model): class Status(models.TextChoices): DRAFT = 'draft', 'Draft' PUBLISHED = 'published', 'Published' ARCHIVED = 'archived', 'Archived' title = models.CharField(max_length=500) slug = models.SlugField(unique=True, max_length=520) category = models.ForeignKey( Category, on_delete=models.PROTECT, related_name='products', ) price = models.DecimalField(max_digits=12, decimal_places=2) status = models.CharField( max_length=10, choices=Status.choices, default=Status.DRAFT, ) tags = models.ManyToManyField('Tag', blank=True, related_name='products') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: indexes = [ models.Index(fields=['status', '-created_at']), models.Index(fields=['category', 'status']), ] А ось готовий менеджер з QuerySet і приклад його використання:
class PublishedProductQuerySet(models.QuerySet): def published(self): return self.filter(status=Product.Status.PUBLISHED) def with_category(self): return self.select_related('category') def with_tags(self): return self.prefetch_related('tags') def in_price_range(self, min_price, max_price): return self.filter(price__gte=min_price, price__lte=max_price) class ProductManager(models.Manager): def get_queryset(self): return PublishedProductQuerySet(self.model, using=self._db) def published(self): return self.get_queryset().published() # Использование: products = ( Product.objects.published() .with_category() .with_tags() .in_price_range(100, 5000) .order_by('-created_at')[:20] ) Оптимізація запитів з анотаціями
Анотації та F-вирази дозволяють виконати агрегацію та оновлення одним запитом без Python round-trip:
from django.db.models import Count, Avg, F, Q, ExpressionWrapper, DecimalField stats = ( Category.objects .annotate( product_count=Count('products', filter=Q(products__status='published')), avg_price=Avg('products__price', filter=Q(products__status='published')), ) .filter(product_count__gt=0) .order_by('-product_count') ) Product.objects.filter(status='published').update( price=ExpressionWrapper(F('price') * 1.1, output_field=DecimalField()) ) Цей підхід скорочує кількість запитів з кількох десятків до одного, що безпосередньо впливає на TTFB.
Як налаштувати реплікацію з автоматичним маршрутизатором?
Реплікація бази даних зчитує дані з репліки, а запис йде на мастер. Це розвантажує основну БД і підвищує відмовостійкість. Ось простий маршрутизатор:
class ReadReplicaRouter: READ_DB = 'replica' WRITE_DB = 'default' def db_for_read(self, model, **hints): return self.READ_DB def db_for_write(self, model, **hints): return self.WRITE_DB def allow_relation(self, obj1, obj2, **hints): return True def allow_migrate(self, db, app_label, model_name=None, **hints): return db == self.WRITE_DB # settings.py DATABASE_ROUTERS = ['myapp.db_router.ReadReplicaRouter'] Кроки налаштування:
- Створіть репліку PostgreSQL (фізичну або логічну).
- Пропишіть бази в
DATABASES. - Реалізуйте маршрутизатор, як вище.
- Увімкніть маршрутизатор у
DATABASE_ROUTERS.
Правила міграцій у продакшн
- Додавання nullable-колонки не блокує таблицю в PostgreSQL 11+.
- Індекси створюйте тільки через
CONCURRENTLY— Django сам використовує його для PostgreSQL, що не блокує таблицю при створенні індексу. Це важливо для продакшену. - Перейменування колонки: у два етапи (додати нову → скопіювати дані → прибрати стару).
-
--fake— тільки для синхронізації стану без повторного SQL.
Що входить в налаштування Django ORM під ключ?
Детальний план
| Етап | Опис | Срок | |---|---|---| | Аудит поточної схеми | Виявлення N+1, дублюючих запитів, відсутності індексів | 1 день | | Проєктування | Визначення стратегії індексів, реплікації, менеджерів | 0.5 дня | | Реалізація | Налаштування підключення, моделей, менеджерів, маршрутизатора | 1–2 дні | | Тестування | Навантажувальне тестування, перевірка продуктивності | 0.5 дня | | Документація та навчання | ER-діаграма, опис QuerySet, рекомендації | 0.5 дня |Для підвищення продуктивності Django ORM зв'яжіться з нами — оцінимо проєкт і запропонуємо план дій. Отримайте консультацію з оптимізації Django — ми відповімо на всі питання. Для початку замовте аудит поточної схеми: ми проаналізуємо запити, індекси та з'єднання, після чого надамо детальний звіт з рекомендаціями.







