Гайды

Django ORM: оптимизация запросов, select_related и prefetch_related

N+1, JOIN и отдельные IN-запросы, Prefetch, only/defer, exists, iterator, annotate и индексы в Meta.

~12 мин чтения

Django ORM: оптимизация запросов, select_related и prefetch_related

Django ORM удобен, но легко получить N+1 запросов. Инструменты: select_related (JOIN для ForeignKey/OneToOne), prefetch_related (отдельный запрос + джойн в Python для reverse FK и M2M), only/defer, аннотации и профилирование. База проекта — Django: первый проект; сравнение с «сырым» SQL и планировщиком — EXPLAIN ANALYZE в PostgreSQL. Для асинхронного стека без Django ORM на стороне API часто смотрят на SQLAlchemy 2.0.


1. N+1: как выглядит

python
for order in Order.objects.all():
    print(order.customer.name)  # каждый раз SELECT к customer

Решение — заранее подтянуть связь.


2. select_related (SQL JOIN)

Для ForeignKey и OneToOne в сторону «один объект»:

python
orders = Order.objects.select_related("customer").all()

Один запрос с JOIN вместо 1+N.


3. prefetch_related (отдельный IN)

Для reverse ForeignKey, ManyToMany:

python
authors = Author.objects.prefetch_related("books").all()

Два запроса: авторы, затем книги с WHERE author_id IN (...).

Prefetch объект

python
from django.db.models import Prefetch

qs = Book.objects.filter(published=True)
authors = Author.objects.prefetch_related(
    Prefetch("books", queryset=qs, to_attr="published_books")
)

to_attr — кэш в атрибуте списка без повторных запросов в шаблоне.


4. only / defer

Уменьшить объём строк:

python
Product.objects.only("id", "name")
Product.objects.defer("heavy_json_field")

Осторожно: при обращении к «выключенному» полю будет дополнительный запрос.


5. exists / count

python
if Order.objects.filter(customer_id=1).exists():
    ...

exists() быстрее, чем count() > 0. Для count() без фильтра на больших таблицах — дорого.


6. iterator() для больших выборок

python
for row in HugeModel.objects.iterator(chunk_size=2000):
    process(row)

Не загружает весь queryset в память.


7. annotate и агрегации на БД

python
from django.db.models import Count

Author.objects.annotate(n_books=Count("books")).filter(n_books__gte=3)

Считать на Python в цикле хуже, если можно в SQL.


8. Отладка запросов

python
from django.db import connection
print(len(connection.queries))

# Временно в settings: LOGGING для django.db.backends

django-debug-toolbar в dev — визуализация SQL и дублирования.


9. Индексы в модели

python
class Meta:
    indexes = [
        models.Index(fields=["status", "-created_at"]),
    ]

Согласуйте с реальными фильтрами и планами на уровне PostgreSQL — см. EXPLAIN ANALYZE.


10. Массовые вставки и обновления

bulk_create / bulk_update снижают round-trip к БД; помните про лимиты параметров в PostgreSQL и разбивайте на чанки. update_or_create / get_or_create удобны, но под нагрузкой дают уникальные гонки — для критичных путей иногда лучше явный INSERT ... ON CONFLICT через raw() / SQLAlchemy.


11. Чек-лист

  • Списки с FK — почти всегда select_related.
  • Списки с M2M / reverse — prefetch_related с узким queryset.
  • Пагинация на больших таблицах (LIMIT/cursor).
  • Нет «магических» .all() в API без лимита.

Дальше: Первый проект Django · Django REST Framework · тег Django