Как в SQLAlchemy решить проблему N+1 запросов

«Как в SQLAlchemy решить проблему N+1 запросов» — вопрос из категории Библиотеки и модули, который задают на 10% собеседований Python Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Проблема N+1 запросов возникает, когда ORM выполняет один запрос для получения основной коллекции объектов (1), а затем по одному дополнительному запросу для загрузки связанных данных для каждого из N объектов. Это приводит к значительному падению производительности.

В SQLAlchemy эта проблема решается с помощью стратегий немедленной загрузки (eager loading), которые указываются через опцию .options().

Основные стратегии:

  1. selectinload() (Рекомендуемый способ) Загружает связанные коллекции отдельным запросом, используя WHERE ... IN (...) для ID родительских объектов. Это самый эффективный способ для отношений "один-ко-многим".

    from sqlalchemy.orm import selectinload
    
    # Загрузит всех пользователей и одним доп. запросом все их адреса
    users = session.query(User).options(selectinload(User.addresses)).all()
  2. joinedload() Использует LEFT OUTER JOIN для загрузки связанных данных в том же запросе. Идеально подходит для отношений "многие-к-одному" или "один-к-одному". Может вызывать избыточность данных для коллекций.

    from sqlalchemy.orm import joinedload
    
    # Загрузит все комментарии и сразу присоединит их авторов (пользователей)
    comments = session.query(Comment).options(joinedload(Comment.user)).all()
  3. subqueryload() Работает аналогично selectinload, но использует подзапрос. В современных версиях SQLAlchemy selectinload обычно предпочтительнее.

Глобальная настройка

Стратегию загрузки можно задать по умолчанию в определении модели с помощью параметра lazy в relationship:

from sqlalchemy.orm import relationship

class User(Base):
    # ...
    # Всегда использовать `selectinload` при доступе к addresses
    addresses = relationship("Address", back_populates="user", lazy="selectin")