Что такое ленивый и жадный способы загрузки в ORM
Что проверяют
Знаете ли вы, во сколько запросов обходится обращение к связанным объектам, и управляете ли этим осознанно. За вопросом почти всегда стоит N+1, самая частая причина медленного обработчика, который на тестовых данных летает.
Как отвечать
Ленивая загрузка подтягивает связанные объекты в момент обращения к атрибуту. Удобно, но обход сотни родителей с чтением связи даёт сотню дополнительных запросов плюс исходный, это и есть N+1.
Жадная загрузка берёт связанные объекты сразу: соединением в том же запросе или отдельным дополнительным. Число запросов становится постоянным и не зависит от размера выборки.
В SQLAlchemy ленивая стратегия стоит по умолчанию, жадная задаётся в запросе или в описании связи:
# один запрос с JOIN, хорошо для связи многие к одному
session.scalars(select(User).options(joinedload(User.profile)))
# два запроса, второй с IN, хорошо для коллекций
session.scalars(select(User).options(selectinload(User.orders)))В Django те же роли играют select_related (соединение, для внешнего ключа и связи один к одному) и prefetch_related (отдельный запрос, для многие ко многим и обратных связей).
Правило выбора простое: связь нужна всегда и она одиночная, берите соединение; связь это коллекция, берите отдельный запрос с IN; связь нужна изредка, оставляйте ленивую.
Где спотыкаются
Лечат N+1 поголовной жадной загрузкой. Соединение по коллекции размножает строки родителя, тянет лишний объём по сети и ломает постраничность: ограничение по числу строк начинает резать не родителей, а строки соединения.
Забывают про время жизни сессии. Объект отдали за пределы блока сессии, там обратились к ленивой связи и получили ошибку отсоединённого объекта. В асинхронном режиме SQLAlchemy неявная ленивая загрузка запрещена и падает сразу, а не деградирует тихо.
Не измеряют. Вместо догадок включают журнал SQL или счётчик запросов в тесте, а неожиданную ленивую загрузку закрывают явным запретом на уровне связи, чтобы она падала на ревью, а не в проде.