ORM экономит время, но легко превращает одну страницу в 500 SQL-запросов. На собесе Django-разработчика почти всегда есть вопрос про N+1 и разницу select_related и prefetch_related. Senior-уровень добавляет транзакции, блокировки и гонки.
QuerySet ленивый
qs = Order.objects.filter(status='paid') # запроса ещё нет
qs = qs.exclude(total=0).order_by('-created')
for order in qs: # запрос выполняется здесь
...
QuerySet выполняется при итерации, list(), len(), bool(), срезе с шагом, repr. Результат кешируется в самом объекте QuerySet: повторный обход того же объекта не делает запрос, а новый .filter() создаёт новый QuerySet и новый запрос.
exists()вместоif qs:(не грузит строки),count()вместоlen(qs), если строки не нужны.values()иvalues_list()возвращают словари и кортежи без создания моделей.only()иdefer()ограничивают колонки.iterator()для огромных выборок без кеширования.
Проблема N+1
for order in Order.objects.all(): # 1 запрос
print(order.customer.name) # +1 запрос на каждый заказ
Каждое обращение к связанному объекту, которого нет в памяти, делает отдельный запрос. 100 заказов = 101 запрос.
select_related и prefetch_related
Order.objects.select_related('customer') # JOIN, один запрос
Order.objects.prefetch_related('items') # 2 запроса: заказы + все позиции
Order.objects.prefetch_related(
Prefetch('items', queryset=Item.objects.select_related('product'))
)
select_relatedдля ForeignKey и OneToOne («к одному»): делает SQL JOIN.prefetch_relatedдля «ко многим» (обратный FK, ManyToMany): отдельный запросWHERE id IN (...)и склейка в Python.Prefetchпозволяет фильтровать и оптимизировать вложенную выборку.
Найти N+1 помогают django-debug-toolbar, assertNumQueries в тестах, логирование SQL, nplusone и аналоги.
F и Q
from django.db.models import F, Q
Product.objects.filter(stock__lt=F('reserved')) # сравнение колонок
Product.objects.filter(pk=pk).update(stock=F('stock') - 1) # атомарно в БД
Order.objects.filter(Q(status='new') | Q(priority__gte=5))
F ссылается на колонку и позволяет обновлять значения на стороне БД без гонки «прочитал, изменил, записал». Q собирает условия с |, &, ~.
Агрегация и аннотации
from django.db.models import Count, Sum
Customer.objects.annotate(orders_count=Count('orders')).filter(orders_count__gt=5)
Order.objects.aggregate(total=Sum('amount')) # словарь
Осторожно с несколькими annotate по разным связям «ко многим»: JOIN размножает строки, и суммы задваиваются. Решение: подзапросы (Subquery, OuterRef) или Count(..., distinct=True).
Транзакции и блокировки
По умолчанию Django в режиме autocommit: каждый запрос сам по себе транзакция. Для атомарности нескольких операций:
from django.db import transaction
with transaction.atomic():
account = Account.objects.select_for_update().get(pk=pk)
account.balance -= amount
account.save(update_fields=['balance'])
transaction.on_commit(lambda: notify.delay(account.id))
atomicоткатывает всё при исключении, вложенныеatomicсоздают savepoint.select_for_update()блокирует строки до конца транзакции: защита от гонок при списании.on_commitвыполняет побочные эффекты (письма, задачи Celery) только после успешного коммита.ATOMIC_REQUESTS = Trueоборачивает каждый запрос в транзакцию: просто, но держит транзакцию всё время запроса.
Async ORM
Есть асинхронные методы (aget, acreate, acount, async for по QuerySet), но внутри запросы пока выполняются синхронно через поток. Транзакции в async-коде всё ещё требуют синхронной обёртки (sync_to_async).
Частые ошибки
- N+1 в циклах, шаблонах и сериализаторах.
len(qs)илиif qsтам, где хватитcount()иexists().- Обновление счётчиков через Python (
obj.x += 1; obj.save()) вместоF. save()безupdate_fields: перезапись всех полей, гонка с параллельными изменениями.- Задача в Celery, отправленная внутри транзакции до коммита: воркер не видит новые данные.
Что спросят на собесе
- Что такое ленивость QuerySet, когда выполняется запрос.
- Что такое N+1 и как его найти и исправить.
- Разница
select_relatedиprefetch_related. - Зачем
FиQ, как сделать атомарное обновление счётчика. - Как работают
transaction.atomic,select_for_update,on_commit.