PostgreSQL schema design with SQLModel. Use when designing database schemas, creating ERDs, defining models, planning migrations, or reviewing table structures...
import uuid
from datetime import datetime
from sqlmodel import Field, Relationship, SQLModel
class TimestampMixin(SQLModel):
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
class User(TimestampMixin, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
email: str = Field(unique=True, index=True)
name: str = Field(index=True)
class Post(TimestampMixin, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
title: str = Field(index=True)
author_id: uuid.UUID = Field(foreign_key="user.id", index=True)
author: User | None = Relationship(back_populates="posts")
| Use UUID | Use Integer |
|---|---|
| Distributed systems | Single database |
| Hide record count | Need ordering by ID |
| Client-generated IDs | Simpler debugging |
| API exposure | Smaller storage |
# UUID (recommended default)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
# Integer auto-increment
id: int | None = Field(default=None, primary_key=True)
SQLModel converts UserAccount → useraccount. Override if needed:
class UserAccount(SQLModel, table=True):
__tablename__ = "user_account"
PostgreSQL does NOT auto-index FKs:
# WRONG - no index on FK
author_id: uuid.UUID = Field(foreign_key="user.id")
# CORRECT - indexed FK
author_id: uuid.UUID = Field(foreign_key="user.id", index=True)
class Team(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
members: list["User"] = Relationship(back_populates="team")
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
team_id: int | None = Field(default=None, foreign_key="team.id", index=True)
team: Team | None = Relationship(back_populates="members")
class UserRoleLink(SQLModel, table=True):
user_id: uuid.UUID = Field(foreign_key="user.id", primary_key=True)
role_id: uuid.UUID = Field(foreign_key="role.id", primary_key=True)
class User(SQLModel, table=True):
roles: list["Role"] = Relationship(back_populates="users", link_model=UserRoleLink)
class Role(SQLModel, table=True):
users: list[User] = Relationship(back_populates="roles", link_model=UserRoleLink)
class Category(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
parent_id: uuid.UUID | None = Field(default=None, foreign_key="category.id", index=True)
parent: "Category | None" = Relationship(
back_populates="children",
sa_relationship_kwargs={"remote_side": "Category.id"}
)
children: list["Category"] = Relationship(back_populates="parent")
from sqlalchemy import Column
from sqlalchemy.dialects.postgresql import JSONB
class User(SQLModel, table=True):
metadata: dict = Field(default={}, sa_column=Column(JSONB))
from sqlalchemy import Index, text
class User(SQLModel, table=True):
__table_args__ = (
Index('idx_user_tenant_email', 'tenant_id', 'email', unique=True),
Index('idx_user_active', 'id', postgresql_where=text('deleted_at IS NULL')),
)
class Order(SQLModel, table=True):
__table_args__ = (
CheckConstraint("status IN ('pending', 'shipped', 'delivered')"),
)
| Need | Reference |
|---|---|
| ERD creation | assets/erd-template.md |
| Migration SQL | assets/migration-template.sql |
| Pre-deploy checklist | assets/schema-checklist.md |
| Naming rules | references/naming-conventions.md |
| Normalization (1NF-3NF) | references/normalization.md |
| Index strategies | references/index-guidelines.md |
| Common patterns (RBAC, audit, soft-delete, multi-tenant) | references/common-patterns.md |
Generate SQLModel boilerplate:
python scripts/generate_models.py --example --output models.py
created_at/updated_at on mutable tables