database-design
Skill1 starsChanged 8 months agoApache-2.0View on GitHub
Designs PostgreSQL and SQLite schemas and speeds up queries, covering normalization, indexing (B-tree, GIN, partial, covering), EXPLAIN ANALYZE reading, zero-downtime migrations, connection pooling, ORM setup (Prisma, Drizzle, SQLAlchemy, GORM), transactions and isolation levels. Use when designing or changing a schema, fixing a slow query, writing or reviewing a migration, choosing indexes, setting up pooling, or chasing N+1 queries and deadlocks.
- Reads credentials
- Deletes or force-pushes
We split this skill into the rules it gives an agent
14 rules. 0 are common, 7 are distinctive, and 7 are very specific to this project.
0
Common
Most of the 52 edited versions we read keep these.
7
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
- Distinctive: line 22
1NF: Every column holds atomic values.
- Distinctive: line 23
2NF: Every non-key column depends on the entire primary key (relevant for composite keys).
- Distinctive: line 24
3NF: No transitive dependencies.
- Distinctive: line 52
Read:write ratio exceeds 100:1 and JOINs are the bottleneck
- Distinctive: line 149
Seq Scan on large tables: Add an index. If the table has <1000 rows, Seq Scan is fine.
- Very specific: line 153
Rows: estimated vs actual: If wildly off (10x+), run ANALYZE tablename to update statistics.
- Distinctive: line 173
Never rename a column directly: Add new, backfill, deploy code that reads both, drop old.
- Very specific: line 238
Use gorm:"index" on all foreign keys (GORM doesn't auto-index FKs).
- Very specific: line 239
Use Preload() or Joins() to avoid N+1 queries.
- Very specific: line 240
Never use AutoMigrate in production.
- Very specific: line 241
Use db.Raw() for complex queries GORM can't express efficiently.
- Very specific: line 242
Add gorm:"-" to fields that shouldn't be persisted.
- Distinctive: line 368
If you're joining 6 tables to render a product card, you've gone too far.
- Very specific: line 387
Use the right type. TIMESTAMPTZ not TEXT for dates.
“Common” is measured: kept by at least 6 in 10 of the 52 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
You are a senior database architect who designs schemas that scale and writes queries that stay fast under load.
Use this skill when
- Designing a new database schema or modifying an existing one
- Optimizing slow queries using EXPLAIN ANALYZE
- Creating or reviewing database migrations
- Choosing between normalization and denormalization
- Setting up connection pooling or ORM configuration
- Debugging N+1 queries, deadlocks, or performance issues
- Deciding between PostgreSQL and SQLite for a project
Schema Design Principles
Normalization to 3NF
1NF: Every column holds atomic values. No arrays, no comma-separated lists. 2NF: Every non-key column depends on the entire primary key (relevant for composite keys). 3NF: No transitive dependencies. If A -> B -> C, then C should be in B's table, not A's.
-- Bad: transitive dependency (city -> state)
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
city TEXT,
state TEXT -- determined by city, not by customer
);
-- Good: normalize to 3NF
CREATE TABLE cities (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
state TEXT NOT NULL,
UNIQUE (name, state)
);
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
city_id INT REFERENCES cities(id)
);When to Denormalize
Denormalize when:
- Read:write ratio exceeds 100:1 and JOINs are the bottleneck
- Reporting queries scan millions of rows across 5+ tables
- You need sub-millisecond reads (caching at DB level)
Patterns:
- Materialized views: precomputed JOINs, refreshed on schedule
- Computed columns: store
full_namealongsidefirst_name+last_name - Counter caches: store
comment_counton posts, update via trigger
-- Materialized view for dashboard stats
CREATE MATERIALIZED VIEW order_stats AS
SELECT
date_trunc('day', created_at) AS day,
COUNT(*) AS order_count,
SUM(total) AS revenue
FROM orders
GROUP BY 1;
-- Refresh on schedule (or after batch inserts)
REFRESH MATERIALIZED VIEW CONCURRENTLY order_stats;CONCURRENTLY requires a unique index on the view. Without it, reads are blocked during refresh.
Indexing Strategy
B-tree (default, 90% of cases)
-- Single column: equality and range queries
CREATE INDEX idx_users_email ON users (email);
-- Composite: leftmost prefix rule applies
CREATE INDEX idx_orders_user_date ON orders (user_id, created_at DESC);
-- This index serves: WHERE user_id = X, WHERE user_id = X AND created_at > Y
-- It does NOT serve: WHERE created_at > Y (without user_id)
-- Covering index: includes columns to avoid table lookup
CREATE INDEX idx_orders_covering ON orders (user_id, created_at DESC)
INCLUDE (total, status);
-- Now: SELECT total, status FROM orders WHERE user_id = 1 ORDER BY created_at DESC
-- is an index-only scan. No heap fetch.Partial Index (filter out noise)
-- Only index active users (skip 80% of rows)
CREATE INDEX idx_users_active_email ON users (email) WHERE is_active = true;
-- Only index unprocessed jobs
CREATE INDEX idx_jobs_pending ON jobs (created_at) WHERE status = 'pending';
-- Example: Query using the partial index
SELECT * FROM users WHERE email = 'user@example.com' AND is_active = true;
-- Uses idx_users_active_email (fast, small index)
SELECT * FROM users WHERE email = 'user@example.com' AND is_active = false;
-- Cannot use partial index, falls back to seq scan or full email indexPartial indexes are smaller, faster, and should be your first optimization before adding hardware.
GIN Index (PostgreSQL full-text, JSONB, arrays)
-- JSONB containment queries
CREATE INDEX idx_products_tags ON products USING GIN (tags);
-- SELECT * FROM products WHERE tags @> '["electronics"]';
-- Full-text search
CREATE INDEX idx_articles_search ON articles USING GIN (to_tsvector('english', title || ' ' || body));
-- SELECT * FROM articles WHERE to_tsvector('english', title || ' ' || body) @@ to_tsquery('database & design');SQLite Indexing
SQLite uses B-tree exclusively. Same composite index rules apply. Key difference: SQLite has no concurrent index creation -- CREATE INDEX locks the entire database.
-- SQLite: use WITHOUT ROWID for narrow lookup tables
CREATE TABLE sessions (
token TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
expires_at INTEGER NOT NULL
) WITHOUT ROWID;EXPLAIN ANALYZE: Reading Query Plans
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > '2025-01-01'
GROUP BY u.id;What to look for:
- Seq Scan on large tables: Add an index. If the table has <1000 rows, Seq Scan is fine.
- Nested Loop with inner Seq Scan: The inner table needs an index on the join column.
- Hash Join: Normal for large table joins. Worry if
Batches > 1(means spilling to disk -- increasework_mem). - Sort with
external merge: Sort is spilling to disk. Add an index that matches the ORDER BY, or increasework_mem. - Rows: estimated vs actual: If wildly off (10x+), run
ANALYZE tablenameto update statistics. - Buffers: shared hit vs read:
readmeans disk I/O. Ifread >> hit, your working set exceedsshared_buffers.
Migrations
Up/Down Pattern
-- 001_create_users.up.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 001_create_users.down.sql
DROP TABLE users;Safe Migration Rules (zero-downtime)
- Never rename a column directly. Add new, backfill, deploy code that reads both, drop old.
- Never add NOT NULL without a default. Existing rows will fail. Add column nullable, backfill, then set NOT NULL.
- Never drop a column that's still read by running code. Deploy code that stops reading it first.
- Create indexes concurrently in PostgreSQL:
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
-- This does NOT lock the table. Regular CREATE INDEX does.- Wrap DDL in transactions (PostgreSQL supports transactional DDL, MySQL does NOT). The exception is
CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block: put it in its own migration file with transactions turned off.
Tools: golang-migrate, Prisma Migrate, Drizzle Kit, Alembic (Python).
GORM (Go ORM)
// Define models with tags
type User struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex;not null"`
Name string `gorm:"size:100;not null"`
Orders []Order `gorm:"foreignKey:UserID"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
type Order struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"index;not null"`
TotalCents int64 `gorm:"not null"` // money as integer cents, never float64
Status string `gorm:"type:varchar(20);default:'pending'"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
// Auto-migrate (development only, use migrations in production)
db.AutoMigrate(&User{}, &Order{})
// Queries with preloading (avoid N+1)
var users []User
db.Preload("Orders").Where("email LIKE ?", "%@example.com").Find(&users)
// Raw SQL for complex queries
var results []struct {
Name string
OrderCount int64
}
db.Raw(`
SELECT users.name, COUNT(orders.id) as order_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id
`).Scan(&results)
// Transactions
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err
}
if err := tx.Create(&order).Error; err != nil {
return err
}
return nil
})GORM best practices:
- Use
gorm:"index"on all foreign keys (GORM doesn't auto-index FKs). - Use
Preload()orJoins()to avoid N+1 queries. - Never use
AutoMigratein production. Generate SQL and review before running. - Use
db.Raw()for complex queries GORM can't express efficiently. - Add
gorm:"-"to fields that shouldn't be persisted.
Connection Pooling
Why
Each PostgreSQL connection costs ~10MB RAM. 100 connections = 1GB. Serverless functions can spawn hundreds.
PgBouncer (external pooler)
[pgbouncer]
pool_mode = transaction ; Release connection after each transaction
max_client_conn = 1000 ; Accept up to 1000 app connections
default_pool_size = 20 ; Only 20 actual PostgreSQL connectionstransaction mode: connection released after COMMIT. Best for web apps. Breaks: prepared statements, LISTEN/NOTIFY, session variables.
session mode: connection held for entire client session. Use only if you need prepared statements.
Prisma with Connection Pooling
// Prisma 6 and older: connection_limit controls the pool size.
// Prisma 7 uses driver adapters, so set the pool size on the adapter instead.
// (Append with "&" when DATABASE_URL already has a query string.)
const prisma = new PrismaClient({
datasources: {
db: { url: process.env.DATABASE_URL + "?connection_limit=5" },
},
});For serverless (Vercel, AWS Lambda): use Prisma Accelerate or PgBouncer. Direct connections from Lambda will exhaust the pool.
ORM Patterns
Prisma
// Avoid N+1: use include, not sequential queries
const users = await prisma.user.findMany({
where: { isActive: true },
include: {
orders: {
where: { status: "completed" },
orderBy: { createdAt: "desc" },
take: 5,
},
},
});Drizzle (SQL-like, type-safe)
import { eq, desc, sql } from "drizzle-orm";
const result = await db
.select({
name: users.name,
orderCount: sql<number>`count(${orders.id})`.as("order_count"),
})
.from(users)
.leftJoin(orders, eq(orders.userId, users.id))
.where(eq(users.isActive, true))
.groupBy(users.id)
.orderBy(desc(sql`count(${orders.id})`));SQLAlchemy (Python)
from sqlalchemy import select, func
from sqlalchemy.orm import joinedload
# Eager load to avoid N+1
stmt = (
select(User)
.options(joinedload(User.orders))
.where(User.is_active)
)
users = session.scalars(stmt).unique().all()Transactions & Isolation Levels
-- PostgreSQL default: READ COMMITTED
-- Each statement sees committed data at statement start.
-- REPEATABLE READ: snapshot at transaction start. Use for reports.
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT sum(balance) FROM accounts; -- consistent snapshot
SELECT count(*) FROM accounts WHERE balance > 1000; -- same snapshot
COMMIT;
-- SERIALIZABLE: full serializability. Use for financial operations.
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- If concurrent transaction conflicts, one gets: ERROR: could not serialize access
-- Your code MUST retry on serialization failure.SQLite: uses DEFERRED (default), IMMEDIATE, or EXCLUSIVE locking.
BEGIN IMMEDIATE: grabs write lock upfront. Prevents "database is locked" errors in concurrent writes.
Anti-Patterns
N+1 Queries
# BAD: 1 query for users + N queries for orders
users = session.query(User).all()
for user in users:
print(user.orders) # Each triggers a SELECT
# GOOD: 1 query with JOIN
users = session.query(User).options(joinedload(User.orders)).all()Missing Indexes on Foreign Keys
PostgreSQL does NOT auto-index foreign keys. Every FK column needs an explicit index, or JOINs and cascading deletes will Seq Scan.
-- After adding a FK, always add the index
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);
CREATE INDEX idx_orders_user_id ON orders (user_id); -- DON'T FORGET THISOver-Normalization
If you're joining 6 tables to render a product card, you've gone too far. Denormalize the read path, keep writes normalized. CQRS pattern: separate read models (denormalized) from write models (normalized).
Unbounded Queries
-- BAD: no LIMIT, could return millions of rows
SELECT * FROM logs WHERE level = 'error';
-- GOOD: always paginate
SELECT * FROM logs WHERE level = 'error'
ORDER BY created_at DESC
LIMIT 50 OFFSET 0;
-- BETTER: cursor-based pagination (stable, no offset scan)
SELECT * FROM logs WHERE level = 'error' AND created_at < $last_seen
ORDER BY created_at DESC
LIMIT 50;Using TEXT for Everything
Use the right type. TIMESTAMPTZ not TEXT for dates. INTEGER not TEXT for IDs. INET not TEXT for IPs. Correct types enable index efficiency, validation, and query planner optimization.
PostgreSQL vs SQLite Decision Guide
Choose SQLite when: single-user app, embedded/mobile, CLI tools, <100 concurrent readers, <10GB database, prototyping. Choose PostgreSQL when: multi-user web app, concurrent writes, need JSONB/full-text/GIS, >10GB, need replication, need row-level security.
SQLite scales reads remarkably well with WAL mode:
PRAGMA journal_mode = WAL; -- Write-ahead logging: concurrent reads during writes
PRAGMA busy_timeout = 5000; -- Wait 5s instead of instant SQLITE_BUSY
PRAGMA synchronous = NORMAL; -- Good enough durability for most apps
PRAGMA cache_size = -64000; -- 64MB cache (negative = KB)Other versions54 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 54
- 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★ 224same name
- kid-sid/claude-spellbook★ 190same name
- xenitV1/Antigravity-Workflows★ 130same name
44 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.
Used alongsideProjects 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
In this repository21 other files it gives its agents.
- accessibility-wcagSkill
- api-designSkill
- bugbounty-reportSkill
- ci-cd-pipelinesSkill
- codebase-analysisSkill
- design-docSkill
- docker-kubernetesSkill
- git-workflowSkill
- go-devSkill
- i18n-localizationSkill
- llm-integrationSkill
- nextjs-16Skill
- oauth-authSkill
- performance-optimizationSkill
- python-best-practicesSkill
- scraping-automationSkill
- security-reviewSkill
- seo-metaSkill
- tailwind-cssSkill
- testingSkill
- ts-devSkill
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.

