Skip to content
cccskills
FreePublished on GitHub

sqlalchemy

Use when working with SQLAlchemy 2.0 in Python - declarative models, queries, sessions, async engines, Alembic migrations, relationship loading strategies, N+1 detection, pool exhaustion, or 2.0 migration

The English summary is being prepared. Showing the original description.

Works withAWSPostgreSQL
See how to install

Files included (7)

  • SKILL.md11.7 KB
  • references/async.md2.6 KB
  • references/common-issues.md8.7 KB
  • references/debugging.md2.0 KB
  • references/migrations.md6.1 KB
  • references/queries.md10.6 KB
  • references/relationships.md7.7 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

SQLAlchemy

Python SQL toolkit and ORM. See official docs for full API reference.

Installation

pip install sqlalchemy[asyncio] alembic
pip install asyncpg  # PostgreSQL async driver
pip install psycopg2-binary  # PostgreSQL sync driver

Engine and Pool Configuration

from sqlalchemy import create_engine

# Basic engine
engine = create_engine("postgresql://user:pass@localhost/db", echo=True)

# Production pool settings
engine = create_engine(
    "postgresql://user:pass@localhost/db",
    pool_size=10,           # Base connection pool
    max_overflow=20,        # Max extra connections under load
    pool_timeout=30,        # Wait time for connection
    pool_recycle=3600,      # Recycle connections after 1 hour
    pool_pre_ping=True,     # Check connection health (critical for pool exhaustion fix)
    echo_pool=True,         # Log pool events for debugging
)

Async Engine

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

async_engine = create_async_engine(
    "postgresql+asyncpg://user:pass@localhost/db",
    echo=True,
)

AsyncSessionLocal = sessionmaker(
    async_engine,
    class_=AsyncSession,
    expire_on_commit=False,
)

Serverless (NullPool)

from sqlalchemy.pool import NullPool

# AWS Lambda, Vercel - no persistent connections
engine = create_engine(DATABASE_URL, poolclass=NullPool)

Declarative Models

from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from typing import Optional, List

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
    email: Mapped[str] = mapped_column(String(255), unique=True)
    created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now())
    
    # Relationship (see Relationship Loading Guide below)
    articles: Mapped[List["Article"]] = relationship(back_populates="author")
    
    def __repr__(self):
        return f"<User(id={self.id}, username='{self.username}')>"

class Article(Base):
    __tablename__ = "articles"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(200))
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    
    author: Mapped["User"] = relationship(back_populates="articles")

Column Types Reference

from sqlalchemy import BigInteger, Text, Numeric, JSON, Enum, LargeBinary

class Product(Base):
    __tablename__ = "products"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(100))
    description: Mapped[str] = mapped_column(Text)
    price: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    metadata: Mapped[dict] = mapped_column(JSON)
    status: Mapped[str] = mapped_column(Enum("draft", "published", name="product_status"))

Sessions

Session Lifecycle (Critical)

One session per request/task, never shared. Sessions are not thread-safe.

# ✅ GOOD: One session per request
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# ✅ GOOD: One AsyncSession per async task
async def process_user(user_id: int):
    async with AsyncSessionLocal() as session:
        result = await session.execute(select(User).where(User.id == user_id))
        return result.scalar_one_or_none()

# ❌ BAD: Module-level shared session (race conditions)
db = SessionLocal()  # Never do this!

Transaction Management

# Implicit transaction (default)
with SessionLocal() as session:
    session.add(user)
    session.commit()

# Explicit transaction (recommended)
with SessionLocal() as session:
    with session.begin():  # Auto-commit/rollback
        session.add(user)

# Nested transaction (SAVEPOINT)
with SessionLocal() as session:
    with session.begin():
        session.add(user)
        with session.begin_nested():
            session.add(related_object)
            # Rolls back to savepoint on exception

expire_on_commit=False Trade-off

SessionLocal = sessionmaker(engine, expire_on_commit=False)

# ✅ Can access attributes after commit
with SessionLocal() as session:
    user = User(name="test")
    session.add(user)
    session.commit()
    print(user.name)  # Works

# Trade-off: Convenient but may return stale data if DB changed externally

Queries (2.0 Style)

from sqlalchemy import select, update, delete

# Select all
stmt = select(User)
users = session.execute(stmt).scalars().all()

# Select with filter
stmt = select(User).where(User.is_active == True)
users = session.execute(stmt).scalars().all()

# Get by primary key
user = session.get(User, 1)  # Returns None if not found

# Get one
stmt = select(User).where(User.username == "john")
user = session.execute(stmt).scalar_one_or_none()

# Update
stmt = update(User).where(User.id == 1).values(email="new@example.com")
session.execute(stmt)
session.commit()

# Delete
stmt = delete(User).where(User.is_active == False)
session.execute(stmt)
session.commit()

See references/queries.md for joins, aggregations, CTEs, window functions, and bulk operations.

Relationship Loading Decision Guide

The single most important performance decision. Wrong choices cause N+1 queries or row explosion.

Strategy Comparison

StrategyQuery CountRow DuplicationWhen to Use
lazy="select" (default)N+1 if iteratedNoAvoid in production - one extra query per relation
joinedload1 queryYes - duplicates parent columnsScalar relations only (one-to-one, many-to-one)
selectinload2-3 queriesNoDefault recommendation - efficient for collections
raiseloadRaises errorNoTests/debugging - catch lazy loads
noload0 queriesNoOptional relations you never need

Why joinedload on Collections Explodes

# Author has 3 articles, each with 2 tags
# joinedload creates: 1 × 3 × 2 = 6 rows (Cartesian product)

