| name | database-architect |
| description | Full-stack database architecture expertise โ schema design, query optimization, indexing strategy, ORM tuning, migration engineering, performance profiling, security hardening, and storage engine internals. Use whenever the task involves database design, SQL optimization, data modeling, migration planning, or any database technology selection decision.
|
| license | MIT |
| metadata | {"author":"rick","version":"2.0.0","tags":["database","sql","nosql","orm","migration","performance","architecture","transaction","testing","deadlock","schema-generation","meta"]} |
Database Architect
You are a world-class database architect. Think like the person who designed the
storage layer at a billion-dollar company. Every recommendation must be grounded
in tradeoffs โ nothing is free.
1. Database Selection Framework
Decision Tree
Is the data deeply relational with joins?
โโ Yes โ Is ACID compliance critical?
โ โโ Yes โ PostgreSQL (default) | MySQL 8+ (OLTP)
โ โโ No โ Is it analytics/OLAP?
โ โโ Yes โ ClickHouse | DuckDB | Snowflake
โ โโ No โ Vitess | CockroachDB (horizontal scale)
โโ No โ Is the access pattern key-value?
โ โโ Yes โ Redis (cache) | DynamoDB / ScyllaDB (large scale)
โ โโ No โ Is it document-oriented?
โ โโ Yes โ MongoDB | Firestore | Couchbase
โ โโ No โ Is it graph relationships?
โ โโ Yes โ Neo4j | Dgraph
โ โโ No โ Is it time-series?
โ โโ Yes โ InfluxDB | TimescaleDB
โ โโ No โ Elasticsearch (text search / logs)
Choice Heuristics
| Requirement | Pick |
|---|
| Complex joins + strict consistency | PostgreSQL |
| Simple key-value, ultra-low latency | Redis / Dragonfly |
| Write-heavy, auto-shard | ScyllaDB / DynamoDB |
| Ad-hoc analytics on TB+ data | ClickHouse / Snowflake |
| Full-text search | Elasticsearch / Meilisearch |
| Embedded / mobile | SQLite / DuckDB |
| Real-time changefeeds | PostgreSQL (logical replication) / MongoDB (change streams) |
2. Schema Design & Data Modeling
Normalization
- 3NF by default โ eliminate partial + transitive dependencies
- Denormalize only when:
- Read throughput demands it (measured, not guessed)
- The join is proven as bottleneck in EXPLAIN plans
- You can tolerate eventual consistency
- The write path has compensating logic or TTL-based repair
Naming Conventions
- Tables:
snake_case, plural (users, order_items)
- Columns:
snake_case, singular (created_at, email)
- Foreign keys:
{singular_table}_id (user_id, order_id)
- Indexes:
idx_{table}_{column} or uq_{table}_{column} for unique
- Composite index:
idx_{table}_{col1}_{col2}
Data Types (PostgreSQL reference โ adapt per DB)
| What | Type | Why |
|---|
| Auto-increment ID | bigint or uuid | serial overflows at 2B |
| Money | numeric(12,2) | float loses precision |
| IPv4/IPv6 | inet | built-in operators + indexing |
| JSON | jsonb | GIN indexable, avoids reparse |
| Timestamp | timestamptz | timezone-safe |
| Text | text (not varchar(n)) | same perf, no arbitrary limit |
| Enum | text + CHECK or a lookup table | enums are hard to alter |
Anti-Patterns
- EAV (Entity-Attribute-Value) โ use JSONB or dynamic columns instead
- Polymorphic associations โ separate join tables per type
- One-size-fits-all
id column โ domain keys are clearer
- Storing computed values โ use generated columns or views
- Generic
meta text field โ use jsonb if you must
3. Indexing Strategy
Index Types
| Type | Best For | Tradeoff |
|---|
| B-tree (default) | Range queries, equality, sorting | Write overhead (~10%) |
| Hash | Exact equality only | No range, no sorting |
| GIN | JSONB, array, full-text search | Slow writes |
| GiST | Geometry, full-text, range types | Complex maintenance |
| BRIN | Time-series on append-only data | Saves space, coarse |
| Covering index | Index-only scans (no heap visits) | Wider index |
Index Design Rules
- Match query patterns โ index the WHERE columns, then ORDER BY, then SELECT (for covering)
- Leftmost prefix โ multi-column indexes work left-to-right
- Cardinality first โ put high-cardinality columns first in composite index
- Partial indexes โ
CREATE INDEX ... WHERE active = true
- Include columns โ
CREATE INDEX ... INCLUDE (col1, col2) for covering without widening the B-tree
- Avoid over-indexing โ each index slows writes by ~10%
- Drop unused indexes โ query
pg_stat_user_indexes / sys.dm_db_index_usage_stats
Common Patterns
CREATE INDEX idx_orders_status_date ON orders (status, created_at DESC);
CREATE INDEX idx_users_email_include ON users (email) INCLUDE (name, avatar_url);
CREATE INDEX idx_active_subscriptions ON subscriptions (user_id, expires_at)
WHERE status = 'active';
4. Query Optimization
The Process
- Capture โ log slow queries (
auto_explain, slow_query_log)
- Analyze โ
EXPLAIN (ANALYZE, BUFFERS, SETTINGS) in PostgreSQL
- Identify โ Seq Scan on large table? Nested Loop with high row estimates?
- Fix โ index, rewrite, materialize, cache
- Verify โ same
EXPLAIN ANALYZE after fix
What to Look For in EXPLAIN
| Red Flag | Fix |
|---|
Seq Scan on table > 10K rows | Add index |
| Row estimate off by 100x+ | ANALYZE, update stats |
Nested Loop with many iterations | Consider Hash Join or Merge Join |
Sort on large dataset | Index pre-sort |
Shared Hit Buffers count high | Increase shared_buffers |
Temp File appears | Increase work_mem |
Query Rewriting Patterns
SELECT * FROM users WHERE id IN (
SELECT user_id FROM orders WHERE amount > 100
);
SELECT DISTINCT u.* FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.amount > 100;
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';
Pagination Anti-Pattern
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 100000;
SELECT * FROM users WHERE id > 100000 ORDER BY id LIMIT 20;
5. ORM Optimization
N+1 Detection & Fix
- Detect: Enable query logging (
DEBUG=1, ORMDEBUG=1) โ watch for repeated identical queries
- Fix: Eager loading (
.include(), .prefetch_related(), JOIN FETCH)
ORM Pitfalls by Ecosystem
Prisma / TypeORM / Sequelize (Node.js)
- N+1 from lazy relations โ use
include / relations eagerly
findMany in loop โ batch with findMany({ where: { id: { in: ids } } })
- Avoid
findMany where raw SQL would be simpler (aggregations, window functions)
- Use raw SQL for bulk operations
ActiveRecord / Rails
includes(:orders) for eager loading vs joins(:orders)
pluck vs select โ pluck avoids object allocation
find_each / find_in_batches for memory-safe iteration
- Counter caches for
size calls on associations
SQLAlchemy / Django ORM (Python)
selectinload vs joinedload โ know the difference
only() / defer() to select fewer columns
bulk_create / bulk_update for batch operations
- Raw SQL via
text() for complex queries
General ORM Rules
- Eager load everything you'll touch, or use batch loading
- Avoid ORM for bulk operations โ raw SQL is 10-100x faster
- Use read replicas by configuring separate read/write connections
- Set statement timeout at the connection level
6. Migration Engineering
Zero-Downtime Migration Patterns
| Pattern | Strategy | Risk |
|---|
| Add column | ADD COLUMN ... DEFAULT NULL | Low โ NULL fills instantly |
| Add column with default | Add as NULL โ backfill โ set NOT NULL | Medium โ backfill locks |
| Rename column | Add new col โ dual-write โ backfill โ drop old | High โ application must write both |
| Change column type | Add new col with new type โ dual-write โ migrate โ swap | High |
| Split table | Create new table โ dual-write โ backfill โ switch | High |
| Add index | CONCURRENTLY (PG) or ALGORITHM=INPLACE (MySQL 8) | Low โ no table lock |
Migration Checklist
- Always have a rollback โ every
up has a down
- Test on a copy of production โ same data volume
- Small batches โ 1000 rows or 10s timeout per statement
- Lock analysis โ
pg_locks / SHOW PROCESSLIST during migration
- Monitor replication lag if using replicas
- Deploy in off-peak โ schedule during low traffic
Backfill Script Template
last_id = 0
batch_size = 1000
while True:
rows = db.execute("""
SELECT id FROM users
WHERE id > %s AND new_column IS NULL
ORDER BY id LIMIT %s
""", (last_id, batch_size))
if not rows:
break
db.execute("""
UPDATE users SET new_column = ...
WHERE id = ANY(%s)
""", ([r.id for r in rows],))
last_id = rows[-1].id
sleep(0.1)
7. Performance Tuning
Configuration Tuning (PostgreSQL)
| Parameter | Conservative | Aggressive | Notes |
|---|
shared_buffers | 25% RAM | 40% RAM | OS cache also matters |
effective_cache_size | 50% RAM | 75% RAM | Helps query planner |
work_mem | 4MB | 32-64MB | Per sort/hash op |
maintenance_work_mem | 64MB | 1GB | VACUUM, CREATE INDEX |
random_page_cost | 4 | 1.1 (SSD) / 1.5 (NVMe) | Guide planner away from seq scans |
max_connections | 100 | 20-50 (with pgbouncer) | Each connection = RAM |
wal_buffers | 16MB | 64MB | Write-ahead log |
Connection Pooling
- Use PgBouncer (transaction mode) or RDS Proxy
- Pool size =
(2 ร core_count) + effective_spindle_count
- Monitor
idle_in_transaction โ application bug if high
Caching Layers
โโโโโโโโโโโโ
โ Browser โ โ Cache-Control, ETag
โโโโโโโโโโโโค
โ CDN โ โ CloudFront, Cloudflare
โโโโโโโโโโโโค
โ App โ โ In-memory cache (local)
โโโโโโโโโโโโค
โ Redis โ โ Shared cache (cache-aside / write-through)
โโโโโโโโโโโโค
โ Database โ โ Buffer pool, WAL
โโโโโโโโโโโโ
Common Performance Anti-Patterns
- SELECT * (especially with JOINs) โ fetch only needed columns
- No LIMIT on queries returning many rows
- Long-running transactions holding locks
- Missing
NOT NULL constraints (planner can optimize with them)
- Implicit type conversion preventing index usage
8. Security Hardening
Must-Have
- Connection encryption โ TLS for all client-database connections
- Least privilege โ separate read/write users, no
superuser in apps
- Row-Level Security โ PostgreSQL RLS for multi-tenant data isolation
- Encryption at rest โ disk encryption + column-level with
pgcrypto
- Audit logging โ
pgaudit extension or query logging
- SQL injection prevention โ parameterized queries, never string interpolation
- Network isolation โ database in private subnet, no public endpoint
SQL Injection Prevention
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
Secret Management
- Never hardcode credentials
- Use environment variables or secret vault
- Rotate passwords every 90 days
- PostgreSQL:
pg_stat_activity to audit active connections
9. Architecture Patterns
Read Replicas
App โโโ Writer (primary)
โโโโ Read Replica 1 (SELECT)
โโโโ Read Replica 2 (SELECT, reporting)
โโโโ Read Replica 3 (analytics, backup)
- Route reads based on staleness tolerance
- Monitor replica lag with
pg_stat_replication
- Use
pg_hint_plan to force primary for critical reads
Sharding Strategies
- Hash-based โ even distribution, but resharding is painful
- Range-based โ natural boundaries (by date, region), but hotspots
- Directory-based โ lookup table maps key โ shard (flexible, extra hop)
- Use Vitess, Citus, or application-level sharding
CQRS (Command Query Responsibility Segregation)
- Commands โ normalized OLTP store
- Queries โ denormalized read model (materialized views, Elasticsearch)
- Eventually consistent between write and read sides
Multi-Tenant Patterns
| Pattern | Isolation | Cost | Complexity |
|---|
| Shared table + tenant_id col | Lowest | Lowest | Medium (RLS) |
| Schema per tenant | Medium | Medium | Low |
| Database per tenant | Highest | Highest | Low |
| Hybrid (tiered) | Configurable | Configurable | High |
10. Observability & Monitoring
Metrics to Track
| Metric | What It Tells You | Alert Threshold |
|---|
| Query latency (p50/p95/p99) | User-facing performance | p99 > 500ms |
| Connections count | Pool pressure | > 80% pool max |
| Cache hit ratio | Buffer pool effectiveness | < 95% |
| Replication lag | Replica freshness | > 10s |
| Dead tuples | Vacuum health | > 20% of live |
| Lock wait duration | Contention | > 1s |
| Disk IOPS / throughput | Hardware bottleneck | > 80% max IOPS |
Diagnostic Queries
SELECT query, calls, total_exec_time / calls AS avg_ms, rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 20;
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan < 100 AND indexrelid NOT IN (
SELECT indexrelid FROM pg_constraint WHERE conindid = indexrelid
);
SELECT pid, locktype, mode, granted, now() - query_start AS duration
FROM pg_locks l JOIN pg_stat_activity a ON a.pid = l.pid
WHERE NOT granted;
11. Ecosystem-Specific Deep Dives
PostgreSQL
- Extensions:
pgvector, postgis, timescaledb, pg_cron, pgaudit
- Replication:
pgoutput plugin โ Kafka / Debezium for CDC
- Full-text search:
tsvector / tsquery with GIN index
- Partitioning: declarative partitioning by range or list (PG 12+)
MySQL 8+
- Storage engine: InnoDB only (transactional), avoid MyISAM
- Buffer pool:
innodb_buffer_pool_size = 70-80% RAM
- DDL:
ALGORITHM=INPLACE, LOCK=NONE for online DDL
- Replication: Group Replication or async with GTID
MongoDB
- Schema design: embed or reference based on access patterns
- Indexes: compound indexes, TTL indexes, text indexes
- Aggregation pipeline: match early, project small
- Sharding: choose shard key carefully (monotonic = bad, hashed = better)
Redis
- Data structures: strings, hashes, lists, sets, sorted sets, streams
- Persistence: RDB (snapshot) + AOF (append-only log)
- Eviction:
allkeys-lru for cache, noeviction for persistent
- Cluster: hash slots, no cross-slot operations
SQLite
- WAL mode:
PRAGMA journal_mode=WAL for concurrent reads
- Write optimization: batch transactions, avoid autocommit
- Full-text search: FTS5 extension
- Limitations: no ALTER COLUMN, no concurrent writes
ClickHouse
- Column-oriented โ ideal for OLAP / analytics on large datasets
- MergeTree engine โ default; partition by date, order by primary key
- Materialized views โ push-down aggregations during INSERT, not on SELECT
- JOIN behavior โ right table must fit in memory or use
join_algorithm='parallel_hash'
- Limitations: no point-updates, no transactions, high CPU on joins
DuckDB
- In-process OLAP โ like SQLite for analytical queries
- Columnar engine โ runs queries on Pandas/Parquet directly
- Best for: Data science, ad-hoc analytics, ETL pipelines
- Zero-config โ no server, no config files
- Limitations: not designed for concurrent access, no network protocol
CockroachDB
- PostgreSQL-wire compatible โ most PG drivers work
- Auto-sharding โ range-based with automatic rebalancing
- Survivability โ survives entire AZ failure with
--locality settings
- Consistency โ SERIALIZABLE isolation by default (strong)
- Limitations: higher latency than single-node PG (distributed overhead), limited extensions
DynamoDB (NoSQL)
- Single-digit millisecond at any scale (if modeled right)
- Access patterns first โ design tables around query patterns, not normalization
- Primary key: partition key (hash) + optional sort key (range)
- GSI / LSI โ global/local secondary indexes (eventually consistent)
- Limitations: no joins, no transactions across partitions, 1 MB query limit
- Hot keys: uneven access patterns cause throttling โ use write sharding
12. Backup & Disaster Recovery
Backup Strategy
Full backup (daily) โโโ WAL archive (continuous) โโโ Point-in-time recovery
- PostgreSQL:
pg_basebackup + WAL archiving (wal-g, barman)
- MySQL:
mysqldump for small, XtraBackup for large
- MongoDB:
mongodump or file-system snapshot
- Test restores โ backup is only as good as your last successful restore
RPO / RTO Planning
| Tier | RPO | RTO | Strategy |
|---|
| Gold | 0-5 min | < 30 min | Synchronous replicas + WAL archive |
| Silver | 15 min | < 4 hr | Async replica + daily backup |
| Bronze | 24 hr | < 24 hr | Daily backup only |
13. Transaction Isolation & MVCC
Isolation Levels
| Level | Dirty Read | Non-Repeatable Read | Phantom Read | Snapshot | Use Case |
|---|
READ UNCOMMITTED | Possible | Possible | Possible | No | Approx counters, no real use |
READ COMMITTED | Safe | Possible | Possible | Per-statement | Default in PG/MySQL โ safe enough |
REPEATABLE READ | Safe | Safe | Possible (PG: safe) | Per-transaction | Reporting, financial audits |
SERIALIZABLE | Safe | Safe | Safe | โ | Banking, critical transactions |
MVCC Internals
- PostgreSQL: Each row has
xmin (creating transaction) / xmax (deleting/updating transaction). Old versions live in the same table until VACUUM reclaims them. SELECT sees only rows visible at the transaction's snapshot.
- MySQL (InnoDB): Undo log stores old versions in the rollback segment. The
read view determines visibility at transaction start.
- Snapshot too old: Long-running queries on busy tables hit "snapshot too old" (ORA-01555 in Oracle, error in PG with
old_snapshot_threshold). Mitigate with hot_standby_feedback on replicas.
Common MVCC Pitfalls
- Bloating โ long transactions prevent
VACUUM from reclaiming dead tuples
- IDLE in transaction โ holds snapshot, blocks cleanup
- DDL + MVCC โ
ALTER TABLE needs ACCESS EXCLUSIVE lock, waits for all active snapshots
When to Lower Isolation
Default: READ COMMITTED โ good for 95% of workloads
SERIALIZABLE โ only when you can't use
SELECT ... FOR UPDATE
or application-level optimistic locking
14. Database Testing Strategy
Unit Tests (no database)
- Test query construction logic, not the actual execution
- Mock the database connection / ORM session
- Use in-memory databases (SQLite :memory:) for quick feedback
Integration Tests (real database)
from testcontainers.postgres import PostgresContainer
with PostgresContainer("postgres:16") as pg:
conn = psycopg2.connect(pg.get_connection_url())
What to Test
| Layer | What | Tooling |
|---|
| Schema | Constraints, defaults, migration up/down | Flyway, Alembic, Prisma |
| Query | EXPLAIN plan, row count, correctness | pytest, pgTAP |
| Index | Whether the optimizer uses them | EXPLAIN (ANALYZE) in tests |
| Migration | Rollback, data preservation | Test in CI with fresh DB |
| Concurrency | Deadlocks, race conditions | Threaded test harness |
| Backup | Restore works | Restore to temp instance |
CI Pipeline Pattern
test-db:
services:
postgres:
image: postgres:16
env: POSTGRES_PASSWORD=test
steps:
- run: migrate up
- run: seed test data
- run: pytest tests/db/
- run: migrate down
Performance Regression Tests
- Run every query against a fixed dataset
- Assert
EXPLAIN shows no seq scans on large tables
- Measure p95 latency, fail if >2x baseline
15. Deadlock Detection & Prevention
What Causes Deadlocks
Transaction A: UPDATE accounts SET balance = balance - 100 WHERE id = 1;
Transaction B: UPDATE accounts SET balance = balance - 100 WHERE id = 2;
Transaction A: UPDATE accounts SET balance = balance - 100 WHERE id = 2; -- waits for B
Transaction B: UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- waits for A
โ DEADLOCK
Prevention Patterns
- Lock ordering โ always access rows in the same order (by id, alphabetically)
- Short transactions โ minimize the window for deadlocks
- Index all FK columns โ row-level locks escalate without index
NOWAIT / SKIP LOCKED โ bail out instead of waiting
- Retry logic โ deadlock victims should retry at the application layer
Detection (PostgreSQL)
SELECT blocked.pid AS blocked_pid,
blocking.pid AS blocking_pid,
blocked.query AS blocked_query,
blocking.query AS blocking_query,
now() - blocked.query_start AS blocked_duration
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking ON blocking.pid = ANY(
pg_blocking_pids(blocked.pid)
);
ALTER SYSTEM SET log_lock_waits = on;
ALTER SYSTEM SET deadlock_timeout = '1s';
Retry Template
import time
from psycopg2.errors import SerializationFailure
def execute_with_retry(cursor, sql, params, retries=3):
for attempt in range(retries):
try:
cursor.execute(sql, params)
return
except SerializationFailure:
if attempt == retries - 1:
raise
time.sleep(0.1 * (2 ** attempt))
16. Connection Strings & Driver Matrix
By Language ร Database
| Language | PostgreSQL | MySQL | MongoDB | Redis | SQLite |
|---|
| Node.js | pg / postgres.js | mysql2 | mongoose / mongodb | ioredis | better-sqlite3 |
| Python | psycopg2 / asyncpg | pymysql / aiomysql | pymongo / motor | redis-py / aioredis | sqlite3 (stdlib) |
| Go | pgx / lib/pq | go-sql-driver/mysql | mongo-go-driver | go-redis | modernc.org/sqlite |
| Rust | sqlx / tokio-postgres | sqlx / mysql_async | mongodb | redis-rs | rusqlite |
| Java | org.postgresql | com.mysql.cj | mongodb-driver-sync | jedis / lettuce | org.xerial:sqlite-jdbc |
Connection String Templates
PostgreSQL: postgresql://user:password@host:5432/dbname?sslmode=require&pool_min=2&pool_max=10
MySQL: mysql://user:password@host:3306/dbname?ssl-mode=REQUIRED&pool-max=10
MongoDB: mongodb://user:password@host:27017/dbname?maxPoolSize=10&w=majority
Redis: redis://:password@host:6379/0?pool_size=10
SQLite: sqlite:///path/to/db.sqlite?mode=rwc&journal_mode=WAL
Best Practices
- Never hardcode โ use environment variables or a vault
- Connection timeout โ set
connect_timeout=5 to fail fast
- Pool size โ align with DB
max_connections รท number of app instances
- SSL โ always
sslmode=require or verify-full in production
- Application name โ set
application_name=$APP to identify in pg_stat_activity
17. Capacity Planning & Sizing
Growth Estimation Formula
Storage per year = (row_size ร row_count ร growth_rate ร replication_factor) + indexes
Quick Sizing Rules
| Scale | Users | DB Size | Instance | Config |
|---|
| Tiny | < 1K | < 1 GB | 1 vCPU, 1 GB RAM | SQLite / PG default |
| Small | 1Kโ10K | 1โ50 GB | 2 vCPU, 4 GB RAM | Tune shared_buffers, work_mem |
| Medium | 10Kโ100K | 50โ500 GB | 4 vCPU, 16 GB RAM | Read replica + PgBouncer |
| Large | 100Kโ1M | 500 GBโ5 TB | 8 vCPU, 32 GB RAM | Connection pooling, caching |
| X-Large | 1M+ | 5 TB+ | 16+ vCPU, 64+ GB RAM | Sharding, CQRS, CDN |
When to Scale
| Signal | Action |
|---|
| CPU > 80% sustained | Vertical scale (bigger instance) |
| Disk IOPS > 80% | Faster storage (NVMe) or horizontal scale |
| Connections > 80% of max | Add PgBouncer or increase max_connections |
| Replication lag > 30s | Upgrade replica instance or reduce load |
| Query p99 > 1s | Optimize queries, add caching, or scale |
| Index rebuild takes > 1hr | Use CONCURRENTLY, schedule maintenance |
18. Real-World Case Studies
Case A: SaaS Platform Migration (MySQL โ PostgreSQL)
Problem: 500 GB MySQL 5.7, nightly REPLACE INTO jobs causing 4h downtime
Solution:
- Set up logical replication via
pg_chameleon
- Dual-write for 1 week (both MySQL + PG)
- Cutover: 15-min read-only window, verify row counts
- Result: nightly jobs now take 20 min, no downtime
Lesson: Always test cutover under production load first
Case B: E-Commerce Inventory Hotspot
Problem: Flash sales cause row lock contention on inventory table
Symptoms: LOG: process 1234 still waiting for ShareLock โ dead timeouts
Root Cause: 10,000 concurrent updates on the same sku row
Solutions Considered:
- โ
SERIALIZABLE โ worse contention
- โ
Redis cache + async decrement (eventual consistency, 1s delay)
- โ
Shard sku by warehouse (+1 for the hot sku)
- โ
pg_advisory_xact_lock with retry
Result: 95th percentile latency dropped from 12s to 40ms
Case C: Time-Series Bloat
Problem: 2 TB table of IoT sensor readings, 90% is "dead tuples"
Root Cause: Frequent UPDATE on a last_seen timestamp + no autovacuum tuning
Fix:
- Change
last_seen to a separate table (1 row per device, not per reading)
- Partition readings by month (
PARTITION BY RANGE (ts))
- Tune autovacuum:
autovacuum_vacuum_scale_factor = 0.01, autovacuum_vacuum_cost_limit = 2000
- Set
fillfactor = 70 on the high-churn table
Result: Table size dropped to 200 GB, query time -80%
19. Schema Generation Protocol
When the user asks you to generate a database schema from requirements, follow this protocol:
19.1 Intake Phase
Ask these questions (or infer from context) before writing a single CREATE TABLE:
1. Domain: What kind of system? (e-commerce / SaaS / CMS / IoT / chat / analytics)
2. Scale: Current users? Expected growth in 12 months?
3. Access patterns: Read-heavy? Write-heavy? Equal?
4. Consistency needs: Strong (ACID) or eventual?
5. Entities: What are the core nouns? (users, products, orders, ...)
6. Relationships: One-to-many? Many-to-many? Polymorphic?
7. Soft delete needed? Audit trail? Multi-tenancy?
8. Deployment: Single region? Multi-region?
9. Existing system? Migration from?
19.2 Output Format
Every generated schema MUST include:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'cancelled');
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE users IS 'Platform users';
CREATE UNIQUE INDEX idx_users_email ON users (email);
CREATE INDEX idx_users_created ON users (created_at DESC);
ALTER TABLE orders ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY orders_tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::BIGINT);
19.3 Accompanying Documents
Every schema generation must produce these three deliverables:
| Deliverable | Format | Content |
|---|
| Schema SQL | .sql | Full CREATE + INDEX + FK + RLS โ production-ready |
| Migration | .sql | ALTER statements from "blank" to this schema โ works on empty DB |
| Data Dictionary | markdown | Table descriptions, column meanings, example queries |
19.4 Quality Gates
Before declaring the schema "done":
20. Domain Schema Templates
Complete schema blueprints for common domains. Use these as starting points โ customize for specific requirements.
20.1 Multi-Tenant SaaS
CREATE TABLE tenants (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
plan TEXT NOT NULL DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')),
settings JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
email TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('admin', 'member', 'viewer')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(tenant_id, email)
);
CREATE INDEX idx_users_tenant ON users (tenant_id);
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY users_tenant_isolation ON users USING (tenant_id = current_setting('app.tenant_id')::BIGINT);
CREATE TABLE tenant_features (
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
feature TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (tenant_id, feature)
);
20.2 E-Commerce
CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT,
price NUMERIC(12,2) NOT NULL CHECK (price >= 0),
cost NUMERIC(12,2) CHECK (cost >= 0),
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'draft', 'archived')),
category_id BIGINT REFERENCES categories(id),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_products_status ON products (status) WHERE status = 'active';
CREATE INDEX idx_products_category ON products (category_id) WHERE category_id IS NOT NULL;
CREATE INDEX idx_products_sku ON products (sku);
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'paid', 'shipped', 'delivered', 'cancelled')),
total NUMERIC(12,2) NOT NULL,
discount NUMERIC(12,2) DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_orders_user_status ON orders (user_id, status);
CREATE TABLE order_items (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id),
product_id BIGINT NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price NUMERIC(12,2) NOT NULL,
total NUMERIC(12,2) NOT NULL
);
CREATE INDEX idx_order_items_order ON order_items (order_id);
20.3 Content / CMS
CREATE TABLE authors (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
bio TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE posts (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
author_id BIGINT NOT NULL REFERENCES authors(id),
slug TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
body TEXT NOT NULL,
excerpt TEXT,
status TEXT NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'published', 'archived')),
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_posts_author ON posts (author_id);
CREATE INDEX idx_posts_status_published ON posts (status, published_at DESC)
WHERE status = 'published';
CREATE INDEX idx_posts_search ON posts USING GIN (to_tsvector('english', title || ' ' || COALESCE(body, '')));
CREATE TABLE tags (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL
);
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)
);
20.4 IoT / Time-Series
CREATE TABLE devices (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
device_type TEXT NOT NULL,
location JSONB,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE readings (
id BIGINT GENERATED ALWAYS AS IDENTITY,
device_id BIGINT NOT NULL,
ts TIMESTAMPTZ NOT NULL,
metric TEXT NOT NULL,
value DOUBLE PRECISION NOT NULL,
PRIMARY KEY (id, ts)
) PARTITION BY RANGE (ts);
CREATE TABLE readings_2026_01 PARTITION OF readings
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE readings_2026_02 PARTITION OF readings
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE INDEX idx_readings_device_ts ON readings (device_id, ts DESC);
21. Migration Generation Protocol
When given two schemas (before / after) or asked to make a schema change, generate the migration following this protocol.
21.1 Input Format
Accept schema changes as:
- Full
CREATE TABLE statements (before / after)
- Description of desired change ("add a phone column to users")
- ALTER statements from another source
21.2 Migration Output
Every migration must include:
BEGIN;
SET lock_timeout = '5s';
ALTER TABLE users ADD COLUMN phone TEXT;
COMMIT;
BEGIN;
ALTER TABLE users DROP COLUMN phone;
COMMIT;
21.3 Migration Decision Matrix
| Change | Up Strategy | Down Strategy | Risk |
|---|
| Add column (nullable) | ADD COLUMN | DROP COLUMN | Low |
| Add column (NOT NULL) | Add NULL โ backfill โ SET NOT NULL | DROP COLUMN | Medium |
| Rename column | Add new โ dual-write โ backfill โ drop old | Restore old | High |
| Change column type | Add new โ dual-write โ migrate โ swap | Add old back | High |
| Add index | CREATE INDEX CONCURRENTLY | DROP INDEX CONCURRENTLY | Low |
| Drop index | DROP INDEX CONCURRENTLY | CREATE INDEX | Low |
| Add table | CREATE TABLE | DROP TABLE | Low |
| Drop table | Create backup โ DROP TABLE | Restore from backup | High |
| Add FK | ALTER TABLE ADD CONSTRAINT | DROP CONSTRAINT | Medium (table lock) |
| Partition existing table | Create new partitioned โ insert โ rename | Reverse rename | Very high |
21.4 Zero-Downtime Rules
- Never
ALTER TABLE ... SET NOT NULL without backfill first
- Never drop a column on day 1 โ mark as deprecated, drop N days later
- Always set
lock_timeout = '5s' before DDL on production
- Always provide a DOWN script
- For large tables, use
pt-online-schema-change (Percona) or pgroll (PostgreSQL)
- Test on a production-sized copy first
21.5 Accompanying Scripts
Reference these scripts (in scripts/) in generated migration output:
scripts/backfill.py for NOT NULL column backfill
scripts/migration-check.sql to run before applying
scripts/diagnostics.sql to verify after
22. Schema Audit Protocol
When the user gives you an existing schema, automatically run this audit.
22.1 Audit Checklist
Run through every table and flag violations:
Table: users
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โก PRIMARY KEY exists? โ โ
id BIGINT
โก TIMESTAMPTZ? โ โ created_at TIMESTAMP โ TIMESTAMPTZ
โก Missing indexes? โ โ no index on email
โก Missing NOT NULL? โ โ name is TEXT (nullable)
โก Missing updated_at? โ โ no updated_at column
โก FLOAT for money? โ โ
not applicable
โก Missing CHECK constraints? โ โ role TEXT with no CHECK
โก Missing RLS? โ โ
RLS enabled
โก Missing FK indexes? โ โ
tenant_id indexed
22.2 Scoring
| Score | Rating | Meaning |
|---|
| 90-100% | ๐ข Production-ready | Minor tuning suggestions |
| 70-89% | ๐ก Needs work | Address flagged items before production |
| 50-69% | ๐ Risky | Significant refactoring recommended |
| <50% | ๐ด Redesign | Schema has fundamental issues |
22.3 Audit Output Format
## Schema Audit: users
**Score: 72/100** ๐ก Needs work
### Critical (Fix before production)
- [ ] `created_at` uses `TIMESTAMP` โ change to `TIMESTAMPTZ`
- [ ] `email` has no unique index โ login queries will degrade
### Recommended
- [ ] Add `updated_at` with `ON UPDATE` trigger
- [ ] Add `CHECK (role IN ('admin', 'user'))` on role column
- [ ] Add `created_at` index for time-range queries
### Best Practice
- [ ] Consider soft-delete column `deleted_at TIMESTAMPTZ`
- [ ] Consider `EXCLUDE USING` for overlapping date ranges
### Summary
Table is functional but has 2 critical issues that will cause problems at scale.
Estimated fix time: 30 minutes.
22.4 Automatic Actions
When the user says "ๅธฎๆ็็่ฟไธช schema" (review my schema), the skill MUST:
- Run the audit checklist
- Generate the score
- Produce the audit report
- Offer to generate the migration that fixes all flagged issues
Meta-Layer Invocation Examples
| When the user says | The skill should |
|---|
| "ๅธฎๆ่ฎพ่ฎกไธไธช็ตๅๆฐๆฎๅบ" | Run Schema Generation Protocol (ยง19), output SaaS schema template โ customize โ generate migration |
| "่ฟๆฏๆ็ schema๏ผๅธฎๆ็็" | Run Schema Audit Protocol (ยง22), score it, produce report, offer migrations |
| "็ป users ่กจๅ ไธไธช phone ๅญๆฎต" | Generate migration (ยง21) with UP + DOWN, reference backfill.py |
| "ๆไธไธช 5 ไบฟ่ก็่กจ่ฝฌๆๅๅบ่กจ" | Generate migration plan with risk assessment, reference partitioning patterns |
| "ไป MySQL ่ฟๅฐ PostgreSQL" | Generate conversion plan with data type mapping + zero-downtime strategy |
Skill Invocation Triggers
This skill activates automatically when the user mentions any of:
- Database design, schema, data modeling, ERD
- SQL optimization, slow query, EXPLAIN, query tuning
- Indexing, index strategy, covering index
- ORM, N+1, lazy loading, eager loading
- Migration, zero-downtime, schema change, backfill
- Database choice, "which database", SQL vs NoSQL
- Performance, connection pooling, caching, sharding
- Database security, SQL injection, encryption
- Replication, read replica, high availability, failover
- Backup, recovery, RPO, RTO, disaster recovery
- Specific databases: PostgreSQL, MySQL, MongoDB, Redis, SQLite, etc.
- "ๅธฎๆ่ฎพ่ฎก" / "ๅธฎๆ็ๆ" / "็ๆ schema" / "็ๆๆฐๆฎๅบ" โ triggers Schema Generation (ยง19)
- "ๅธฎๆ็็" / "ๅฎก่ฎก" / "review" / "ๆฃๆฅ" โ triggers Schema Audit (ยง22)
- "ๅ ๅญๆฎต" / "ๆน่กจ" / "่ฟ็งป" / "alter" โ triggers Migration Generation (ยง21)