Playbooks

Database

Salir

Schema Design

La base de datos sobrevive a tu código. Un mal schema te persigue durante años. Uno bueno es invisible — simplemente funciona.

Naming conventions

Tablas en plural, snake_case ✅ user_orders ❌ UserOrder / userOrders
Primary keys: id (UUID o bigint) ✅ id UUID DEFAULT gen_random_uuid() ❌ userId SERIAL
Foreign keys: tabla_singular_id ✅ user_id REFERENCES users(id) ❌ userId / user_fk
Timestamps en todas las tablas ✅ created_at, updated_at ❌ created / lastUpdate
Índices: idx_tabla_columna ✅ CREATE INDEX idx_orders_user_id ON orders(user_id) ❌ user_id_idx / index1
Booleanos: is_ o has_ ✅ is_active, has_subscription ❌ active, subscribed
Enum como string con CHECK ✅ status TEXT CHECK (status IN (...)) ❌ ENUM type (difícil de migrar)

Tipos de datos — qué usar y qué evitar

UUID
✅ Primary keys, external IDs
❌ IDs autoincrementales (PK de MySQL clásico)
TEXT / VARCHAR(n)
✅ Strings con tamaño predecible
❌ VARCHAR sin límite si no sabés el tamaño
TIMESTAMPTZ
✅ Siempre. TIMESTAMP WITH TIME ZONE
❌ TIMESTAMP sin zona horaria. Pesadilla garantizada.
NUMERIC / DECIMAL
✅ Dinero, porcentajes financieros
❌ FLOAT para dinero. 0.1 + 0.2 !== 0.3
JSONB
✅ Datos semi-estructurados, metadata flexible
❌ JSON (sin B). No indexable. O usás JSONB o no usás JSON en DB.
BOOLEAN
✅ Flags binarios
❌ TINYINT / INTEGER para booleanos. El tipo existe para algo.

Migrations

Las migraciones son el historial clínico de tu base de datos. Si no podés recrear la DB desde cero con un comando, no tenés migraciones — tenés deuda.

1. Cada migración es un archivo con timestamp. Nunca edites una migración ya aplicada en producción.
2. Migraciones forward-only en producción. Si necesitás revertir, creá una nueva migración que deshaga el cambio.
3. Probá las migraciones en staging antes de producción. Un ALTER TABLE en producción sin probar es jugar a la ruleta rusa.
4. Nunca mezcles cambios de schema con cambios de datos en la misma migración.
5. Incluí seeds para desarrollo local. Que levantar el proyecto desde cero sea un solo comando.
6. Migraciones deben ser reversibles (método down). Aunque no lo uses en prod, sirve para desarrollo.

Query Patterns

Una query mal escrita puede tirar abajo producción. Las reglas no son muchas — pero violarlas es caro.

N+1 Queries
Hacés una query para obtener N registros, y luego N queries más para obtener datos relacionados.
✅ Eager loading (JOIN o INCLUDE). Una query con JOIN es mejor que N+1 queries separadas.
SELECT *
Treaés todas las columnas aunque solo necesites 2. Desperdiciás ancho de banda, memoria, y tiempo de query.
✅ Seleccioná solo las columnas que necesitás. Especialmente importante en tablas con columnas JSONB o TEXT grandes.
Missing index
Queries lentas que hacen full table scan. Se vuelve exponencial con el crecimiento de datos.
✅ Creá índices en columnas usadas en WHERE, JOIN y ORDER BY. Monitoreá slow queries y creá índices según uso real, no adivinando.
Query en loop
Ejecutás una query dentro de un for loop. Peor que N+1 porque ni siquiera es intencional.
✅ Usá WHERE IN con arrays o batch inserts. Una query que procesa 1000 registros es mejor que 1000 queries de 1 registro.
Sin paginación
SELECT sin LIMIT/OFFSET en una tabla con millones de registros. El backend se cae, la DB se satura.
✅ Siempre paginá. Usá cursor-based pagination para datasets que cambian frecuentemente. OFFSET se rompe si se insertan/eliminan filas.

Índices: la diferencia entre 10ms y 10s

1. Creá índices basado en queries reales, no en suposiciones. Monitoreá slow queries.
2. Un índice por query frecuente. Demasiados índices ralentizan escrituras (INSERT, UPDATE, DELETE).
3. Índices compuestos: el orden importa. WHERE (a, b) necesita índice en (a, b), no en (b, a).
4. Partial indexes para queries con WHERE fijo. Más chicos, más rápidos, menos overhead de escritura.
5. EXPLAIN ANALYZE antes de deployar. Si el query plan no usa tu índice, el índice no sirve.

Transacciones & Locks

Una transacción bien usada es una red de seguridad. Mal usada, es un cuello de botella que bloquea toda la aplicación.

Niveles de isolation

READ COMMITTED
Default en PostgreSQL. Cada query ve datos commiteados. Suficiente para el 90% de los casos.
REPEATABLE READ
Todas las lecturas dentro de la transacción ven el mismo snapshot. Útil para reportes que necesitan consistencia.
SERIALIZABLE
La transacción falla si otra transacción modificó datos que esta leyó. Máxima seguridad, mínimo rendimiento.

Reglas de oro

1. Abrí transacciones solo cuando sea necesario. Cada transacción bloquea recursos.
2. Mantené las transacciones cortas. Nada de llamadas HTTP o emails dentro de una transacción.
3. Si actualizás múltiples tablas que deben ser atómicas, usá una transacción. Si falla una, fallan todas.
4. Cuidado con los deadlocks: dos transacciones esperándose mutuamente. Orden consistente de updates lo previene.
5. En producción, siempre manejá errores de transacción y reintentá con backoff exponencial.