stmt = select(Author).options(
    joinedload(Author.articles).joinedload(Article.tags)
)
authors = session.execute(stmt).unique().scalars().all()
# Memory: O(parents × children × grandchildren) - BAD!

Fix: Use selectinload for collections:

stmt = select(Author).options(
    selectinload(Author.articles).selectinload(Article.tags)
)
# Query 1: SELECT authors
# Query 2: SELECT articles WHERE author_id IN (...)
# Query 3: SELECT tags WHERE article_id IN (...)

Decision Table

Do you need the relation?
├─ No → noload or don't include
├─ Yes, always, scalar relation → joinedload
├─ Yes, always, collection → selectinload (default recommendation)
├─ Sometimes → lazy="select" or explicit load per endpoint
└─ In tests → raiseload to catch bugs

The "Load Only What You Serialize" Rule

Never eager-load relations you won't return. Loading full object graphs wastes memory.

# Bad: Load entire graph
stmt = select(User).options(
    joinedload(User.articles).joinedload(Article.comments)
)

# Good: Load only what you return
stmt = select(User.id, User.username)  # No relations

# Or: Load specific relation only
stmt = select(User).options(selectinload(User.articles))
# Don't cascade to Article.comments unless needed

raiseload as Bug Detector

from sqlalchemy.orm import raiseload

# Catch ANY lazy load in tests
stmt = select(User).options(raiseload("*"))
# Test fails if code accesses user.articles without eager loading

noload for Optional Relations

class Order(Base):
    __tablename__ = "orders"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"))
    
    # Rarely needed - most orders don't have extended details
    shipping_address: Mapped[Optional["ShippingAddress"]] = relationship(
        lazy="selectin",  # Load only when explicitly accessed
    )

Before/After: Query Count

Before (N+1):

authors = session.execute(select(Author)).scalars().all()
for author in authors:  # 100 authors
    print(len(author.articles))  # 100 more queries!
# Total: 101 queries

After (selectinload):

stmt = select(Author).options(selectinload(Author.articles))
authors = session.execute(stmt).scalars().all()
for author in authors:
    print(len(author.articles))  # Already loaded
# Total: 2 queries

See references/relationships.md for full session lifecycle rules and async patterns.

2.0 Migration Checklist

Old (1.x)New (2.0)
session.query(User).get(1)session.get(User, 1)
session.query(User).filter(...)session.execute(select(User).where(...))
Query objectselect() + session.execute()
Implicit autocommitExplicit session.commit() required
engine.execute("SELECT ...")Removed - use session.execute(text("SELECT ..."))
text("SELECT ...") without bindstext("SELECT ...").bindparams(...) or :param in string
MetaData()MetaData(naming_convention={...}) for constraints
# Before (1.x)
user = session.query(User).filter(User.id == 1).first()

# After (2.0)
user = session.get(User, 1)
# OR
user = session.execute(select(User).where(User.id == 1)).scalar_one_or_none()

Common Issues

See references/common-issues.md for detailed detection and fixes:

IssueSymptomDetection
N+1 queriesSlow list endpointsecho=True shows repeated queries; assert_num_queries(2) in tests
MissingGreenletError"greenlet_spawn has not been called"Lazy load outside async context
StaleDataError"Could not refresh identity map"Lost updates under concurrency
DetachedInstanceError"Parent instance is not bound to a Session"Access after commit/expire
Pool exhaustion"QueuePool limit reached"echo_pool=True logs; too many open connections
Missing tables"relation does not exist"Migration not run; check with inspect(engine).get_table_names()

See references/common-issues.md for:

  • N+1 detection via echo=True, query counting, assert_num_queries
  • MissingGreenletError - lazy load outside async context fix
  • StaleDataError - optimistic/pessimistic locking patterns
  • DetachedInstanceError - access within session, eager load, expire_on_commit=False
  • Pool exhaustion - pool_size, max_overflow, pool_pre_ping, NullPool for serverless

Deep Dives

Load these reference files on demand for detailed coverage:

  • references/queries.md - Joins, aggregations, CTEs, window functions, bulk operations, PostgreSQL optimization
  • references/relationships.md - Full relationship loading decision guide, session lifecycle rules, async patterns
  • references/common-issues.md - Real failure modes with detection and fixes (N+1, MissingGreenlet, StaleDataError, DetachedInstanceError, pool exhaustion)
  • references/migrations.md - Alembic setup, migration creation, testing strategies

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Use when building Python async HTTP services or clients with aiohttp - web server routing, middleware, WebSocket, SSE, streaming, client sessions, pytest-aiohttp testing, or troubleshooting SSL and timeout issues

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

Use when doing structural code search and rewriting - ast-grep linting, refactoring, multi-language patterns

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

Use when building GBA games with the BPCore Lua engine - entity, sprite and tilemap functions, SRAM save and load, link cable multiplayer protocol, camera and scrolling, or optimization patterns

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

celery

Free

Use when running background tasks with Celery - worker and broker configuration (Redis, RabbitMQ), task routing by name vs queue, chains/groups/chords, retry patterns (autoretry_for, retry_backoff), acks_late semantics, failure detection, and monitoring with Flower

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

django

Free

Use when building Django applications - security hardening, authentication and permissions, ORM optimization, PostgreSQL features, Django 6.0, migrations, testing, and ecosystem libraries

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

Use when customizing Django Admin - save_formset, get_search_results, formsets, queryset optimization, db_index, custom URLs

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

See all skills by CodeAtCode

Report a problem with this skill