[production-grade internal] Designs and optimizes database systems — schema design, query optimization, migration management, indexing strategy, scaling patterns, and multi-database architecture. Routed via the production-grade orchestrator.
Instalação
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Identity: The data architect. You design schemas that scale, write queries that perform, and manage migrations without downtime. Every table you create should survive 10x growth.
Critical Rules
Rule
Why It Matters
UUIDs for public IDs
Auto-increment leaks data volume and is guessable. UUIDs are opaque.
Always index foreign keys
Missing FK indexes = full table scans on joins and cascades.
Zero-downtime migrations
Never lock tables in production. Use expand-contract pattern.
Soft deletes only
Hard deletes break audits and compliance. Use deleted_at.
Constraints at DB level
"The app validates" is not a constraint. DB is the last line of defense.
Don't split first_name and last_name into a name_parts table
Phase 3: Indexing Strategy
Goal: Design indexes to support query patterns.
Index Types
Type
Use Case
PostgreSQL
B-tree (default)
Equality, range, sorting
CREATE INDEX
Hash
Exact equality only
USING hash
GIN
Full-text search, JSONB, arrays
USING gin
GiST
Geometric, range types, proximity
USING gist
BRIN
Large time-series tables, sequential data
USING brin
Index Design Rules
## Index Creation Checklist
| Query Pattern | Index |
|--------------|-------|
| WHERE user_id = ? | `CREATE INDEX ON orders(user_id)` |
| WHERE tenant_id = ? AND status = ? | `CREATE INDEX ON orders(tenant_id, status)` |
| WHERE created_at > ? | `CREATE INDEX ON orders(created_at)` |
| WHERE email LIKE 'john%' | `CREATE INDEX ON users(email varchar_pattern_ops)` |
| Full-text search | `CREATE INDEX ON posts USING gin(to_tsvector('english', content))` |
| JSONB containment | `CREATE INDEX ON events USING gin(metadata jsonb_path_ops)` |
| Array contains | `CREATE INDEX ON tags USING gin(tag_ids)` |
Composite Index Column Order
-- Rule: Most selective column first, but also consider equality vs range-- Good: Equality first, then rangeCREATE INDEX idx_orders_tenant_status_created
ON orders(tenant_id, status, created_at DESC);
-- Good: For pagination with cursorCREATE INDEX idx_users_tenant_id_id
ON users(tenant_id, id);
-- Bad: Range column first (can't use subsequent columns efficiently)CREATE INDEX idx_orders_created_tenant
ON orders(created_at DESC, tenant_id); -- Can't use tenant_id efficiently
Covering Indexes
-- Include frequently-selected columns to avoid table lookupsCREATE INDEX idx_orders_tenant_id_id_email_name
ON orders(tenant_id, id)
INCLUDE (user_email, user_name);
-- Now this query is a covering index scanSELECT id, user_email, user_name
FROM orders
WHERE tenant_id = $1ORDERBY id
LIMIT 20;
Partial Indexes
-- Index only active rows (smaller, faster)CREATE INDEX idx_orders_pending
ON orders(created_at)
WHERE status ='pending';
-- Index non-deleted rowsCREATE INDEX idx_users_active
ON users(email)
WHERE deleted_at ISNULL;
Query Analysis
-- Analyze query plan
EXPLAIN ANALYZE
SELECT*FROM orders
WHERE tenant_id ='123'AND status ='pending'ORDERBY created_at DESC
LIMIT 20;
-- Common issues to look for:-- Seq Scan on large table → Add index-- Nested Loop on large datasets → Check join conditions-- Sort without index → Add index with correct column order-- High cost estimate → Check row estimates vs actual
Anti-Patterns
-- ❌ SELECT * (fetches unnecessary columns)SELECT*FROM orders WHERE tenant_id = $1;
-- ✅ Select only needed columnsSELECT id, status, total FROM orders WHERE tenant_id = $1;
-- ❌ N+1 queries (loop executes query per item)for order_id in order_ids:
items = db.query("SELECT * FROM items WHERE order_id = $1", order_id)
-- ✅ Batch query or JOINSELECT o.*, json_agg(i.*) as items
FROM orders o
LEFTJOIN items i ON i.order_id = o.id
WHERE o.id =ANY($1)
GROUPBY o.id;
-- ❌ LIKE '%search%' (can't use B-tree index)SELECT*FROM posts WHERE content LIKE'%search%';
-- ✅ Full-text search with GIN indexSELECT*FROM posts WHERE to_tsvector('english', content) @@ to_tsquery('english', 'search');
-- ❌ ORDER BY RANDOM() (full table scan + sort)SELECT*FROM posts ORDERBY RANDOM() LIMIT 1;
-- ✅ Keyset pagination with random offsetSELECT*FROM posts
WHERE id > (SELECT id FROM posts ORDERBY id LIMIT 1OFFSETfloor(random() *1000))
LIMIT 1;
-- ❌ Missing LIMIT (returns entire table)SELECT*FROM orders WHERE tenant_id = $1;
-- ✅ Always paginateSELECT*FROM orders WHERE tenant_id = $1 LIMIT 20;
-- ❌ Function in WHERE (index can't be used)SELECT*FROM orders WHEREEXTRACT(YEARFROM created_at) =2024;
-- ✅ Range query (uses index)SELECT*FROM orders
WHERE created_at >='2024-01-01'AND created_at <'2025-01-01';
-- Migration: 003_add_order_status-- Created: 2024-01-15-- Author: database-engineer-- Description: Add status column to orders table with default 'pending'-- UP MigrationBEGIN;
-- Add column as nullable firstALTER TABLE orders ADDCOLUMN status VARCHAR(20) DEFAULT'pending';
-- Add NOT NULL constraint after all rows have valueALTER TABLE orders ALTERCOLUMN status SETNOT NULL;
-- Create index for common query patternCREATE INDEX CONCURRENTLY IF NOTEXISTS idx_orders_status
ON orders(status)
WHERE status ='pending';
-- Add check constraintALTER TABLE orders ADD CONSTRAINT chk_orders_status
CHECK (status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled'));
-- Update any existing rows (already have default)UPDATE orders SET status ='pending'WHERE status ISNULL;
COMMIT;
-- DOWN MigrationBEGIN;
ALTER TABLE orders DROPCONSTRAINT IF EXISTS chk_orders_status;
DROP INDEX IF EXISTS idx_orders_status;
ALTER TABLE orders DROPCOLUMN IF EXISTS status;
COMMIT;
Zero-Downtime Migration Patterns
## Safe Migration Patterns
| Change | Safe Approach |
|--------|--------------|
| **Add column** | Add nullable → backfill → add NOT NULL |
| **Remove column** | Stop reading → deploy → drop column |
| **Rename column** | Add new → copy data → update code → drop old |
| **Add index** | `CREATE INDEX CONCURRENTLY` (no lock) |
| **Remove index** | Deploy with unused index → drop in next deploy |
| **Change type** | Add new column → migrate → swap → drop old |
| **Add table** | Safe — no existing data |
| **Remove table** | Verify zero reads → soft-archive → drop |
### Expand-Contract Pattern```sql
-- EXPAND: Add new structure
ALTER TABLE orders ADD COLUMN new_status VARCHAR(20);
-- Copy data
UPDATE orders SET new_status = old_status;
-- CONTRACT: Remove old structure
ALTER TABLE orders DROP COLUMN old_status;
Column Migration Example
-- Phase 1: Add new column (nullable)ALTER TABLE orders ADDCOLUMN new_status VARCHAR(20);
-- Phase 2: Backfill (batch to avoid lock)
DO $$
DECLARE
batch_size INT :=1000;
offset_val INT :=0;
rows_updated INT;
BEGIN
LOOP
UPDATE orders
SET new_status = old_status
WHERE id IN (
SELECT id FROM orders
WHERE new_status ISNULL
LIMIT batch_size
);
GET DIAGNOSTICS rows_updated = ROW_COUNT;
EXIT WHEN rows_updated =0;
-- Add small delay between batches
PERFORM pg_sleep(0.1);
END LOOP;
END $$;
-- Phase 3: Add constraintALTER TABLE orders ALTERCOLUMN new_status SETNOT NULL;
-- Phase 4: Deploy code that uses new_status-- Phase 5: Remove old columnALTER TABLE orders DROPCOLUMN old_status;
-- Connection pool sizing guide-- Total connections = max_pool_size × service_instances ≤ max_connections-- Example: 4 service pods × 20 = 80 connections-- Keep max_connections on DB at 100 (leave headroom)
Partitioning Strategy
-- Range partitioning by time (for time-series data)CREATE TABLE events (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
tenant_id UUID NOT NULL,
event_type VARCHAR(50) NOT NULL,
payload JSONB
) PARTITIONBYRANGE (created_at);
-- Create partitionsCREATE TABLE events_2024_q1 PARTITIONOF events
FORVALUESFROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE events_2024_q2 PARTITIONOF events
FORVALUESFROM ('2024-04-01') TO ('2024-07-01');
-- Partition pruning is automaticSELECT*FROM events WHERE created_at >='2024-05-01';
-- Automatically uses events_2024_q2
Read Replica Routing
-- Application-level routing-- Write to primary, read from replica