FastAPI: From Basics to Production · Databases with SQLAlchemy 2.0 and Alembic

Relationships and Avoiding N+1 Queries

Eager-load related data explicitly in async SQLAlchemy.

Lazy loading doesn't work implicitly with async sessions, and in sync code it causes N+1 queries. Load relationships explicitly with selectinload (a second IN query — best for collections) or joinedload (a JOIN — good for many-to-one). Set lazy="raise" on relationships to fail fast when something isn't eagerly loaded.

from sqlalchemy import select
from sqlalchemy.orm import joinedload, selectinload


async def orders_with_items(db: AsyncSession, user_id: int) -> list[Order]:
    stmt = (
        select(Order)
        .where(Order.user_id == user_id)
        .options(
            selectinload(Order.items).joinedload(OrderItem.product),   # collection + many-to-one
            joinedload(Order.user),
        )
    )
    return list(await db.scalars(stmt))


class Order(Base):
    # ...
    items: Mapped[list["OrderItem"]] = relationship(lazy="raise")   # fail fast if not eager-loaded

Common pitfalls

  • MissingGreenlet errors from accessing unloaded relationships in async code.
  • joinedload on large collections, multiplying rows.
  • Serializing nested responses without loading their relationships.

Key points

  • selectinload for collections
  • joinedload for many-to-one
  • lazy='raise' surfaces mistakes

Further reading: SQLAlchemy: Relationship loading

Practice questions

When would you choose selectinload over joinedload?

Answer

For one-to-many collections: selectinload issues one extra IN query and avoids duplicating parent rows that a JOIN would produce.

What does lazy='raise' do?

Answer

Accessing the relationship without eager loading raises an error immediately, catching N+1 problems and async lazy-load errors during development.

Open in the app to mark lessons complete, take notes and track your progress.