JSON y JSONB en PostgreSQL

Avanzado
SQL
SQL
Actualizado: 18/04/2026

Diagrama: Sql json jsonb postgresql

JSON vs JSONB

PostgreSQL ofrece dos tipos para datos semiestructurados:

  • JSON: guarda el texto exacto tal como se insertó. Preserva whitespace, duplicados de claves y orden. Bueno si necesitas fidelidad al original (p. ej. logs de API).
  • JSONB: formato binario optimizado. Descarta whitespace, deduplica claves (la última gana), no preserva orden. Indexable con GIN. Acceso mucho más rápido.

Regla: usa JSONB salvo que necesites preservar el texto original. Para el 99% de casos es la opción correcta.

CREATE TABLE eventos (
    id SERIAL PRIMARY KEY,
    datos JSONB NOT NULL,
    creado_en TIMESTAMPTZ DEFAULT NOW()
);

INSERT INTO eventos (datos) VALUES
    ('{"tipo": "login", "usuario": {"id": 42, "nombre": "alan"}, "tags": ["web", "chrome"]}'),
    ('{"tipo": "compra", "usuario": {"id": 42, "nombre": "alan"}, "importe": 99.5, "items": [{"sku": "ABC", "cant": 2}]}');

Operadores básicos

-> devuelve JSON, ->> devuelve texto

-- -> devuelve JSON (manteniendo el tipo)
SELECT datos->'usuario' FROM eventos;
-- {"id": 42, "nombre": "alan"}

-- ->> devuelve texto (string plano)
SELECT datos->>'tipo' FROM eventos;
-- login, compra

-- Encadenados: acceder a campos anidados
SELECT datos->'usuario'->>'nombre' FROM eventos;
-- alan, alan

#> y #>> con path

Alternativa para acceso anidado pasando un path como array:

SELECT datos #> '{usuario,nombre}'  FROM eventos;  -- JSON
SELECT datos #>> '{usuario,nombre}' FROM eventos;  -- texto

@> contenencia

El operador @> (contains) es el más útil para filtrar por contenido:

-- Eventos donde el usuario.id es 42
SELECT * FROM eventos WHERE datos @> '{"usuario": {"id": 42}}';

-- Eventos con tag "web" en el array tags
SELECT * FROM eventos WHERE datos @> '{"tags": ["web"]}';

@> comprueba que el lado derecho es subconjunto del izquierdo. La mayoría de índices GIN están optimizados para este operador.

?, ?|, ?& para claves

-- Eventos que tienen la clave "importe"
SELECT * FROM eventos WHERE datos ? 'importe';

-- Eventos que tienen CUALQUIERA de estas claves (OR)
SELECT * FROM eventos WHERE datos ?| ARRAY['importe', 'tags'];

-- Eventos que tienen TODAS estas claves (AND)
SELECT * FROM eventos WHERE datos ?& ARRAY['tipo', 'usuario'];

Acceso a arrays con índice

-- Primer tag
SELECT datos->'tags'->>0 FROM eventos;

-- Longitud del array
SELECT jsonb_array_length(datos->'tags') FROM eventos;

-- Desensamblar array a filas
SELECT id, tag
FROM eventos, jsonb_array_elements_text(datos->'tags') AS tag;

Indexación con GIN

Sin índice, cualquier consulta que filtre por contenido de JSONB hace un Seq Scan. Con GIN, se vuelven instantáneas.

GIN default (jsonb_ops)

CREATE INDEX idx_eventos_datos ON eventos USING GIN(datos);

-- Ahora estas queries usan el indice:
SELECT * FROM eventos WHERE datos @> '{"usuario": {"id": 42}}';
SELECT * FROM eventos WHERE datos ? 'importe';
SELECT * FROM eventos WHERE datos @> '{"tags": ["web"]}';

Este índice soporta todos los operadores pero ocupa más espacio.

GIN con jsonb_path_ops

Solo soporta @>, pero es más pequeño y más rápido que el default:

CREATE INDEX idx_eventos_datos_path ON eventos USING GIN(datos jsonb_path_ops);

Regla: si solo usas @> (lo más común), jsonb_path_ops es mejor.

Índices sobre campos concretos

Si solo consultas campos específicos:

-- Indice B-tree sobre un campo extraido
CREATE INDEX idx_eventos_usuario_id
ON eventos((datos->'usuario'->>'id'));

SELECT * FROM eventos WHERE datos->'usuario'->>'id' = '42';

Útil cuando el campo es muy selectivo y las consultas no necesitan contenencia general.

Construir JSONB dinámicamente

jsonb_build_object

Equivalente a construir un diccionario:

