SQL es el contrato que sobrevive a tu ORM

2026-09-10

Cada producto que he lanzado ha sobrevivido al menos a una decisión sobre frameworks. La base de datos ha sobrevivido a todas. Cambian las versiones de Next.js, se sustituye el ORM y el equipo rota; las filas permanecen, junto con cada supuesto incorporado al esquema. Por eso considero que SQL es el contrato real del producto, no un detalle de implementación escondido tras una clase repository.

Prisma cubre gran parte de mi día: CRUD tipado, migraciones en el repositorio y un esquema que puede leer alguien recién incorporado. Pero también leo el SQL que genera. Si una página de listado lanza una consulta por fila, un informe necesita una función de ventana o un dashboard requiere un GROUP BY sobre tres joins, escribo la consulta a mano con $queryRaw o la guardo en una vista. Antes de que alguien añada Redis, abro EXPLAIN ANALYZE. La mayoría de los tickets «necesitamos una caché» esconden un índice que falta.

Las restricciones son el control de calidad más barato que existe. NOT NULL, UNIQUE, FOREIGN KEY y CHECK se aplican en cada escritura, desde cada servicio, incluso desde el script que alguien ejecuta a mano un viernes. Zod protege la API; la base de datos protege la verdad. El dinero se almacena como unidades menores enteras o NUMERIC, nunca como float. Los webhooks de pago llegan dos veces, por eso la clave de idempotencia necesita un índice único, no un comentario en el handler.

Los problemas de concurrencia son donde viven los errores de FinTech. Dos solicitudes leen un saldo, ambas deciden que hay suficiente y ambas escriben: es una actualización perdida, y ninguna validación en el frontend la arregla. Prefiero una instrucción atómica —UPDATE ... SET balance = balance - $1 WHERE id = $2 AND balance >= $1— y comprobar después que cambió exactamente una fila. Otra opción es una transacción con SELECT ... FOR UPDATE y una tabla de ledger de solo anexado para poder explicar cada movimiento. En sistemas que han procesado más de 1,5 millones de dólares, «poder explicarlo después» es gran parte del trabajo.

Los índices deben seguir las consultas reales, no las columnas que parecen importantes. En índices compuestos, primero van las columnas de igualdad y al final la de rango u ordenación; los índices parciales sirven para filas «activas», y la paginación por clave es preferible a OFFSET 50000. Las migraciones en tablas activas se hacen por etapas: añadir la columna como nullable, rellenarla por lotes y luego agregar la restricción; crear el índice con CONCURRENTLY. Una migración que bloquea la tabla de pedidos al mediodía es una caída con una pull request adjunta.

Rechazo la tabla entity-attribute-value «flexible» en lugar de un esquema, usar JSONB para esconder campos esenciales que nadie quiere modelar, aplicar soft delete en todas partes sin pensar en las restricciones únicas y poner una caché delante de una consulta que nadie ha analizado con EXPLAIN. Si me envías una página lenta o un esquema dudoso, adjunta la consulta y el número de filas. Te responderé con el plan, el índice y la primera restricción que añadiría.

Dale al próximo producto alguien que se quede.

¿Listo para empezar? Envíame un mensaje: puedo comenzar ahora y quedarme. Colaboraciones a largo plazo, no una entrega de dos semanas. Hablemos de tu proyecto, requisitos y la mejor forma de avanzar.

Pregunta sobre mi experiencia o servicios