Ответ
Проблема N+1 запросов возникает, когда ORM выполняет один запрос для получения основной коллекции объектов (1), а затем по одному дополнительному запросу для загрузки связанных данных для каждого из N объектов. Это приводит к значительному падению производительности.
В SQLAlchemy эта проблема решается с помощью стратегий немедленной загрузки (eager loading), которые указываются через опцию .options().
Основные стратегии:
-
selectinload()(Рекомендуемый способ) Загружает связанные коллекции отдельным запросом, используяWHERE ... IN (...)для ID родительских объектов. Это самый эффективный способ для отношений "один-ко-многим".from sqlalchemy.orm import selectinload # Загрузит всех пользователей и одним доп. запросом все их адреса users = session.query(User).options(selectinload(User.addresses)).all() -
joinedload()ИспользуетLEFT OUTER JOINдля загрузки связанных данных в том же запросе. Идеально подходит для отношений "многие-к-одному" или "один-к-одному". Может вызывать избыточность данных для коллекций.from sqlalchemy.orm import joinedload # Загрузит все комментарии и сразу присоединит их авторов (пользователей) comments = session.query(Comment).options(joinedload(Comment.user)).all() -
subqueryload()Работает аналогичноselectinload, но использует подзапрос. В современных версиях SQLAlchemyselectinloadобычно предпочтительнее.
Глобальная настройка
Стратегию загрузки можно задать по умолчанию в определении модели с помощью параметра lazy в relationship:
from sqlalchemy.orm import relationship
class User(Base):
# ...
# Всегда использовать `selectinload` при доступе к addresses
addresses = relationship("Address", back_populates="user", lazy="selectin") Ответ 18+ 🔞
Давай разберём эту классическую проблему, которая может превратить твоё приложение из шустрой ракеты в телегу с квадратными колёсами. Проблема N+1 запросов, ёпта!
Представь себе: ты запрашиваешь список пользователей — это один запрос. А потом в шаблоне или логике для каждого юзера ты обращаешься к его адресам (user.addresses). ORM, такая добрая душа, для КАЖДОГО пользователя делает ОТДЕЛЬНЫЙ запрос в базу, чтобы подтянуть его адреса. Получается 1 запрос на список + N запросов на адреса. Если пользователей 1000, то запросов будет 1001. Это пиздец, а не производительность. База захлебнётся, а приложение будет отвечать, как будто его сервера находятся на Марсе и связь через дозорного ворона.
К счастью, в SQLAlchemy с этим борются, как умеют. Есть штуки, которые называются стратегии немедленной загрузки (eager loading). Всё это хозяйство настраивается через .options() в запросе. Сейчас разжуём.
Основные приёмы, чтобы не выстрелить себе в ногу:
-
selectinload()(Самый часто рекомендуемый) Это, блядь, почти волшебство. ORM сначала одним запросом вытащит всех пользователей, запомнит их ID, а потом ВТОРЫМ запросом вытащит ВСЕ адреса для этих ID, используяWHERE address.user_id IN (... список ID ...). Вместо 1001 запроса — всего 2. Красота! Идеально для отношений "один-ко-многим".from sqlalchemy.orm import selectinload # Загрузит всех юзеров и одним махом все их адреса users = session.query(User).options(selectinload(User.addresses)).all() -
joinedload()Старый добрыйLEFT OUTER JOIN. Он присоединяет связанную таблицу прямо в основном запросе. Отлично работает для отношений "многие-к-одному" или "один-к-одному". Но вот для коллекций ("один-ко-многим") будь осторожен: если у одного пользователя 10 адресов, то в результатах будет 10 строк с дублирующейся информацией о пользователе. Может раздуть результат, но запрос всё равно один.from sqlalchemy.orm import joinedload # Загрузит все комменты и сразу прицепит к каждому автора comments = session.query(Comment).options(joinedload(Comment.user)).all() -
subqueryload()Похож наselectinload, но использует подзапрос. Раньше был в ходу, но сейчасselectinloadчаще оказывается шустрее и надёжнее, особенно на больших данных. Но знать о его существовании не помешает.
А можно сделать это поведение по умолчанию?
А то можно! Чтобы не писать .options() в каждом втором запросе, можно прописать стратегию прямо в модели. Параметр lazy в relationship — твой друг.
from sqlalchemy.orm import relationship
class User(Base):
# ...
# Теперь при обращении к addresses всегда будет срабатывать selectinload
addresses = relationship("Address", back_populates="user", lazy="selectin")
Вот и вся магия. Главное — не забывать об этом, а то потом сидишь и думаешь: "Чё это у меня всё так медленно, я же всего 1000 записей вывожу?". А там, оказывается, ORM втихаря настреляла ещё 100000 запросов. Пиздец, да и только.