DanLevy.net

JSONB: La Mejor Forma de Arruinar Tu Base de Datos

JSONB es poderoso, útil y muy fácil de mal usar cuando dejas que un blob se convierta en tu esquema real.

PostgreSQL añadió JSONB para permitirte almacenar datos semiestructurados sin definir esquemas rígidos desde el principio. La idea era sólida: a veces realmente no sabes cómo serán los datos, o cambian con demasiada frecuencia para que las columnas tradicionales tengan sentido.

Eso importa porque JSONB no es un error. En muchos sistemas es la representación más limpia del espacio del problema. Si estás almacenando payloads de webhooks de terceros, cuerpos de eventos versionados, feature flags u objetos de configuración de LLM donde cada proveedor y modelo expone un conjunto de opciones ligeramente diferente y en constante cambio, forzar todo en columnas de primera clase puede ser más incómodo que útil.

El problema es que JSONB también es la forma más fácil de posponer decisiones de esquema sin admitir que las estás posponiendo. En algún punto entre la intención y la implementación, se convirtió en el equivalente a base de datos de “luego ordeno mi habitación”. Esa solución temporal que usaste hace seis meses? Sigue ahí, y ahora producción depende de ella.

Sigo viendo el mismo patrón. Un equipo añade una columna JSONB porque no está seguro de los requisitos. Se prometen que la normalizarán una vez que las cosas se estabilicen. Tres años después, esa columna contiene cuarenta versiones diferentes de lo que se suponía que era un perfil de usuario, consultada por quince servicios que cada uno hace suposiciones diferentes sobre lo que hay dentro.

La deuda técnica no es el JSONB en sí. Es la brecha entre lo que te dijiste que estabas construyendo y lo que realmente construiste: un sistema de esquema-en-lectura no documentado.

Lo Que Suele Pasar

Estás añadiendo una funcionalidad y no estás seguro de si los usuarios necesitan un twitter_handle o un bluesky_handle o algo más completamente. En lugar de pensar bien el esquema, haces esto:

CREATE TABLE users (
id SERIAL PRIMARY KEY,
profile JSONB
);

Funciona. Lanzas la funcionalidad, pasas a la siguiente, y luego a la siguiente. La columna JSONB crece silenciosamente en segundo plano.

Esta es la bifurcación en el camino. Si profile se mantiene como un bloque opaco que se obtiene por user.id, probablemente estés bien. Si empieza a convertirse en el lugar principal donde viven los datos del negocio, los tradeoffs cambian rápido.

Producto pregunta: “¿Cuántos usuarios hay en Nueva York?”

Escribes:

SELECT count(*) FROM users WHERE profile->>'location' = 'New York';

Postgres realiza un escaneo completo de la tabla. Cada una de las filas.

Así que añades un índice GIN. Quizás eso siga siendo aceptable. A veces lo es. Pero ahora estás pagando un coste real de complejidad y almacenamiento porque un campo que se comporta como dato relacional de primera clase nunca se convirtió en una columna de primera clase.

Año 1: Deriva de Esquema

Tienes tres versiones de datos en la misma columna.

Tu código de aplicación ahora se ve así:

const city = user.location || user.city || user.address?.city || "Unknown";

No eliminaste el esquema. Solo moviste la validación y las comprobaciones de consistencia desde la base de datos hacia código de aplicación disperso.


Cuándo Usar JSONB de Verdad

JSONB tiene casos de uso válidos. Muchas veces es perfectamente aceptable, y a veces es la mejor opción disponible.

La distinción crítica no es “estructurado bueno, JSON malo”. Se acerca más a esto:

Casos de Uso Legítimos de JSONB

  1. Payloads de Webhooks: Recibes datos de Stripe, Slack o GitHub. No tienes control sobre el esquema. Puede que nunca lo consultes. Solo necesitas almacenarlo para depuración o replay. Perfecto para JSONB.

  2. Logs y Flujos de Eventos: Logs de aplicación, trails de auditoría, contextos de error. Son escritura intensiva, rara vez consultados por campos específicos, y a menudo analizados en bloque o exportados a plataformas de analítica. JSONB está bien aquí.

  3. Preferencias y Configuraciones de Usuario: Objetos de configuración donde tienes 100+ flags booleanos, la mayoría son falsos, y siempre estás obteniendo el bloque completo por ID de usuario. No estás ejecutando WHERE preferences->>'theme' = 'dark'. JSONB funciona.

  4. Configuración de Proveedor/Modelo de LLM: Este es uno de los ejemplos modernos más claros. OpenAI, Anthropic, Gemini, modelos locales de peso abierto y gateways específicos de proveedor exponen parámetros superpuestos pero diferentes. Incluso dentro de un mismo proveedor, las capacidades del modelo y los nombres de opciones evolucionan. Un blob de configuración JSONB es a menudo mucho más honesto que fingir que temperature, top_p, reasoning_effort, json_schema, tool_choice y otras veinte perillas deberían ser columnas universales. JSONB es a menudo la abstracción correcta aquí.

  5. Caché de Respuestas de API: Estás cacheando respuestas completas de API. La base de datos es solo un Redis más rápido. Obtienes por clave de caché, nunca por propiedades anidadas. JSONB es apropiado.

  6. Event Sourcing: Almacenas payloads de eventos inmutables. Tus consultas son siempre “dame todos los eventos para el agregado X” ordenados por tiempo. Nunca ejecutas cláusulas WHERE sobre propiedades del evento. JSONB encaja.

  7. Superficies de Extensibilidad: Integraciones, configuraciones de plugins, overrides por tenant, metadatos de marketplace, capacidades de proveedor, o campos “extras” donde explícitamente esperas que la forma varíe por subtipo. JSONB puede ser el contrato correcto, no un compromiso.

Regla general: si la aplicación obtiene el documento por una clave conocida y entiende cómo validarlo/versionarlo, JSONB puede ser excelente. Si el negocio sigue haciendo preguntas relacionales sobre claves anidadas, esos campos están intentando convertirse en columnas.

El Mejor Patrón Es a Menudo Híbrido

Muchos sistemas maduros terminan aquí:

CREATE TABLE llm_requests (
id UUID PRIMARY KEY,
provider TEXT NOT NULL,
model TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
config JSONB NOT NULL
);

Esto suele ser mejor que cualquiera de los extremos.

Eso no es “fallar en normalizar”. Eso es dibujar la línea en el lugar correcto.

A Escala: Versionado de Objetos > Normalización

Aquí es donde se pone interesante. A escala suficientemente grande, la solución “correcta” no es la normalización—es el versionado de objetos.

Si tienes miles de millones de filas y evolución frecuente de esquema, migrar columnas se vuelve costoso. Empresas como Stripe, GitHub y Netflix no normalizan todo. En su lugar:

CREATE TABLE entities (
id UUID PRIMARY KEY,
version INT NOT NULL,
data JSONB NOT NULL
);

Tu aplicación sabe cómo leer version: 1, version: 2, version: 3. Sin migraciones de base de datos para nuevos campos. El código maneja la compatibilidad hacia atrás.

Esta es una decisión arquitectónica, no pereza. Intercambia complejidad de base de datos por complejidad de aplicación. A veces ese es exactamente el trade correcto, especialmente cuando el documento está naturalmente versionado y la app es el intérprete canónico.

El modo de fallo no es “usar JSONB”. El modo de fallo es usar JSONB sin versionado, validación, reglas de promoción, o un límite claro entre datos de documento y datos relacionales.

Las Preguntas Que Realmente Importan

Antes de añadir una columna JSONB, pregúntate:

  1. ¿Consultaremos campos anidados en WHERE, JOIN, GROUP BY, u ORDER BY regularmente?
  2. ¿Controlamos este esquema, o está definido externamente y es volátil?
  3. ¿La forma es intencionalmente heterogénea entre registros?
  4. ¿Tenemos validación y versionado a nivel de aplicación?
  5. ¿Qué campos probablemente se convertirán en dimensiones operacionales más adelante?

Si la respuesta a #1 es “sí, constantemente”, esa es una señal fuerte para columnas.

Si las respuestas a #2 y #3 son “sí”, JSONB probablemente está haciendo trabajo real por ti.


Escapando de la Trampa

Si ya estás en este agujero, deja de cavar.

  1. Auditoría: Ejecuta jsonb_object_keys e inspecciona la deriva real de forma, no la forma que asumes que existe.
  2. Promoción: Identifica los campos que filtras, unes, ordenas o reportas más a menudo. Conviértelos en columnas reales.
  3. Validación: Añade validación a nivel de aplicación o base de datos para lo que quede en JSONB.
  4. Versionado: Si el blob es realmente dato de dominio, versiónalo explícitamente.
  5. Recorte: Elimina claves duplicadas del blob una vez establecidas las columnas promovidas.

No te digas que cada blob debe ser normalizado. Tampoco te digas que un blob con semántica permanente de negocio es “temporal”.

JSONB es genial cuando el documento tiene genuinamente forma de documento. Es peligroso cuando es un esquema relacional con un bigote falso.

Recursos