Skip to main content

database-modeling

Design relational schemas, write efficient queries, plan indexes, and implement safe migrations.

소스 정보

저장소
sawrus/gemma
최근 소스 활동
2026년 4월 28일 10:46
감지된 SKILL.md 언어
영어
스타
0
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
database-modeling
type
skill
description
Design relational schemas, write efficient queries, plan indexes, and implement safe migrations.
related-rules
["data_access.md","architecture.md"]
allowed-tools
Read, Write, Edit, Bash
agentic
{"generated_by":"agentic","source":"areas/software/backend/skills/database-modeling/SKILL.md","repository":"https://github.com/sawrus/agent-guides"}
# Database Modeling Skill > **Expertise:** PostgreSQL schema design, SQLAlchemy (async), query optimization, indexing, migrations (Alembic), safe schema changes. ## Schema Design Patterns ### Standard column set (all tables) ```python from sqlalchemy import Column, Integer, DateTime, func from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): pass class TimestampMixin: created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False ) class Order(TimestampMixin, Base): __tablename__ = "orders" id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True) status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending") total_amount: Mapped[Decimal] = mapped_column(Numeric(12, 2), nullable=False) ``` ### Soft delete pattern ```python class SoftDeleteMixin: deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) @property def is_deleted(self) -> bool: return self.deleted_at is not None # Always filter in repository, never expose deleted records by default class OrderRepository: async def list_active(self, session: AsyncSession): return await session.execute( select(Order).where(Order.deleted_at.is_(None)) ) ``` ## Indexing Strategy ```sql -- Single column: high-cardinality columns used in WHERE/JOIN/ORDER BY CREATE INDEX idx_orders_user_id ON orders(user_id); CREATE INDEX idx_orders_status ON orders(status) WHERE deleted_at IS NULL; -- partial index -- Composite: query uses both columns together (order matters: equality first, then range) CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC); -- Full-text search CREATE INDEX idx_products_search ON products USING gin(to_tsvector('english', name || ' ' || description)); -- Never index: low-cardinality boolean columns, small tables (<1000 rows) ``` ### EXPLAIN ANALYZE checklist ```sql -- Run before every new query on a table with >10k rows EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC LIMIT 20; -- Watch for: Seq Scan on large table → add index -- Index Scan with high actual rows >> estimated rows → ANALYZE the table -- Nested Loop with large inner side → consider Hash Join ``` ## Repository Pattern ```python from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update class OrderRepository: def __init__(self, session: AsyncSession): self.session = session async def get_by_id(self, order_id: int) -> Optional[Order]: result = await self.session.execute( select(Order).where(Order.id == order_id, Order.deleted_at.is_(None)) ) return result.scalar_one_or_none() async def list_by_user( self, user_id: int, *, limit: int = 20, cursor_id: Optional[int] = None ) -> list[Order]: q = select(Order).where(Order.user_id == user_id, Order.deleted_at.is_(None)) if cursor_id: q = q.where(Order.id < cursor_id) # cursor-based pagination q = q.order_by(Order.id.desc()).limit(limit) result = await self.session.execute(q) return list(result.scalars()) async def update_status(self, order_id: int, status: str) -> None: await self.session.execute( update(Order).where(Order.id == order_id).values(status=status) ) # No commit here — caller (service layer) owns the transaction ``` ## Migration Safety (Alembic) ```bash # Generate migration alembic revision --autogenerate -m "add_index_orders_user_id" # ALWAYS review generated file before applying alembic show head # Apply alembic upgrade head # Rollback one step alembic downgrade -1 ``` ### Safe vs. unsafe schema operations | Operation | Safe to deploy | Strategy | |---|---|---| | Add nullable column | ✅ Non-breaking | Apply directly | | Add column with default | ✅ (PostgreSQL 11+) | Apply directly | | Add NOT NULL column | ⚠️ Breaking | Add nullable → backfill → add constraint | | Add index | ✅ with CONCURRENTLY | `CREATE INDEX CONCURRENTLY` | | Rename column | ❌ Breaking | Expand/contract (add new → migrate code → drop old) | | Drop column | ❌ Breaking | Deprecate in code → drop in next release | | Change type | ❌ Breaking | Add new column with new type → migrate → drop old | ```python # Alembic: create index without locking table def upgrade(): op.execute("CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_user_id ON orders(user_id)") def downgrade(): op.execute("DROP INDEX CONCURRENTLY IF EXISTS idx_orders_user_id") ``` ## N+1 Query Prevention ```python # ❌ N+1: loads orders, then 1 query per order to get user orders = await session.execute(select(Order)).scalars() for order in orders: print(order.user.name) # each access fires a query # ✅ Eager load with joinedload from sqlalchemy.orm import joinedload orders = await session.execute( select(Order) .options(joinedload(Order.user)) # single JOIN .where(Order.status == "pending") ) ```
GitHub에서 보기