database-design
Skill12 starsChanged 23 days agoMITView on GitHub
Design relational database schemas (and choose when to go NoSQL) that stay maintainable and fast. Use this skill whenever the user mentions tables, DDL, entities and relationships, normalization (1NF/2NF/3NF), primary and foreign keys, UUID vs bigint IDs, indexes (B-tree, composite, covering, partial), EXPLAIN, constraints (CHECK, UNIQUE, exclusion), transactions and isolation levels, migrations (Alembic, Prisma, Flyway, expand-contract, backfilling), or SQL vs NoSQL (MongoDB, DynamoDB, Cassandra, graph databases). Also trigger for "design the database", "model this domain", "which database should I use", or writing ORM models and migration files for PostgreSQL, MySQL, SQLite, or SQL Server.
58 other projects use a version of this.
Adopted by
Most recently taken up by
- chaunsin/agent-skillstheir own version, first seen Nov 8, 2026
- kinhluan/skillstheir own version, first seen Nov 8, 2026
- ishandutta2007/Awesome-Claude-Skillstheir own version, first seen Nov 8, 2026
- mahdy-gribkov/arcanatheir own version, first seen Nov 8, 2026
- KbWen/agentic-ostheir own version, first seen Nov 7, 2026
- its-ash/skillstheir own version, first seen Nov 1, 2026
Used with
Projects with it also use intelligent-routing, bash-linux and 10 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
SKB3002Skillintelligent-routing
Automatic agent selection and intelligent task routing. Analyzes user requests and automatically selects the best specialist agent(s) without requiring explicit user mentions.
Also in 6 projects that use this
xenitV1Skillbash-linux
Bash/Linux terminal patterns. Critical commands, piping, error handling, scripting. Use when working on macOS or Linux systems.
Also in 7 projects that use this
xenitV1Skillred-team-tactics
Red team tactics principles based on MITRE ATT&CK. Attack phases, detection evasion, reporting.
Also in 7 projects that use this
pepebot-spaceSkillllm-application-dev
Building applications with Large Language Models - prompt engineering, RAG patterns, and LLM integration. Use for AI-powered features, chatbots, or LLM-based automation.
Also in 4 projects that use this
- +8 more from xenitV1
Other versions58 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 58
- vudovn/ag-kit★ 8.2ksame name
- vudovn/antigravity-kit★ 8.2ksame name
- CloudAI-X/claude-workflow★ 1.4ksame name
- CloudAI-X/claude-workflow-v2★ 1.4ksame name
- MoizIbnYousaf/Ai-Agent-Skills★ 1.1ksame name
- skillcreatorai/Ai-Agent-Skills★ 1.1ksame name
- MoizIbnYousaf/ai-agent-skills★ 1.1ksame name
- KbWen/agentic-os★ 229same name
- kid-sid/claude-spellbook★ 190same name
- xenitV1/Antigravity-Workflows★ 130same name
48 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository25 other files it gives its agents.
- AGENTS.mdAGENTS.md
- skills/genai-tk/references/agents.mdAGENTS.md
- skills/mastra/references/agents.mdAGENTS.md
- adaption-aiSkill
- api-designSkill
- clairicSkill
- dddSkill
- genai-tkSkill
- langchainSkill
- linkup-extractSkill
- linkup-fetchSkill
- linkup-researchSkill
- linkup-searchSkill
- linkup-workflowSkill
- mastraSkill
- microservices-patternsSkill
- performance-optimizationSkill
- prime-rlSkill
- scalingoSkill
- security-best-practicesSkill
- smolagentsSkill
- testing-patternsSkill
- typesafe-aiSkill
- uiSkill
- unsloth-hf-jobsSkill
The file
We split this skill into the rules it gives an agent
9 rules. 0 are common, 2 are distinctive, and 7 are very specific to this project.
0
Common
Most of the 55 edited versions we read keep these.
2
Distinctive
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 34
Choose keys: Prefer surrogate BIGINT IDENTITY primary keys unless a natural key is genuinely stable.
- Distinctive: line 35
Add constraints: NOT NULL, UNIQUE, CHECK, defaults, and foreign keys belong in the database, not just in the ORM.
- Very specific: line 36
Design indexes: Start with the indexes backing PK/FK/UNIQUE constraints, then add query-driven indexes for the hot paths found in step 1.
- Very specific: line 37
Validate with EXPLAIN: Run EXPLAIN (ANALYZE, BUFFERS) against realistic data.
- Very specific: line 100
Composite index column order matters: Put equality columns first, then range/ORDER BY columns: (customer_id, created_at) serves WHERE …
- Very specific: line 103
When an index hurts: every INSERT/UPDATE/DELETE maintains every index on the table (write amplification).
- Very specific: line 134
Optimistic: read a version/rowversion column and UPDATE only if unchanged (WHERE version = ?), retrying on conflict.
- Distinctive: line 177
Start from the workload, not the hype:
- Very specific: line 258
Constraints: CHECK quantity > 0 and unit_price_cents >= 0.
“Common” is measured: kept by at least 6 in 10 of the 55 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Database Design
Database design turns a domain into a schema that stays fast, correct, and easy to evolve for years. This skill covers the full pipeline — requirements analysis, entity modeling, normalization, keys, constraints, indexes, and safe migrations — plus deciding when a document, wide-column, or graph store beats a relational database.
Quick Reference
| Topic | Reference |
| Normalization worked examples, natural vs surrogate keys, UUID vs bigint | normalization-and-keys.md |
| B-tree internals, composite/covering/partial indexes, reading EXPLAIN | indexing-and-query-tuning.md |
| Migration tools (Alembic, Prisma, Flyway), expand-contract, backfilling | migrations-zero-downtime.md |
Core Workflow
Work through these steps in order — each informs the next, and skipping requirements analysis is the most common source of bad schemas.
- Gather requirements. List entities, attributes, and relationships. Note cardinalities and whether each read is CRUD (single-row) or reporting (aggregate). Ask about scale, growth, retention, and compliance (e.g. GDPR erasure).
- Model entities and relationships. Draw an ER sketch. Normalize to 3NF by default; denormalize only later, with a measured reason.
- Choose keys. Prefer surrogate
BIGINT IDENTITYprimary keys unless a natural key is genuinely stable. Decide UUID vs bigint now — changing it later is painful. - Add constraints. NOT NULL, UNIQUE, CHECK, defaults, and foreign keys belong in the database, not just in the ORM.
- Design indexes. Start with the indexes backing PK/FK/UNIQUE constraints, then add query-driven indexes for the hot paths found in step 1.
- Validate with EXPLAIN. Run
EXPLAIN (ANALYZE, BUFFERS)against realistic data. Fix the plan, not the schema, before considering denormalization. - Plan migrations. Versioned migrations in code; expand-contract for anything touching a live table. Never hand-edit production.
Requirements Analysis
Collect these before writing a single CREATE TABLE:
| Question | Why it matters |
| What are the entities and their attributes? | Defines tables and columns |
| What are the relationships and cardinalities? | 1:1, 1:N, M:N (junction table) |
| Required or optional attributes? | NOT NULL vs NULLable |
| Is a read CRUD or reporting? | CRUD wants normalized lookups; reporting wants aggregates, maybe denormalized |
| Write volume and read/write ratio? | Drives index count and normalization level |
| Data growth and retention? | Partitioning, archival, time-series decisions |
| Consistency requirements? | Isolation levels; whether eventual consistency is acceptable |
| Compliance (GDPR, HIPAA, finance)? | Audit columns, soft vs hard deletes, retention rules |
CRUD vs reporting reads want opposite schema shapes: CRUD queries touch few rows, narrow columns, and normalized tables; reporting wants few tables, wide pre-joined rows, and precomputed aggregates. If both matter, keep the OLTP schema normalized and serve reporting from a separate path (materialized views, a read replica, or a warehouse). Cardinality:
| Relationship | Model |
| 1:1 | FK with UNIQUE, or merge into one table when always accessed together |
| 1:N | Foreign key on the "many" side |
| M:N | Junction table with two FKs, composite PK over both |
Normalization (1NF–3NF)
Normalization removes redundancy so each fact is stored and updated in exactly one place.
| Form | Rule | Typical violation | Fix |
| 1NF | Atomic values; no repeating groups | phones column holding "555-1,555-2" | Child table, one row per value |
| 2NF | No partial dependency on part of a composite key | order_item storing product_name (depends only on product_id) | Move it to product |
| 3NF | No transitive dependency on a non-key column | order storing customer_city (depends on customer_id, not order_id) | Move it to customer |
Normalize to 3NF by default. Denormalize deliberately — copy or aggregate data to serve a specific hot read — only when a measured query needs it. The costs are real: write amplification (every write maintains the copies), risk of drift, and backfill work whenever the derivation logic changes. Full worked example from spreadsheet to 3NF: normalization-and-keys.md.
Keys
Natural vs surrogate:
| Natural key | Surrogate key | |
| Examples | isbn, email, slug, country_code | id BIGINT GENERATED ALWAYS AS IDENTITY |
| Pros | Meaningful, real-world stable, no extra column for lookups | Never changes, compact, no domain coupling, DB-generated |
| Cons | Can change or be re-issued; may be long or non-uniform | Meaningless (still need UNIQUE business keys); opaque joins |
| Role | Add as a UNIQUE business key | Primary key for most tables |
Foreign keys and referential actions:
| Action | Behavior | Use when |
RESTRICT / NO ACTION | Refuse to delete a referenced row | Default; safest |
CASCADE | Delete children with the parent | Owner-child lifecycles (order → items); audit the blast radius first |
SET NULL | Null out the child FK | Optional references (deleted user keeps orders) |
UUID vs bigint (full comparison in normalization-and-keys.md): bigint is 8 bytes, sequential, and cache-friendly — prefer it for high-write OLTP. UUIDv7 (time-ordered) is the choice for distributed or client-generated IDs where a central sequence is impossible. Avoid random UUIDv4 as a PK on hot tables: random inserts thrash the B-tree and bloat indexes.
Indexing
A B-tree index is an ordered copy of one or more columns that lets the engine find rows without scanning the whole table — O(log n) equality lookups and efficient range scans. Design indexes for your queries, not for every column.
- Composite index column order matters. Put equality columns first, then range/ORDER BY columns:
(customer_id, created_at)servesWHERE customer_id = ? AND created_at > ?. A leading range column makes the later columns useless for lookups. - Covering indexes include every column the query touches (
INCLUDE (status)) so the engine can answer from the index alone (index-only scan). - Partial indexes carry a
WHEREclause — e.g.WHERE status = 'pending'— keeping a hot subset small: faster scans, fewer writes to maintain. - When an index hurts: every INSERT/UPDATE/DELETE maintains every index on the table (write amplification); indexes consume disk and buffer cache; unused indexes still cost writes. Remove indexes no query uses.
- EXPLAIN is the source of truth.
EXPLAIN (ANALYZE, BUFFERS)on a slow query shows Seq Scan vs Index Scan vs joins and where time actually goes. Reading guide: indexing-and-query-tuning.md.
Constraints
Constraints make the database enforce your invariants, so app bugs and concurrent writers cannot corrupt data:
| Constraint | Example | Purpose |
NOT NULL | email TEXT NOT NULL | Column is always present |
UNIQUE | UNIQUE (tenant_id, slug) | No duplicates within scope |
CHECK | CHECK (quantity > 0) | Value sanity and business rules |
DEFAULT | created_at TIMESTAMPTZ NOT NULL DEFAULT now() | Fills values on insert |
| Foreign key | FOREIGN KEY (customer_id) REFERENCES customers(id) | Referential integrity |
EXCLUDE (PostgreSQL) | EXCLUDE USING gist (room WITH =, during WITH &&) | No overlapping reservations |
Why they belong in the DB rather than the ORM: enforced under concurrency, enforced for every code path (imports, ad-hoc SQL, future apps), and they document the schema's contract. ORM-level validation is a convenience; DB constraints are the guarantee.
Transactions and Isolation
ACID transactions keep multi-step writes atomic. The isolation level is a consistency-vs-throughput tradeoff:
| Level | Prevents | Can still see |
| Read committed (default in PostgreSQL/MySQL) | Dirty reads | Non-repeatable reads, phantoms |
| Repeatable read | Dirty and non-repeatable reads | Phantoms (PostgreSQL also prevents these here) |
| Serializable | Dirty, non-repeatable, phantoms, write skew | — (at a concurrency cost) |
Locking strategies:
- Pessimistic:
SELECT ... FOR UPDATElocks rows so no one else modifies them until commit. Simple to reason about; hurts under contention. - Optimistic: read a
version/rowversion column and UPDATE only if unchanged (WHERE version = ?), retrying on conflict. Scales better; needs retry logic.
Rule of thumb: start at read committed; raise the level only for a concrete anomaly (e.g. financial invariants); prefer optimistic locking over long-held locks.
Migrations
Schema changes are code: versioned, reviewed, tested, and applied exactly once in order. Tools: Alembic (Python/SQLAlchemy), Prisma Migrate (TypeScript), Flyway (SQL-first, JVM/CLI).
# Alembic
alembic revision --autogenerate -m "add orders table"
alembic upgrade head
# Prisma
prisma migrate dev --name add_orders
# Flyway (versioned SQL files, e.g. V2__add_orders.sql)
flyway migrateExpand–contract (a.k.a. expand–migrate–contract) is the zero-downtime pattern for anything touching a live table:
- Expand — add the new column/table as additive (nullable or new); deploy code that writes both old and new.
- Backfill — populate the new structure in bounded batches without blocking writes.
- Contract — switch reads, then writes, then drop the old structure once nothing references it.
-- 1. Expand: add nullable column, dual-write in the app
ALTER TABLE orders ADD COLUMN total_cents BIGINT;
-- 2. Backfill in batches
UPDATE orders SET total_cents = /* recompute from items */
WHERE id BETWEEN :start AND :end AND total_cents IS NULL;
-- 3. Contract: flip reads, then drop the old column
ALTER TABLE orders ALTER COLUMN total_cents SET NOT NULL;
-- later: ALTER TABLE orders DROP COLUMN legacy_total;Backfilling runs in bounded batches (WHERE id > :last_id ORDER BY id LIMIT 1000) so it neither locks the table for minutes nor floods the WAL. Never backfill a large table with a single unbounded UPDATE. Full guide with locking gotchas: migrations-zero-downtime.md.
SQL vs NoSQL
Start from the workload, not the hype:
| Store | Strengths | Pick when | Avoid when |
| Relational (PostgreSQL, MySQL) | Joins, transactions, constraints, ad-hoc queries | Data has relationships and invariants; reporting; money | Horizontal write scale beyond a single DB is the top need |
| Document (MongoDB) | Flexible schemas, natural aggregates, fast single-doc reads | Document-shaped data, read-mostly, evolving attributes | Cross-document transactions/joins are core; strong invariants |
| Wide-column (Cassandra, DynamoDB) | Linear read/write scale, partition-friendly | Known access patterns, huge scale, time-series at scale | Ad-hoc queries, joins, evolving query patterns |
| Graph (Neo4j) | Traversal of relationships | Deep relationship queries (fraud, social, routing) | Simple CRUD, bulk reporting |
A pragmatic middle path: keep the relational DB as the system of record and add specialized stores (search, cache, analytics warehouse) around it — don't fork the domain model across stores unnecessarily.
Practical Patterns
| Pattern | How | Gotchas |
| JSON columns | data JSONB for rarely-queried flexible attributes; GIN index only if you filter on them | Don't put query-critical or joined data in JSON |
| Full-text search | PostgreSQL tsvector + GIN, MySQL FULLTEXT | Move to OpenSearch/Elasticsearch past ~1M docs or for ranking complexity |
| Time-series | TimescaleDB hypertables or range partitioning by time | Keep recent partitions small; retention via DROP PARTITION |
| Soft deletes | deleted_at TIMESTAMPTZ NULL | Breaks UNIQUE constraints and FKs — filter everywhere, or hard delete + audit table |
| Audit columns | created_at, updated_at, created_by; triggers or ORM callbacks | updated_at needs a trigger for raw-SQL writes; full history needs event sourcing |
| N+1 prevention | JOINs, IN (...) batching, ORM eager loading | N+1 = 1 query for N parents + N queries for children |
-- N+1 anti-pattern: 1 query for orders + N queries for order_items. Fix: one query
SELECT o.*, oi.*
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.customer_id = $1;Worked Example: Small E-Commerce
Domain: customers, products (each in many categories), orders (many items, each item is one product). Requirements: order CRUD, product browsing by category, "recent orders for a customer" reporting, and a search box.
erDiagram
CUSTOMER ||--o{ "ORDER" : places
"ORDER" ||--|{ ORDER_ITEM : contains
PRODUCT ||--o{ ORDER_ITEM : "is ordered as"
PRODUCT ||--o{ PRODUCT_CATEGORY : has
CATEGORY ||--o{ PRODUCT_CATEGORY : has
CUSTOMER {
bigint id PK
text email UK
text name
timestamptz created_at
}
"ORDER" {
bigint id PK
bigint customer_id FK
text status
bigint total_cents
timestamptz created_at
}
ORDER_ITEM {
bigint order_id PK,FK
bigint product_id PK,FK
int quantity
bigint unit_price_cents
}
PRODUCT_CATEGORY {
bigint product_id PK,FK
bigint category_id PK,FK
}
PRODUCT {
bigint id PK
text sku UK
text name
text description
tsvector search_vector
}
CATEGORY {
bigint id PK
text slug UK
}Key decisions:
- Keys: surrogate
bigintPKs everywhere;sku,email,slugas UNIQUE natural keys for lookups. - Normalization: 3NF —
productandcategoryare separate with a junction table.orders.total_centsis a deliberate denormalized aggregate, recomputed in the same transaction as its items. - Constraints: CHECK
quantity > 0andunit_price_cents >= 0; CHECK onstatus; FKON DELETE RESTRICTfor products/categories,CASCADEfor order items; the junction's composite PK already enforces uniqueness. - Indexes:
-- Hot paths
CREATE INDEX idx_orders_customer_created ON orders (customer_id, created_at DESC); -- "recent orders" (equality first, then sort)
CREATE INDEX idx_order_items_order ON order_items (order_id); -- order detail
CREATE INDEX idx_products_search ON products USING gin (search_vector); -- search box (tsvector kept fresh by a trigger)
CREATE INDEX idx_orders_status_created ON orders (status, created_at)
WHERE status = 'pending'; -- partial: tiny fulfillment queueThe composite (customer_id, created_at) serves both WHERE customer_id = ? and ORDER BY created_at DESC. The partial index stays tiny because only pending orders enter the queue.
Anti-Patterns to Avoid
- No requirements analysis — building tables before knowing queries, cardinalities, and read patterns.
- Premature denormalization — copying data "for performance" before EXPLAIN shows a problem.
- Mutable natural keys as PK (
email,username) — churn breaks every referencing row. - Random UUIDv4 PKs on hot tables — random inserts fragment the B-tree and bloat indexes.
- Index on every column — each index costs writes; unused ones are pure overhead.
- Constraints only in the ORM — race conditions and raw SQL bypass validation.
- Soft deletes without a plan —
deleted_atsilently breaks UNIQUE constraints, and every query needsWHERE deleted_at IS NULL. SELECT *in production queries — defeats covering indexes and widens scans.- Giant or hand-applied migrations — schema changes must be small, versioned, and reversible.
- EAV (entity-attribute-value) tables — "flexible" schemas that make every query painful.
When to Use / Not Use
Use this skill when:
- Designing a new schema from requirements or a domain description
- Reviewing an existing schema for normalization, key, constraint, or index problems
- Deciding between SQL and NoSQL for a workload
- Planning a migration, backfill, or zero-downtime schema change
- Tuning slow queries with EXPLAIN
Do NOT use when:
- The task is only writing queries against an existing, settled schema (no design decisions)
- You need a runbook for a specific DB's operations (backups, failover, tuning knobs) — that is ops, not design
- Deep implementation work inside a NoSQL store (DynamoDB partition keys, Cassandra compaction) — this skill covers choosing the store, not its internal best practices
- The user just needs boilerplate ORM models generated from an already-decided schema
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