SELECT jsonb_build_object(
    'id', 1,
    'nombre', 'Alan',
    'roles', jsonb_build_array('admin', 'editor')
);
-- {"id": 1, "nombre": "Alan", "roles": ["admin", "editor"]}

jsonb_agg para agregar filas

Convierte filas en un array JSON:

SELECT cliente_id,
       jsonb_agg(jsonb_build_object('id', id, 'fecha', fecha, 'total', total)) AS pedidos
FROM pedidos
GROUP BY cliente_id;
-- cliente_id | pedidos
-- 42         | [{"id":1,"fecha":"2026-04-01","total":100},{"id":2,...}]

Útil para APIs REST: devolver el cliente con sus pedidos anidados en una sola query.

jsonb_object_agg

Como jsonb_agg pero construye un objeto con clave/valor:

SELECT jsonb_object_agg(codigo, nombre) FROM productos;
-- {"ABC": "Laptop", "DEF": "Tablet", ...}

Modificar JSONB

jsonb_set para actualizar un campo

-- Actualizar datos->'usuario'->'rol' a 'admin'
UPDATE eventos
SET datos = jsonb_set(datos, '{usuario,rol}', '"admin"', true)
WHERE id = 1;

El cuarto parámetro (true) crea el path si no existe.

|| para merge (concatenar)

-- Anadir campos sin sobrescribir
UPDATE eventos
SET datos = datos || '{"verificado": true, "revisado_por": "admin"}'::jsonb
WHERE id = 1;

Si las claves existen, el de la derecha las sobrescribe.

- para eliminar claves

-- Eliminar una clave
UPDATE eventos SET datos = datos - 'campo_temporal';

-- Eliminar varias
UPDATE eventos SET datos = datos - ARRAY['campo1', 'campo2'];

-- Eliminar un path anidado
UPDATE eventos SET datos = datos #- '{usuario,telefono}';

SQL/JSON path (PostgreSQL 12+)

Estándar SQL:2016 para consultas JSON complejas:

-- Encontrar eventos cuyo items contenga uno con cant > 1
SELECT * FROM eventos
WHERE jsonb_path_exists(datos, '$.items[*] ? (@.cant > 1)');

-- Extraer todos los SKUs
SELECT jsonb_path_query_array(datos, '$.items[*].sku') AS skus
FROM eventos WHERE datos->>'tipo' = 'compra';
-- ["ABC"]

Sintaxis compacta y potente, similar a XPath pero para JSON.

Caso real: configuración flexible por cliente

CREATE TABLE configuraciones (
    cliente_id INT PRIMARY KEY,
    config JSONB NOT NULL DEFAULT '{}'
);

-- Cada cliente tiene su config:
INSERT INTO configuraciones VALUES
    (1, '{"tema": "oscuro", "idioma": "es", "notificaciones": {"email": true, "sms": false}}'),
    (2, '{"tema": "claro", "idioma": "en", "features": ["beta"]}');

-- Clientes con notificaciones por email activadas
SELECT cliente_id FROM configuraciones
WHERE config @> '{"notificaciones": {"email": true}}';

-- Actualizar solo un campo
UPDATE configuraciones
SET config = jsonb_set(config, '{tema}', '"claro"')
WHERE cliente_id = 1;

-- Merge con valores por defecto
SELECT cliente_id,
       '{"tema":"claro","idioma":"es","notificaciones":{"email":true,"sms":false}}'::jsonb || config AS config_efectiva
FROM configuraciones;

Rendimiento: cuándo usar JSONB vs columnas

Usa columnas relacionales cuando:

  • El esquema es estable y todos los registros tienen los mismos campos.
  • Las consultas filtran/ordenan frecuentemente por esos campos.
  • Necesitas foreign keys y constraints sobre esos campos.

Usa JSONB cuando:

  • El esquema varía por registro (configuraciones, metadatos).
  • Los campos raramente se filtran (guardas y lees, pero no buscas por ellos).
  • Integras con APIs que ya devuelven JSON.
  • Los datos tienen profundidad arbitraria (arrays anidados).

Un anti-patrón común: usar JSONB para TODO en lugar de columnas reales. Pierdes los beneficios del modelo relacional. La regla: estructura lo estable en columnas, semiestructura en JSONB.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, SQL es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de SQL

Explora más contenido relacionado con SQL y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Diferenciar JSON vs JSONB y elegir el correcto para cada caso. Consultar campos anidados con los operadores ->, ->> y #>. Usar @> y ? para filtrar por contenido dentro del documento. Indexar JSONB con GIN y jsonb_path_ops para acelerar busquedas. Construir documentos con jsonb_build_object, jsonb_build_array y jsonb_agg en agregaciones. Aplicar jsonb_path_query (SQL/JSON path) para extracciones avanzadas.

Cursos que incluyen esta lección

Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje