Python

Database Architecture: SQLAlchemy, Migrations & Performance

Production database patterns with SQLAlchemy 2.0 and SQLModel. Master async database connections, Alembic migrations, connection pooling, transaction management, N+1 prevention, and query optimization.

By TechCoder TeamLast updated: 2026-06-02
In a Nutshell

Production database patterns with SQLAlchemy 2.0 and SQLModel. Master async database connections, Alembic migrations, connection pooling, transaction management, N+1 prevention, and query optimization. This hands-on tutorial focuses on practical implementation of database architecture: sqlalchemy, migrations & performance concepts.

Database Architecture: SQLAlchemy, Migrations & Performance

ORMs are powerful but dangerous without understanding what happens underneath. This chapter covers production database patterns: async sessions, migration strategies, connection pooling, and the performance patterns that keep your API fast at scale.

SQLAlchemy 2.0 — The Modern Way

SQLAlchemy 2.0 (2023) unified the Core and ORM APIs with a consistent select-based approach:

# ❌ Old SQLAlchemy 1.x style (deprecated)
users = session.query(User).filter(User.age > 18).all()

# ✅ SQLAlchemy 2.0 style
from sqlalchemy import select, update, delete

users = session.execute(
    select(User).where(User.age > 18)
).scalars().all()

Async Database Architecture

FastAPI's async nature demands async database access. Synchronous ORM calls block the event loop:

PYTHON PLAYGROUND
⏳ Loading editor…

SQLModel — The Best of Both Worlds

SQLModel combines SQLAlchemy ORM with Pydantic validation in a single model class:

from sqlmodel import SQLModel, Field, Relationship, Session, select

class User(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    username: str = Field(index=True, unique=True)
    email: str
    is_active: bool = True
    
    # Relationship
    posts: list["Post"] = Relationship(back_populates="author")

class Post(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    content: str
    user_id: int = Field(foreign_key="user.id")
    
    author: User = Relationship(back_populates="posts")

# Same model works for API AND database!
@app.post("/users/", response_model=UserRead)
def create_user(user: UserCreate, session: Session = Depends(get_session)):
    db_user = User.model_validate(user)
    session.add(db_user)
    session.commit()
    session.refresh(db_user)
    return db_user

Alembic Migrations — Version Your Schema

Never modify your database schema manually. Alembic tracks every change as a migration file:

# Install
pip install alembic

# Initialize
alembic init alembic

# Generate migration from model changes
alembic revision --autogenerate -m "Add users table"

# Apply migration
alembic upgrade head

# Rollback one version
alembic downgrade -1

Migration file example:

# alembic/versions/abc123_add_users.py
def upgrade():
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('username', sa.String(50), unique=True, nullable=False),
        sa.Column('email', sa.String(100), nullable=False),
        sa.Column('is_active', sa.Boolean(), default=True),
        sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
    )
    op.create_index('ix_users_username', 'users', ['username'])

def downgrade():
    op.drop_table('users')

Query Optimization Patterns

PYTHON PLAYGROUND
⏳ Loading editor…

Connection Pool Configuration

For production with SQLAlchemy:

from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker

engine = create_async_engine(
    DATABASE_URL,
    pool_size=20,              # Base pool connections
    max_overflow=10,            # Extra connections under load
    pool_pre_ping=True,         # Verify connection before use
    pool_recycle=3600,          # Recycle connections hourly
    echo=False,                 # Set True for debugging
    connect_args={
        "statement_cache_size": 0,   # Required for PostgreSQL
        "prepared_statement_cache_size": 0,
    }
)

# Session factory — create per request
AsyncSessionLocal = async_sessionmaker(
    engine,
    expire_on_commit=False,     # Don't expire after commit
    autoflush=False,            # Manual flush control
)

AI Mentor

Confused about "SQLAlchemy SQLModel async database migrations Alembic connection pooling N+1 query optimization"? Ask our AI mentor for a simplified explanation.

Quiz

Quiz

Question 1 of 10

What is the N+1 query problem?

Database needs N+1 connections
Fetching N users then querying their posts one-by-one creates N extra queries
Having more than N indexes
Inserting N+1 rows at once

Key Takeaways

Async database sessions keep your event loop unblocked.
Connection pooling reuses connections — don't create per request.
Eager loading (selectinload) prevents the N+1 performance trap.
Alembic migrations version your schema like you version code.
Keyset pagination scales to millions of rows; OFFSET doesn't.
Index strategically — index what you query, profile with EXPLAIN.

Keep coding! 🚀