PostgreSQL JSONB vs tablas normalizadas: cómo decidir y migrar
PostgreSQL JSONB vs tablas normalizadas: un marco práctico para elegir en prototipos y una ruta de migración segura a medida que la app escala.

El problema real: avanzar rápido sin atraparte
Los requisitos que cambian cada semana son normales cuando estás construyendo algo nuevo. Un cliente pide un campo más. Ventas quiere un flujo distinto. Soporte necesita un historial de auditoría. Tu base de datos acaba cargando con todas esas variaciones.
Iterar rápido no significa solo lanzar pantallas más deprisa. Significa poder añadir, renombrar y quitar campos sin romper informes, integraciones o registros antiguos. También significa poder responder nuevas preguntas ("¿Cuántos pedidos tuvieron notas de entrega faltantes el mes pasado?") sin convertir cada consulta en un script puntual.
Por eso la elección entre JSONB y tablas normalizadas importa desde el principio. Ambos pueden funcionar, y ambos pueden causar dolor si se usan para el trabajo equivocado. JSONB se siente como libertad porque puedes almacenar casi cualquier cosa hoy. Las tablas normalizadas se sienten más seguras porque imponen estructura. El objetivo real es emparejar el modelo de almacenamiento con cuán inciertos son tus datos ahora y con qué rapidez deben volverse fiables.
Cuando los equipos eligen el modelo equivocado, los síntomas suelen ser obvios:
- Preguntas simples se convierten en consultas lentas y desordenadas o en código personalizado.
- Dos registros representan lo mismo pero usan nombres de campo diferentes.
- Campos opcionales se vuelven obligatorios más tarde y los datos antiguos no coinciden.
- No puedes imponer reglas (valores únicos, relaciones obligatorias) sin soluciones alternativas.
- Los reportes y exportaciones siguen rompiéndose tras pequeños cambios.
La decisión práctica es esta: ¿dónde necesitas flexibilidad (y puedes tolerar inconsistencia por un tiempo) y dónde necesitas estructura (porque los datos mueven dinero, operaciones o cumplimiento)?
JSONB y tablas normalizadas, explicado sencillamente
PostgreSQL puede almacenar datos en columnas clásicas (texto, número, fecha). También puede guardar todo un documento JSON dentro de una columna usando JSONB. La diferencia no es "nuevo vs viejo". Es lo que quieres que la base de datos garantice.
JSONB almacena claves, valores, arrays y objetos anidados. No impone automáticamente que cada fila tenga las mismas claves, que los valores siempre sean del mismo tipo o que un elemento referenciado exista en otra tabla. Puedes añadir comprobaciones, pero tienes que decidirlas e implementarlas.
Las tablas normalizadas implican separar datos en tablas distintas según lo que es cada cosa y conectarlas con IDs. Un cliente está en una tabla, una orden en otra, y cada orden apunta a un cliente. Esto te da mayor protección contra contradicciones.
En el trabajo diario, los trade-offs son directos:
- JSONB: flexible por defecto, fácil de cambiar, más propenso a derivar.
- Tablas normalizadas: más deliberadas para cambiar, más fáciles de validar y de consultar de forma consistente.
Un ejemplo simple son los campos personalizados de tickets de soporte. Con JSONB, puedes añadir un campo nuevo mañana sin migración. Con tablas normalizadas, añadir un campo es más intencional, pero los reportes y las reglas quedan más claros.
Cuándo JSONB es la herramienta adecuada para iterar rápido
JSONB es una gran opción cuando tu mayor riesgo es construir la forma equivocada de los datos, no imponer reglas estrictas. Si tu producto aún está encontrando su flujo de trabajo, forzar todo en tablas fijas puede frenarte con migraciones constantes.
Un buen indicio es cuando los campos cambian semanalmente. Piensa en un formulario de incorporación donde marketing sigue añadiendo preguntas, renombrando etiquetas y quitando pasos. JSONB te permite guardar cada envío tal cual, aunque la versión de mañana se vea distinta.
JSONB también encaja con los “desconocidos”: datos que aún no entiendes por completo o que no controlas. Si ingieres payloads de webhooks de socios, guardar el payload crudo en JSONB te permite soportar nuevos campos de inmediato y decidir después cuáles deben convertirse en columnas de primera clase.
Usos tempranos comunes incluyen formularios que cambian rápido, captura de eventos y registros de auditoría, configuraciones por cliente, feature flags y experimentos. Es especialmente útil cuando principalmente escribes los datos, los lees completos y la forma sigue moviéndose.
Una medida de control ayuda más de lo que la gente espera: mantén una nota corta y compartida de las claves que usas para que no termines con cinco grafías distintas de la misma propiedad en distintas filas.
Cuándo las tablas normalizadas son la opción más segura a largo plazo
Las tablas normalizadas ganan cuando los datos dejan de ser "solo para esta característica" y se vuelven compartidos, consultados y confiables. Si la gente va a segmentar y filtrar registros de muchas maneras (estado, propietario, región, periodo), las columnas y relaciones hacen el comportamiento predecible y más fácil de optimizar.
La normalización también importa cuando las reglas deben ser impuestas por la base de datos, no por código de aplicación "a fuerza de voluntad". JSONB puede almacenar cualquier cosa, lo cual es exactamente el problema cuando necesitas garantías fuertes.
Señales de que deberías normalizar ahora
Suele ser hora de alejarse de un modelo JSON-first cuando varias de estas son ciertas:
- Necesitas reporting y dashboards consistentes.
- Necesitas restricciones como campos obligatorios, valores únicos o relaciones con otras entidades.
- Más de un servicio o equipo lee y escribe los mismos datos.
- Las consultas empiezan a escanear muchas filas porque no pueden usar índices simples de forma efectiva.
- Estás en un entorno regulado o auditado y las reglas deben ser demostrables.
El rendimiento es un punto de inflexión común. Con JSONB, filtrar a menudo significa extraer valores repetidamente. Puedes indexar rutas JSON, pero los requisitos tienden a crecer hasta formar un parcheo de índices difícil de mantener.
Un ejemplo concreto
Un prototipo guarda “solicitudes de cliente” como JSONB porque cada tipo de solicitud tiene campos distintos. Más tarde, operaciones necesita una cola filtrada por prioridad y SLA. Finanzas necesita totales por departamento. Soporte necesita garantizar que cada solicitud tenga un customer_id y un status. Ahí es donde brillan las tablas normalizadas: columnas claras para campos comunes, claves foráneas a clientes y equipos, y restricciones que impiden que entre mala data.
Un marco de decisión simple que puedes usar en 30 minutos
No necesitas un gran debate sobre teoría de bases de datos. Necesitas una respuesta rápida y escrita a una pregunta: ¿dónde vale más la flexibilidad que la estructura estricta?
Haz esto con las personas que construyen y usan el sistema (desarrollador, operaciones, soporte y quizá finanzas). El objetivo no es escoger un ganador absoluto. Es elegir el ajuste correcto por área del producto.
La lista de verificación de 5 pasos
-
Enumera tus 10 pantallas más importantes y las preguntas exactas detrás de ellas. Ejemplos: “abrir un registro de cliente”, “encontrar órdenes vencidas”, “exportar los pagos del mes pasado”. Si no puedes nombrar la pregunta, no puedes diseñarla.
-
Destaca los campos que deben ser correctos siempre. Estas son reglas estrictas: estado, importes, fechas, propiedad, permisos. Si un valor erróneo costaría dinero o desataría un problema de soporte, suele pertenecer a columnas normales con restricciones.
-
Marca lo que cambia a menudo vs lo que cambia raramente. Cambios semanales (nuevas preguntas de formulario, detalles específicos de socios) son fuertes candidatos para JSONB. Los campos “centrales” que cambian raramente apuntan a normalización.
-
Decide qué debe ser buscable, filtrable u ordenable en la UI. Si los usuarios filtran constantemente por ello, suele ser mejor como columna de primera clase (o una ruta JSONB indexada con cuidado).
-
Elige un modelo por área. Una división común es tablas normalizadas para entidades y flujos centrales, y JSONB para extras y metadatos que cambian rápido.
Conceptos básicos de rendimiento sin perderse en detalles
La velocidad suele venir de una cosa: hacer que tus preguntas más comunes sean baratas de responder. Eso importa más que la ideología.
Si usas JSONB, mantenlo pequeño y predecible. Unos pocos campos extra están bien. Un blob gigantesco y siempre cambiante es difícil de indexar y fácil de malusar. Si sabes que una clave existirá (como "priority" o "source"), mantén el nombre de la clave consistente y el tipo de valor consistente.
Los índices no son magia. Cambian lecturas más rápidas por escrituras más lentas y más disco. Indexa solo lo que filtras o unes a menudo, y solo en la forma en que realmente consultas.
Reglas prácticas de indexado
- Pon índices btree normales en filtros comunes como status, owner_id, created_at, updated_at.
- Usa un índice GIN en una columna JSONB cuando busques dentro de ella a menudo.
- Prefiere índices de expresión para una o dos claves JSON calientes (por ejemplo (meta-\u003e\u003e'priority')) en lugar de indexar todo el JSONB.
- Usa índices parciales cuando solo una porción importa (por ejemplo, solo filas donde status = 'open').
Evita almacenar números y fechas como cadenas dentro de JSONB. "10" ordena antes que "2", y la aritmética de fechas se vuelve problemática. Usa tipos numéricos y timestamp reales en columnas, o al menos guarda números JSON como números.
Un modelo híbrido suele ganar: campos centrales en columnas, extras flexibles en JSONB. Ejemplo: una tabla de operaciones con id, status, owner_id, created_at como columnas, más meta JSONB para respuestas opcionales.
Errores comunes que causan dolor más adelante
JSONB puede sentirse como libertad al principio. El dolor suele aparecer meses después, cuando más gente toca los datos y el “lo que sea sirve” se convierte en “no podemos cambiar esto sin romper algo”.
Estos patrones causan la mayor parte del trabajo de limpieza:
- Tratar JSONB como un vertedero. Si cada equipo guarda formas ligeramente distintas, terminas escribiendo lógica de parseo personalizada por todas partes. Establece convenciones básicas: nombres de clave consistentes, formatos de fecha claros y un pequeño campo de versión dentro del JSON.
- Ocultar entidades centrales dentro de JSONB. Guardar clientes, órdenes o permisos solo como blobs parece simple al principio, pero luego los joins se vuelven incómodos, las restricciones difíciles de imponer y aparecen duplicados. Mantén el quién/qué/cuándo en columnas y pon detalles opcionales en JSONB.
- Esperar a pensar en la migración hasta que sea urgente. Si no rastreas qué claves existen, cómo cambiaron y cuáles son “oficiales”, tu primera migración real será arriesgada.
- Suponer que JSONB automáticamente significa flexible y rápido. Flexibilidad sin reglas es solo inconsistencia. La velocidad depende de patrones de acceso e índices.
- Romper la analítica cambiando claves con el tiempo. Renombrar status a state, cambiar números a cadenas o mezclar zonas horarias arruinará reportes silenciosamente.
Un ejemplo concreto: un equipo empieza con una tabla tickets y un campo details JSONB para respuestas de formularios. Más tarde, finanzas quiere desglose semanal por categoría, operaciones quiere seguimiento de SLA y soporte quiere dashboards “abiertos por equipo”. Si categorías y timestamps se dispersan entre claves y formatos, cada reporte se vuelve una consulta ad-hoc.
Un plan de migración cuando el prototipo se vuelve crítico
Cuando un prototipo empieza a manejar nómina, inventario o soporte al cliente, “lo arreglamos después” deja de ser aceptable. El camino más seguro es migrar en pasos pequeños, manteniendo los datos JSONB antiguos funcionando mientras la nueva estructura demuestra su validez.
Un enfoque por fases evita una reescritura arriesgada de una sola vez:
- Diseña el destino primero. Escribe las tablas objetivo, claves primarias y reglas de nombrado. Decide qué es una entidad real (Customer, Ticket, Order) y qué se queda flexible (notas, atributos opcionales).
- Construye tablas nuevas junto a los datos antiguos. Mantén la columna JSONB, añade tablas normalizadas e índices en paralelo.
- Backfill en lotes y valida. Copia campos JSONB a las tablas nuevas por partes. Valida con conteos de filas, campos obligatorios no nulos y comprobaciones puntuales.
- Cambia lecturas antes que escrituras. Actualiza consultas e informes para leer de las tablas nuevas primero. Cuando las salidas coincidan, empieza a escribir los cambios nuevos en las tablas normalizadas.
- Ciérralo. Deja de escribir en JSONB, luego elimina o congela los campos antiguos. Añade restricciones (claves foráneas, reglas únicas) para que no vuelva a entrar mala data.
Antes del corte final:
- Ejecuta ambas rutas durante una semana (antigua vs nueva) y compara salidas.
- Monitorea consultas lentas y añade índices donde haga falta.
- Prepara un plan de reversión (feature flag o conmutador de configuración).
- Comunica la hora exacta del cambio de escrituras al equipo.
Comprobaciones rápidas antes de comprometerte
Antes de consolidar tu enfoque, haz una verificación de realidad. Estas preguntas capturan la mayoría de los problemas futuros mientras el cambio sigue siendo barato.
Cinco preguntas que deciden la mayor parte del resultado
- ¿Necesitamos unicidad, campos obligatorios o tipos estrictos ahora (o en la próxima versión)?
- ¿Qué campos deben poder filtrarse y ordenarse para los usuarios (búsqueda, estado, propietario, fechas)?
- ¿Necesitaremos dashboards, exportaciones o reportes para finanzas/operaciones pronto?
- ¿Podemos explicar el modelo de datos a un nuevo compañero en 10 minutos, sin ambigüedades?
- ¿Cuál es nuestro plan de reversión si una migración rompe un flujo?
Si respondes “sí” a las tres primeras, ya estás inclinándote hacia tablas normalizadas (o al menos un híbrido: campos centrales normalizados y atributos de larga cola en JSONB). Si el único “sí” es el último, tu problema mayor es de proceso, no de esquema.
Una regla práctica
Usa JSONB cuando la forma de los datos aún no está clara, pero puedas nombrar un pequeño conjunto de campos estables que siempre necesitarás (como id, owner, status, created_at). El momento en que la gente depende de filtros consistentes, exportaciones fiables o validación estricta, el coste de la “flexibilidad” sube rápido.
Ejemplo: de un formulario flexible a un sistema de operaciones fiable
Imagina un formulario de ingreso de soporte que cambia semanalmente. Una semana añades “modelo de dispositivo”, la siguiente añades “motivo de reembolso”, luego renombras “priority” a “urgency”. Al principio, guardar el payload del formulario en una sola columna JSONB es perfecto: puedes lanzar cambios sin migración y nadie se queja.
Tres meses después, los gerentes quieren filtros como “urgency = high y device model empieza con iPhone”, SLAs basadas en el nivel del cliente y un informe semanal que deba coincidir con el de la semana pasada.
El modo de fallo es predecible: alguien pregunta “¿Dónde quedó este campo?” Los registros antiguos usan un nombre de clave distinto, el tipo de valor cambió ("3" vs 3) o el campo nunca existió en la mitad de los tickets. Los reportes se convierten en un parche de casos especiales.
Un punto intermedio práctico es un diseño híbrido: conserva campos estables y críticos para el negocio como columnas reales (created_at, customer_id, status, urgency, sla_due_at) y deja un área JSONB para campos nuevos o raros que siguen cambiando.
Una cronología de baja interrupción que funciona bien:
- Semana 1: Elige 5 a 10 campos que deben ser filtrables y reportables. Añade columnas.
- Semana 2: Backfill de esas columnas desde JSONB empezando por registros recientes, luego los más antiguos.
- Semana 3: Actualiza escrituras para que los registros nuevos llenen columnas y JSONB (doble escritura temporal).
- Semana 4: Cambia lecturas e informes a las columnas. Mantén JSONB solo para extras.
Próximos pasos: decide, documenta y sigue entregando
Si no haces nada, la decisión se toma sola. El prototipo crece, los bordes se endurecen y cada cambio empieza a sentirse arriesgado. Un movimiento mejor es tomar una pequeña decisión escrita ahora y seguir construyendo.
Lista las 5 a 10 preguntas que tu app debe responder rápidamente ("Mostrar todas las órdenes abiertas de este cliente", "Encontrar usuarios por email", "Reportar ingresos por mes"). Junto a cada una, escribe las restricciones que no puedes romper (email único, estado obligatorio, totales válidos). Luego traza una frontera clara: usa JSONB para campos que cambian a menudo y rara vez se filtran o unen, y promueve a columnas y tablas todo aquello que busques, ordenes, juntes o debas validar siempre.
Si usas una plataforma no-code que genera aplicaciones reales, esta división puede ser más fácil de gestionar con el tiempo. Por ejemplo, AppMaster (appmaster.io) te permite modelar tablas PostgreSQL visualmente y regenerar el backend y las apps a medida que cambian los requisitos, lo que hace que los cambios iterativos de esquema y las migraciones planificadas sean menos dolorosos.
FAQ
Usa JSONB cuando la forma cambie con frecuencia y principalmente almacenes y recuperes la carga tal cual, por ejemplo formularios que cambian rápido, webhooks de socios, feature flags o configuraciones por cliente. Mantén un pequeño conjunto de campos estables como columnas normales para poder filtrar y reportar con fiabilidad.
Normaliza cuando los datos se comparten, se consultan de muchas formas o deben ser confiables por defecto. Si necesitas campos obligatorios, valores únicos, claves foráneas o dashboards y exportaciones consistentes, las tablas con columnas claras y restricciones suelen ahorrar tiempo más adelante.
Sí. Un enfoque híbrido suele ser la mejor opción por defecto: pon los campos críticos para el negocio en columnas y relaciones, y deja atributos opcionales o que cambian rápido en una columna JSONB “meta”. Así mantienes estables los reportes y reglas mientras sigues iterando sobre campos de larga cola.
Pregúntate qué deben filtrar, ordenar y exportar los usuarios en la UI y qué debe ser correcto siempre (dinero, estado, pertenencia, permisos, fechas). Si un campo se usa frecuentemente en listas, dashboards o joins, promuévelo a columna real; deja en JSONB los extras raramente usados.
Los mayores riesgos son nombres de clave inconsistentes, tipos de valor mezclados y cambios silenciosos que rompen la analítica. Evítalo usando claves consistentes, manteniendo JSONB pequeño, guardando números/fechas como tipos adecuados (o como números JSON) y añadiendo un campo de versión simple dentro del JSON.
Puede ser seguro, pero requiere trabajo extra. JSONB no impone estructura por defecto, así que necesitarás validaciones explícitas, indexación cuidadosa de las rutas que consultas y convenciones estrictas. Los esquemas normalizados suelen hacer estas garantías más simples y visibles.
Indexa solo lo que realmente consultas. Usa índices btree normales para columnas comunes como status y timestamps; para JSONB, prefiere índices de expresión sobre claves calientes (por ejemplo extraer un solo campo) en lugar de indexar todo el documento, a menos que realmente busques por muchas claves.
Busca consultas lentas y desordenadas, escaneos completos frecuentes y un creciente conjunto de scripts puntuales solo para responder preguntas simples. Otros indicadores son varios equipos escribiendo las mismas claves JSON de forma distinta y una necesidad creciente de restricciones estrictas o exportaciones estables.
Diseña primero las tablas destino y luego ejecútalas en paralelo con los datos JSONB. Backfill en lotes, valida los resultados, cambia las lecturas a las nuevas tablas, luego las escrituras, y finalmente aplica restricciones para que no entre mala data de nuevo.
Modela tus entidades centrales (clientes, órdenes, tickets) como tablas con columnas claras para los campos que las personas filtran y reportan, y añade una columna JSONB para extras flexibles. Herramientas como AppMaster pueden ayudar porque te permiten actualizar el modelo PostgreSQL visualmente y regenerar el backend y las apps cuando cambian los requisitos.


