Back to skills
SKILL.md
Database Design
ASecurityUse when Database design mastery. Schema design with normalization, denormalization strategies, indexing, migration pipelines, ORM selection (Prisma/Drizzle/SQLAlchemy/EF Core), connection pooling, soft deletes, audit trails, multi-tenancy, and serverless database patterns. Use when designing schemas, choosing databases, planning migrations, or architecting data layers.
- 5 stars
- 0 votes
- 0 copies
- 0 views
- Added September 27, 2026
Works with
Security analysis
100/100Pro scans all 2 files and shows the line behind each finding
npx -y skills add Harmitx7/tribunal-kit --skill database-design --agent claude-codeAre you the author of Database Design?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/harmitx7-database-design-tribunal-kit)---
name: database-design
description: "Use when Database design mastery. Schema design with normalization, denormalization strategies, indexing, migration pipelines, ORM selection (Prisma/Drizzle/SQLAlchemy/EF Core), connection pooling, soft deletes, audit trails, multi-tenancy, and serverless database patterns. Use when designing schemas, choosing databases, planning migrations, or architecting data layers."
version: 5.0.0
last-updated: 2026-09-13
skills:
- supabase-postgres-best-practices
- sql-pro
- db-latency-auditor
tools: Read, Grep, Glob, Bash, Edit, Write
scripts-binding:
- .agent/scripts/lint_runner.js
- .agent/scripts/verify_all.js
---
# Database Design β Schema & Architecture Mastery
---
## π οΈ Technical Architecture & Reference Recipes
---
## 2026 Database Performance & Schema Invariants
1. **Time-Ordered UUID v7 (RFC 9562)**: When UUIDs are required across distributed systems, use UUID v7 so records append sequentially to B-tree indexes, avoiding fragmentation.
2. **Partial Indexing on Soft Deletes**:
```sql
CREATE INDEX idx_users_active_email ON users (email) WHERE deleted_at IS NULL;
```
3. **Covering Indexes**: Use `INCLUDE (col_a, col_b)` to allow index-only scans without table heap lookups on read-heavy query patterns.
4. **Connection Pooling in Serverless**: Always route serverless connections through Supavisor, PgBouncer, or Neon connection poolers with transaction-mode pooling.
## Hallucination Traps (Read First)
- β `TIMESTAMP` without timezone β β
Always `TIMESTAMPTZ`
- β UUID v4 as primary key β β
UUID v7 (time-ordered) or `BIGINT GENERATED ALWAYS AS IDENTITY`
- β Omitting indexes on foreign keys β β
Postgres does NOT auto-index FKs; always create explicit indexes
- β Adding `NOT NULL` column without default directly on large tables β β
Add nullable first, backfill in batches, then set `NOT NULL`
- β Soft delete without partial index β β
Always index `WHERE deleted_at IS NULL`
- β Direct DB connection inside serverless functions β β
Use pooled connection string (PgBouncer/Supavisor)
---
## Database Selection
```
Relational / Complex queries β PostgreSQL (primary choice)
Serverless PG β Neon, Supabase
Edge / Ultra-low latency β Turso (SQLite @ edge)
Simple / Embedded β SQLite
Global distribution (MySQL) β PlanetScale (no FK support)
Key-value / Cache β Redis / Valkey / Upstash
Document store β MongoDB / Firestore
Full-text search β PostgreSQL tsvector (built-in) or Meilisearch / Typesense
Time-series β TimescaleDB / ClickHouse
Vector (AI embeddings) β pgvector (PostgreSQL ext) / Pinecone / Weaviate
```
---
## Standard Table Template
```sql
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
-- OR: id UUID DEFAULT gen_random_uuid() PRIMARY KEY (use v7 for perf)
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('admin', 'user', 'moderator')),
is_active BOOLEAN NOT NULL DEFAULT true,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ -- soft delete
);
-- Required: auto-update updated_at
CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- Required indexes
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_users_active ON users (email) WHERE deleted_at IS NULL; -- partial index for soft delete
CREATE INDEX idx_users_created_at ON users (created_at DESC);
```
---
## Schema Patterns
### Relationships
```sql
-- One-to-Many: FK on the "many" side + INDEX
CREATE TABLE posts (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
author_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
...
);
CREATE INDEX idx_posts_author_id ON posts (author_id); -- REQUIRED in Postgres
-- Many-to-Many: junction table with composite PK
CREATE TABLE post_tags (
post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
tag_id BIGINT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
CREATE INDEX idx_post_tags_tag_id ON post_tags (tag_id); -- index the non-PK side
```
### Multi-Tenancy
```sql
-- Pattern 1: tenant_id column (simplest β enforce via RLS)
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
-- Pattern 2: Schema per tenant (better isolation, harder migrations)
-- CREATE SCHEMA tenant_acme;
-- Pattern 3: DB per tenant β only for compliance/regulatory needs
```
---
## ORM Selection
| ORM | Best For | Trade-offs |
| ------------------ | --------------------------------------- | -------------------------- |
| **Drizzle** | Edge, TypeScript, bundle-size sensitive | Newer, fewer examples |
| **Prisma** | DX, schema management, Prisma Studio | Heavy, NOT edge-compatible |
| **Kysely** | Type-safe SQL builder, full control | Manual migrations |
| **Raw SQL** | Complex queries, performance-critical | Manual type safety |
| **SQLAlchemy 2.0** | Python async ecosystem | Python only |
```typescript
// Drizzle β SQL-like, edge-compatible
const result = await db
.select({ id: users.id, name: users.name })
.from(users)
.where(and(eq(users.role, 'admin'), eq(users.isActive, true)))
.orderBy(desc(users.createdAt))
.limit(20);
// Prisma β β TRAP: can't express complex joins natively β use prisma.$queryRaw<Type>
const user = await prisma.user.findUnique({ where: { email }, include: { posts: { take: 10 } } });
```
---
## Migrations (Zero-Downtime Strategy)
```sql
-- Safe column add on a large production table:
-- Step 1: Add nullable (no lock)
ALTER TABLE users ADD COLUMN phone TEXT;
-- Step 2: Backfill in batches (non-blocking)
UPDATE users SET phone = '' WHERE phone IS NULL AND id BETWEEN 1 AND 10000;
-- Step 3: Add constraint AFTER all code deploys write the column
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
```
**Migration Rules:**
- Never modify a migration already applied to production β create a new one
- Remove column in 2 deploys: first remove all code references, then `DROP COLUMN`
- `CREATE INDEX CONCURRENTLY` to avoid table locks on existing data
- Test migrations against a copy of production data before running live
---
## Indexing Reference
| Index Type | Use For |
| ------------------ | ---------------------------------------------------- |
| **B-tree** | General purpose β equality & range queries (default) |
| **Hash** | Equality-only lookups (faster than B-tree for =) |
| **GIN** | JSONB, arrays, full-text (`tsvector`) |
| **GiST** | Geometric, range types |
| **HNSW / IVFFlat** | Vector similarity (pgvector) |
**Composite index column order:** equality columns first β range columns last β most selective first
---
## Audit Trail
```sql
CREATE TABLE audit_log (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
table_name TEXT NOT NULL, record_id BIGINT NOT NULL,
action TEXT NOT NULL CHECK (action IN ('INSERT', 'UPDATE', 'DELETE')),
old_data JSONB, new_data JSONB,
changed_by BIGINT REFERENCES users(id),
changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_audit_log_table_record ON audit_log (table_name, record_id);
CREATE INDEX idx_audit_log_changed_at ON audit_log USING brin (changed_at); -- BRIN for time-ordered append-only tables
```
---
## Connection Pooling
```
Without pooling: 100 concurrent requests β 100 DB connections β overwhelms DB
With pooling: 100 concurrent requests β 10β20 reused connections
Sizing formula: max_connections = (cpu_cores Γ 2) + disk_spindles (typically 25β50)
Poolers:
PgBouncer β External, most common for self-hosted Postgres
Prisma Accelerate β Managed, for Prisma projects
Supabase Supavisor β Managed, for Supabase projects
```
Files in this skill
- SKILL.md
- scripts/schema_validator.py
Attribution
Comments
Loading commentsβ¦