Короткий ответ
Это классическая проблема N+1: один запрос получает список заказов, а затем для каждого заказа отдельно выполняется запрос на связанные товары; решение — заранее подгрузить связанные данные одним или несколькими запросами через select_related/prefetch_related (Django) или joinedload/selectinload (SQLAlchemy).
Как это работает подробнее
ORM по умолчанию делает ленивую загрузку связей — обращение к order.items внутри цикла инициирует отдельный запрос к БД на каждой итерации, вместо того чтобы получить все нужные данные заранее.
Пример кода
example.py
# Проблема: N+1 запрос
orders = Order.objects.all()
for order in orders:
print(order.customer.name) # отдельный SELECT на каждой итерации
for item in order.items.all(): # ещё по одному SELECT на заказ
print(item.name)
# Исправление: JOIN для связи "многие к одному" + отдельный IN-запрос для "один ко многим"
orders = Order.objects.select_related("customer").prefetch_related("items")Что использовать на практике
- Практика. Для диагностики включают логирование SQL-запросов ORM или инструмент вроде django-debug-toolbar/SQLAlchemy echo, видят повторяющийся однотипный запрос с разными ID; для связей «один ко многим» уместнее prefetch_related/selectinload (отдельный запрос с IN), для «многие к одному» — select_related/joinedload (JOIN в одном запросе).
Подводные камни
- Риск. Добавляют select_related ко всем связям без разбора, что увеличивает объём одного запроса и может создать декартово произведение при нескольких связях «многие»; исправляют проблему точечно в одном месте кода, не проверяя, что та же модель используется в других эндпоинтах с тем же паттерном доступа.