Skip to main content

database-modeling

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

معلومات المصدر

المستودع
sawrus/gemma
آخر نشاط في المصدر
٢٨ أبريل ٢٠٢٦ في ١٠:٤٦
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٠
التفرعات
٠

خيارات التثبيت

يُحدَّد 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