JSON y JSONB en PostgreSQL
Una tienda vende notebooks, mouses y cables. Cada producto tiene atributos distintos: unos tienen RAM, otros colores, otros nada. ¿Una columna por cada atributo posible? ¿Una tabla por tipo de producto? PostgreSQL ofrece una tercera vía: guardar un documento JSON dentro de una columna y consultarlo con SQL. Es, además, el mejor ensayo para entender MongoDB.
Bienvenido
Hasta ahora, cada dato de tus tablas tenía una columna con nombre y tipo fijos. Eso da orden y garantías, pero cuesta cuando los datos no tienen la misma forma en todas las filas. Con JSONB sigues dentro de PostgreSQL, con sus transacciones, sus índices y sus JOINs, pero una columna puede guardar un documento entero.
Al finalizar esta guía sabrás elegir entre json y jsonb, guardar y leer documentos con ->, ->> y #>>, filtrar con @> y ?, modificar partes de un documento con jsonb_set, convertir entre JSON y filas, acelerar consultas con índices GIN y decidir cuándo conviene JSONB y cuándo columnas normales.
Todos los ejemplos usan una tabla productos de una tienda de tecnología. Cada fila tiene columnas fijas (id, nombre, precio) y una columna atributos de tipo JSONB con lo que cambia de un producto a otro. Todos los resultados que verás fueron ejecutados en PostgreSQL real antes de publicarse.
Encontrarás actividades cortas y autocorregibles repartidas por la guía y una sección final de escenarios de fallas reales donde tienes que decidir qué pasó antes de ver la respuesta. La práctica con editor SQL en vivo es el taller que sigue a esta guía.
🧩 Flexibilidad
Cada fila puede traer atributos distintos sin tocar la estructura de la tabla.
🔎 Consulta real
No es un texto opaco: puedes filtrar, ordenar y agrupar por lo que hay adentro del documento.
🌉 Puente
Los mismos conceptos (documento, clave, arreglo anidado) son los de MongoDB.
¿Por qué guardar JSON en una base relacional?
Imagina el catálogo de la tienda. Un notebook tiene RAM, almacenamiento y puertos; unos auriculares tienen autonomía y bluetooth; un cable HDMI casi no tiene atributos. El modelo relacional clásico ofrece tres salidas, y las tres duelen:
| Enfoque | Cómo se ve | El problema |
|---|---|---|
| Una columna por atributo | Tabla con ram_gb, bluetooth, autonomia_h, pulgadas… | Decenas de columnas casi todas en NULL. Cada producto nuevo con un atributo inédito exige un ALTER TABLE. |
| Una tabla por tipo de producto | notebooks, auriculares, mouses… | Listar "todos los productos" obliga a un UNION de tablas, y cada categoría nueva es una tabla nueva. |
| Clave-valor (EAV) | Tabla producto_atributo(producto_id, clave, valor) | Todo se guarda como texto, y recuperar un producto completo requiere un JOIN o un pivot por cada atributo. |
| Columna JSONB | Una columna atributos con el documento de cada producto | Pierdes algunas garantías del modelo relacional (lo vemos en la sección ¿JSONB o columnas?). |
La idea no es reemplazar columnas por JSON. Es un modelo híbrido: lo que todos los productos comparten y que usas para unir, ordenar y validar (id, nombre, precio) va en columnas normales; lo que varía de fila a fila va en un documento.
Un catálogo tiene 40 categorías de producto y cada una trae entre 3 y 12 atributos propios que cambian cada mes. ¿Qué modelo se adapta mejor a ese ritmo de cambio?
json vs jsonb
PostgreSQL ofrece dos tipos para documentos JSON, y la diferencia importa: json guarda el texto tal como lo escribiste; jsonb lo convierte a un formato binario ya interpretado.
SELECT '{"a":1, "b":2, "a":3}'::json AS como_json,
'{"a":1, "b":2, "a":3}'::jsonb AS como_jsonb;
como_json | como_jsonb
--------------------------+------------------
{"a":1, "b":2, "a":3} | {"a": 3, "b": 2}
El json conservó los espacios y la clave a repetida. El jsonb descartó el espacio, quedó con una sola a (se queda el último valor) y ordenó las claves. Otro ejemplo del orden:
SELECT '{"nombre":"Ana","id":1,"activo":true}'::jsonb;
jsonb
-------------------------------------------
{"id": 1, "activo": true, "nombre": "Ana"}
Las claves salen primero las más cortas y, a igual largo, por orden alfabético. El orden en que las escribiste no se conserva.
json | jsonb | |
|---|---|---|
| Almacenamiento | Texto idéntico al original | Binario ya interpretado |
| Claves repetidas | Las conserva todas | Se queda con la última |
| Orden y espacios | Se conservan | No se conservan |
| Velocidad al consultar | Lenta: reinterpreta el texto en cada consulta | Rápida: ya está procesado |
Operadores @> y ? | No disponibles | Disponibles |
| Índices GIN | No | Sí |
jsonb
Elige json solo si necesitas guardar el texto exacto que llegó (por ejemplo, la firma de un mensaje que se verifica byte a byte). En cualquier otro caso, jsonb: se consulta más rápido y es el único que admite los operadores de filtrado y los índices de esta guía. De aquí en adelante, todo es jsonb.
Guardas en una columna jsonb el texto {"a":1,"a":3}. ¿Qué almacena PostgreSQL?
Guardar documentos
Una columna jsonb se declara como cualquier otra. Conviene ponerle un valor por defecto y una restricción que asegure que siempre guarda un objeto (y no un número o un arreglo suelto).
CREATE TABLE productos (
id SERIAL PRIMARY KEY,
nombre TEXT NOT NULL,
precio NUMERIC(10,2) NOT NULL,
atributos JSONB NOT NULL DEFAULT '{}',
CONSTRAINT atributos_es_objeto
CHECK (jsonb_typeof(atributos) = 'object')
);
El documento se escribe como un literal de texto entre comillas simples. Las claves y los textos van con comillas dobles dentro del JSON:
INSERT INTO productos (nombre, precio, atributos) VALUES
('Notebook Aurora 14', 5200.00,
'{"marca":"Lenovo","ram_gb":16,
"almacenamiento":{"tipo":"SSD","gb":512},
"puertos":["USB-C","HDMI","USB-A"],
"garantia_meses":12}'),
('Auriculares Vento', 350.00,
'{"marca":"Sony","bluetooth":true,"autonomia_h":30,
"colores":["negro","azul"]}'),
('Mouse Pixel', 120.00,
'{"marca":"Logitech","inalambrico":true,"colores":["negro"]}'),
('Monitor Lumen 27', 2100.00,
'{"marca":"LG","pulgadas":27,"resolucion":"2560x1440",
"puertos":["HDMI","DisplayPort"],"garantia_meses":24}'),
('Cable HDMI 2 m', 45.00, '{}');
Observa lo que acabas de lograr: cinco productos con atributos completamente distintos, en una sola tabla y sin una sola columna sobrante. JSON admite seis tipos de valor, y conviene reconocerlos:
| Tipo JSON | Ejemplo | jsonb_typeof devuelve |
|---|---|---|
| Número | 16 | number |
| Texto | "16" | string |
| Booleano | true | boolean |
| Nulo | null | null |
| Objeto | {"gb": 512} | object |
| Arreglo | ["HDMI","USB-C"] | array |
CHECK sí rechaza lo que no es un objeto
Si intentas insertar '[1,2]' en atributos, PostgreSQL responde new row for relation "productos" violates check constraint "atributos_es_objeto". Es la forma de recuperar un poco de la disciplina que el esquema fijo daba gratis.
Ojo con el tipo: 16 (número) y "16" (texto) son valores distintos para jsonb, y más adelante verás por qué eso importa al filtrar.
¿Qué hace la restricción CHECK (jsonb_typeof(atributos) = 'object') sobre la columna?
Leer: ->, ->> y #>>
Para sacar un valor del documento hay dos operadores básicos, y la diferencia entre ellos es la fuente de la mayoría de los errores de principiantes.
| Operador | Devuelve | Ejemplo |
|---|---|---|
-> | El valor como jsonb (todavía JSON) | atributos->'marca' |
->> | El valor como text (SQL normal) | atributos->>'marca' |
#> / #>> | Lo mismo, pero siguiendo una ruta de varios niveles | atributos#>>'{almacenamiento,tipo}' |
SELECT nombre,
atributos->'marca' AS con_flecha,
atributos->>'marca' AS con_doble_flecha
FROM productos ORDER BY id;
nombre | con_flecha | con_doble_flecha
--------------------+------------+------------------
Notebook Aurora 14 | "Lenovo" | Lenovo
Auriculares Vento | "Sony" | Sony
Mouse Pixel | "Logitech" | Logitech
Monitor Lumen 27 | "LG" | LG
Cable HDMI 2 m | |
Con -> el resultado conserva las comillas porque sigue siendo JSON (un texto JSON). Con ->> obtienes el texto limpio. El Cable HDMI no tiene marca: pedir una clave que no existe no es un error, devuelve NULL.
Documentos anidados y arreglos
Los operadores se encadenan para bajar por los niveles. Para un arreglo, el "nombre" es la posición, empezando en 0:
SELECT nombre,
atributos->'almacenamiento'->>'tipo' AS tipo,
atributos#>>'{almacenamiento,gb}' AS gb,
atributos->'puertos'->>0 AS primer_puerto,
jsonb_array_length(atributos->'puertos') AS cant_puertos
FROM productos
WHERE atributos ? 'almacenamiento';
nombre | tipo | gb | primer_puerto | cant_puertos
--------------------+------+-----+---------------+--------------
Notebook Aurora 14 | SSD | 512 | USB-C | 3
->> siempre devuelve text, aunque el valor sea un número. Si comparas sin convertir, PostgreSQL compara letra por letra, y '8' resulta "mayor o igual" que '16' porque '8' viene después de '1'. Para comparar como número hay que convertir con ::int (o ::numeric):
-- Mal: compara texto contra texto
SELECT nombre FROM productos WHERE atributos->>'ram_gb' >= '16';
-- Bien: convierte a número antes de comparar
SELECT nombre FROM productos WHERE (atributos->>'ram_gb')::int >= 16;
Con los datos de esta tabla ambas devuelven el Notebook (el único con RAM), así que el error pasa desapercibido. Pero un equipo de 8 GB también habría entrado en la primera:
SELECT v, v >= '16' AS como_texto,
v::int >= 16 AS como_numero
FROM (VALUES ('8'), ('16'), ('32')) t(v);
v | como_texto | como_numero
----+------------+-------------
8 | t | f
16 | t | t
32 | t | t
Clasifica el tipo de dato que devuelve cada expresión sobre atributos.
atributos->'puertos'
atributos->>'marca'
atributos ? 'bluetooth'
atributos#>>'{almacenamiento,tipo}'
Quieres los productos con al menos 16 GB de RAM y escribes WHERE atributos->>'ram_gb' >= 16. ¿Qué ocurre?
Filtrar: @>, ? y compañía
Podrías filtrar con WHERE atributos->>'marca' = 'Lenovo', y funciona. Pero jsonb trae operadores propios, más expresivos, que además se pueden acelerar con índices (próxima sección). Los dos que más vas a usar:
| Operador | Pregunta que responde |
|---|---|
@> (contiene) | ¿El documento de la izquierda contiene este fragmento de documento? |
? (existe clave) | ¿Existe esta clave en el primer nivel del documento? |
?| | ¿Existe al menos una de estas claves? |
?& | ¿Existen todas estas claves? |
Contiene: @>
Escribes el fragmento que esperas encontrar y PostgreSQL verifica que esté dentro. Funciona con valores simples, con objetos anidados y con arreglos:
-- Un valor simple
SELECT nombre FROM productos WHERE atributos @> '{"marca":"Lenovo"}';
-- Dentro de un objeto anidado
SELECT nombre FROM productos
WHERE atributos @> '{"almacenamiento":{"tipo":"SSD"}}';
-- Dentro de un arreglo: ¿tiene un puerto HDMI?
SELECT nombre FROM productos
WHERE atributos @> '{"puertos":["HDMI"]}' ORDER BY id;
-- 1.º y 2.º SELECT: Notebook Aurora 14
-- 3.º SELECT:
nombre
--------------------
Notebook Aurora 14
Monitor Lumen 27
Fíjate en la tercera consulta: para buscar un elemento dentro de un arreglo no recorres el arreglo, simplemente describes cómo debería verse el documento. Es el mismo estilo de pensamiento que usarás en MongoDB.
Existe la clave: ?, ?| y ?&
-- ¿Tiene garantía registrada?
SELECT nombre FROM productos WHERE atributos ? 'garantia_meses' ORDER BY id;
-- ¿Es bluetooth O inalámbrico? (al menos una de las dos claves)
SELECT nombre FROM productos
WHERE atributos ?| ARRAY['bluetooth','inalambrico'] ORDER BY id;
-- ¿Tiene marca Y garantía? (las dos claves)
SELECT nombre FROM productos
WHERE atributos ?& ARRAY['marca','garantia_meses'] ORDER BY id;
-- 1.º y 3.º SELECT: -- 2.º SELECT:
Notebook Aurora 14 Auriculares Vento
Monitor Lumen 27 Mouse Pixel
? y @>
1. ? solo mira las claves del primer nivel: '{"a":{"b":1}}'::jsonb ? 'b' es false, porque b está un nivel más abajo. 2. @> distingue tipos: atributos @> '{"ram_gb":"16"}' (con el 16 entre comillas, como texto) no encuentra al Notebook, cuyo ram_gb es el número 16, sin comillas. No lanza error: simplemente devuelve cero filas.
Une cada expresión con lo que responde.
Ejecutas WHERE atributos @> '{"ram_gb":"16"}' sobre la tabla del Notebook (cuyo documento tiene "ram_gb": 16, sin comillas) y obtienes 0 filas, sin ningún error. ¿Por qué?
Modificar sin reescribir todo
Un jsonb no se edita "en el lugar": las funciones y operadores de esta sección devuelven un documento nuevo, y tú lo asignas con UPDATE ... SET atributos = ....
| Quiero… | Uso |
|---|---|
| Cambiar o agregar una clave en una ruta | jsonb_set(doc, '{ruta}', nuevo_valor) |
| Combinar con otro documento (agrega y pisa claves) | doc || '{"clave": valor}' |
| Quitar una clave del primer nivel | doc - 'clave' |
| Quitar una clave de una ruta anidada | doc #- '{ruta,clave}' |
jsonb_set: cambiar un valor
-- Subir la RAM del notebook a 32 GB
UPDATE productos
SET atributos = jsonb_set(atributos, '{ram_gb}', '32')
WHERE nombre = 'Notebook Aurora 14';
-- Cambiar un valor anidado: ruta de dos niveles
UPDATE productos
SET atributos = jsonb_set(atributos, '{almacenamiento,gb}', '1024')
WHERE nombre = 'Notebook Aurora 14';
El tercer argumento debe ser jsonb. Un número como '32' se interpreta bien, pero para un texto hay que incluir las comillas dobles dentro de las simples: '"SSD"'. Si escribes 'SSD' a secas, PostgreSQL responde invalid input syntax for type json. Para valores que vienen de una columna o parámetro, usa to_jsonb(valor) y se encarga de las comillas.
|| y -: agregar y quitar
-- Agregar una clave nueva Y pisar una existente en un solo paso
UPDATE productos
SET atributos = atributos || '{"color":"gris","garantia_meses":18}'
WHERE nombre = 'Notebook Aurora 14';
-- Quitar la clave color
UPDATE productos
SET atributos = atributos - 'color'
WHERE nombre = 'Notebook Aurora 14';
-- Quitar la clave tipo, dentro de almacenamiento
UPDATE productos
SET atributos = atributos #- '{almacenamiento,tipo}'
WHERE nombre = 'Notebook Aurora 14';
Con ||, la clave garantia_meses ya existía con valor 12 y quedó en 18: a igual clave, gana el documento de la derecha.
1. No crea rutas intermedias. jsonb_set(atributos, '{wifi,version}', '6') sobre el Mouse no hace nada y no avisa: como wifi no existe, no hay dónde poner version. Primero hay que crear wifi como objeto.
2. NULL borra todo. Si el nuevo valor es un NULL de SQL, jsonb_set devuelve NULL y toda la columna queda en NULL (aquí, el NOT NULL lo rechaza con un error). Para guardar un nulo JSON usa 'null'::jsonb.
3. Reescribe el documento completo. Cambiar una sola clave hace que PostgreSQL escriba de nuevo todo el valor jsonb de esa fila. En documentos pequeños no se nota; en documentos de cientos de KB, sí.
Agregar a un arreglo
Para sumar un elemento al arreglo colores del Mouse, concatenas con || y guardas el arreglo nuevo con jsonb_set:
UPDATE productos
SET atributos = jsonb_set(atributos, '{colores}',
(atributos->'colores') || '"rojo"')
WHERE nombre = 'Mouse Pixel';
["negro", "rojo"]
Predice el efecto de cada instrucción sobre el documento {"marca":"LG","pulgadas":27}:
1. atributos || '{"pulgadas":32,"curvo":true}' deja
.
2. atributos - 'marca' devuelve el documento
.
3. jsonb_set(atributos, '{garantia,meses}', '12'), siendo garantia inexistente,
.
Necesitas guardar el texto Samsung en la clave marca. ¿Cuál es la instrucción correcta?
De JSON a filas y de filas a JSON
El puente entre los dos mundos se cruza en ambos sentidos. Para desarmar un documento en filas se usan funciones que "expanden", y para armar uno desde filas, funciones que "construyen".
Desarmar: jsonb_each_text y jsonb_array_elements_text
-- Cada clave del documento se vuelve una fila
SELECT k AS clave, v AS valor
FROM productos, jsonb_each_text(atributos) AS e(k, v)
WHERE nombre = 'Auriculares Vento'
ORDER BY k;
clave | valor
-------------+----------------
autonomia_h | 30
bluetooth | true
colores | ["negro", "azul"]
marca | Sony
Y para volver filas los elementos de un arreglo, jsonb_array_elements_text. Es lo que permite contar y agrupar contenido que vive adentro de un documento, con el GROUP BY de siempre:
-- ¿Qué puertos aparecen más en el catálogo?
SELECT p AS puerto, count(*) AS productos
FROM productos, jsonb_array_elements_text(atributos->'puertos') AS p
GROUP BY p
ORDER BY productos DESC, puerto;
puerto | productos
-------------+-----------
HDMI | 2
DisplayPort | 1
USB-A | 1
USB-C | 1
Los productos sin puertos simplemente no aportan filas. También puedes agrupar por un valor del documento, como la marca: GROUP BY atributos->>'marca'. Los productos sin esa clave quedan juntos en un grupo cuyo valor es NULL.
Armar: jsonb_build_object, jsonb_agg y to_jsonb
-- Armar un objeto a mano, clave por clave
SELECT jsonb_build_object('id', id, 'nombre', nombre, 'precio', precio)
FROM productos WHERE id = 3;
-- Juntar varias filas en un solo arreglo
SELECT jsonb_agg(nombre ORDER BY id)
FROM productos WHERE atributos ? 'garantia_meses';
-- Convertir una fila completa en un objeto
SELECT to_jsonb(p)
FROM (SELECT id, nombre, precio FROM productos WHERE id = 2) p;
{"id": 3, "nombre": "Mouse Pixel", "precio": 120.00}
["Notebook Aurora 14", "Monitor Lumen 27"]
{"id": 2, "nombre": "Auriculares Vento", "precio": 350.00}
Para que la propia base de datos entregue a tu aplicación la respuesta lista para una API: un pedido con sus líneas anidadas, un producto con sus atributos. Es exactamente la forma en que MongoDB devuelve sus documentos.
Si necesitas leer un documento largo a simple vista, jsonb_pretty(atributos) lo devuelve con sangrías:
{
"marca": "LG",
"puertos": [
"HDMI",
"DisplayPort"
],
"pulgadas": 27,
"resolucion": "2560x1440",
"garantia_meses": 24
}
Quieres saber cuántos productos incluyen cada color, sabiendo que colores es un arreglo dentro de atributos. ¿Qué enfoque lo resuelve?
Índices GIN: que no recorra todo el catálogo
Sin ayuda, una consulta sobre el contenido del documento recorre toda la tabla abriendo cada documento. Con miles de productos eso se nota. Para jsonb existe un tipo de índice hecho a medida: GIN.
CREATE INDEX idx_productos_atributos
ON productos USING GIN (atributos);
Un índice GIN indexa todas las claves y valores del documento. Sirve para acelerar @>, ?, ?| y ?&. Para verlo con EXPLAIN, como la tabla de ejemplo tiene solo cinco filas y el planificador preferiría recorrerla entera, le pedimos que no lo haga:
SET enable_seqscan = off; -- solo para esta demostración
EXPLAIN SELECT nombre FROM productos
WHERE atributos @> '{"marca":"Lenovo"}';
Bitmap Heap Scan on productos
Recheck Cond: (atributos @> '{"marca": "Lenovo"}'::jsonb)
-> Bitmap Index Scan on idx_productos_atributos
Index Cond: (atributos @> '{"marca": "Lenovo"}'::jsonb)
Bitmap Index Scan on idx_productos_atributos confirma que el índice se usó. Pero ese índice no ayuda a esta otra forma de preguntar lo mismo:
EXPLAIN SELECT nombre FROM productos
WHERE atributos->>'marca' = 'Lenovo';
Seq Scan on productos
Filter: ((atributos ->> 'marca'::text) = 'Lenovo'::text)
El GIN no entiende la expresión atributos->>'marca'. Si ese filtro es el que más usas, crea un índice sobre la expresión (un B-tree normal, el de siempre):
CREATE INDEX idx_productos_marca
ON productos ((atributos->>'marca')); -- los paréntesis dobles son obligatorios
EXPLAIN SELECT nombre FROM productos
WHERE atributos->>'marca' = 'Lenovo';
Index Scan using idx_productos_marca on productos
Index Cond: ((atributos ->> 'marca'::text) = 'Lenovo'::text)
| Tipo de consulta | Índice que ayuda |
|---|---|
Buscar por contenido o por existencia de claves (@>, ?) | GIN sobre toda la columna |
Igualdad, rango u orden sobre una clave concreta (->>) | Índice de expresión sobre esa clave |
No adivines: mide con EXPLAIN como hiciste en la guía de índices. Y recuerda el costo de todo índice: acelera las lecturas pero hace más lentas las escrituras. Un GIN completo sobre documentos grandes es relativamente pesado; si solo consultas dos claves, el índice de expresión es más barato.
Creaste un índice GIN sobre atributos. ¿Cuál de estas consultas puede aprovecharlo?
¿JSONB o columnas?
JSONB es una herramienta poderosa y por eso es fácil abusar de ella. Un esquema que termina siendo "id + una columna JSONB con todo" tiró por la borda lo que PostgreSQL hace mejor. Estas son las garantías que pierdes al meter un dato dentro del documento:
| Con una columna | Dentro de un documento JSONB |
|---|---|
Tipo estricto: un INTEGER no acepta texto | Cualquier clave acepta cualquier tipo; 16 y "16" conviven |
NOT NULL, UNIQUE, CHECK y DEFAULT por columna | Solo se pueden simular con CHECK sobre expresiones, más incómodos |
| Clave foránea hacia otra tabla | Imposible: un id dentro del documento no se verifica contra nada |
| Estadísticas del planificador por columna | El planificador sabe poco sobre lo que hay dentro, y puede elegir peores planes |
| Cambiar un valor reescribe esa columna | Cambiar una clave reescribe el documento completo |
Usa columnas para lo que: se usa en JOINs, es clave foránea, debe ser único, es obligatorio o se consulta en casi todas las consultas. Usa JSONB para lo que: varía de fila en fila, no tiene estructura estable, o es una respuesta externa que guardas casi como llegó (por ejemplo, la respuesta de una pasarela de pagos). Y si una clave del documento empieza a usarse en todos los filtros, es señal de que ya merece ser su propia columna.
Decide dónde guardarías cada dato en la tienda: ¿columna normal o documento JSONB?
El puente hacia MongoDB
Lo que acabas de aprender es, casi tal cual, la forma de pensar de MongoDB: la unidad de datos es un documento con claves, valores, objetos anidados y arreglos. La diferencia es que en Mongo todo es documento, y en PostgreSQL el documento convive con tablas, claves foráneas y transacciones. Lo que viene en las próximas guías va a resultarte familiar:
| Idea | PostgreSQL (jsonb) | MongoDB |
|---|---|---|
| Unidad de datos | Una fila con una columna jsonb | Un documento en una colección |
| Filtrar por valor | WHERE atributos @> '{"marca":"Lenovo"}' | find({ marca: "Lenovo" }) |
| Campo anidado | atributos#>>'{almacenamiento,tipo}' | find({ "almacenamiento.tipo": "SSD" }) |
| La clave existe | WHERE atributos ? 'garantia_meses' | find({ garantia_meses: { $exists: true } }) |
| Cambiar un campo | jsonb_set(atributos, '{ram_gb}', '32') | updateOne(filtro, { $set: { ram_gb: 32 } }) |
| Quitar un campo | atributos - 'color' | updateOne(filtro, { $unset: { color: "" } }) |
| Agregar a un arreglo | jsonb_set(..., '{colores}', ... || '"rojo"') | updateOne(filtro, { $push: { colores: "rojo" } }) |
| Acelerar una búsqueda | CREATE INDEX ... ON productos ((atributos->>'marca')) | createIndex({ marca: 1 }) |
En PostgreSQL, un pedido y su cliente se relacionan con una clave foránea y se modifican en una misma transacción. En MongoDB, el modelo natural es guardar el pedido con sus líneas dentro de un único documento. Elegir entre embeber datos o referenciarlos es el tema central de la guía de modelado en Mongo. Y la decisión de qué dato vive en qué motor es la del proyecto integrador.
¿Qué consulta de PostgreSQL con jsonb se parece más a db.productos.find({ "almacenamiento.tipo": "SSD" }) de MongoDB?
Cuando algo falla
Estos son los errores y síntomas más frecuentes al empezar con JSONB. Reconocerlos por su mensaje te ahorra tiempo.
| Mensaje o síntoma | Causa habitual |
|---|---|
invalid input syntax for type json | El texto no es JSON válido: comillas simples en vez de dobles, coma sobrante al final, o un texto suelto sin comillas ('Samsung' en lugar de '"Samsung"'). |
operator does not exist: jsonb >= integer (o text >= integer) | Comparaste directamente el resultado de -> o ->> con un número. Convierte con ::int o ::numeric. |
invalid input syntax for type integer: "Lenovo" | Hiciste ::int sobre una clave que en alguna fila guarda un texto. Un campo sin tipo fijo puede traer cualquier cosa. |
La consulta con -> y un texto devuelve cero filas | -> entrega JSON, y un texto JSON lleva comillas: se compara con '"Lenovo"'. Usa ->> para comparar con 'Lenovo'. |
@> devuelve cero filas y no hay error | El tipo no coincide ("16" texto vs. 16 número), o la clave está en otro nivel. |
El UPDATE con jsonb_set no cambió nada | La ruta intermedia no existe: jsonb_set no la crea. |
null value in column "atributos" violates not-null constraint tras un jsonb_set | Pasaste un NULL de SQL como nuevo valor. Con jsonb_set, eso anula todo el documento. |
Ejecutas WHERE atributos->'marca' = 'Lenovo' y PostgreSQL responde invalid input syntax for type json. ¿Por qué?
Una consulta filtra con (atributos->>'precio_lista')::numeric > 100 y falla con invalid input syntax for type numeric en una sola fila de miles. ¿Qué es lo más probable?
Con la tabla productos del caso, predice qué devuelve cada consulta:
1. WHERE atributos ? 'ram_gb' sobre el Notebook:
.
2. WHERE atributos ? 'tipo' sobre el Notebook, que tiene "almacenamiento": {"tipo": "SSD"}:
.
3. atributos->>'marca' sobre el Cable HDMI, cuyo documento es {}:
.
Actividades de repaso
Un repaso integrador de todo el módulo. Cada actividad se corrige al instante; tu progreso se guarda automáticamente en este navegador.
¿Qué diferencia hay entre los operadores -> y ->>?
Al ejecutar jsonb_set(atributos, '{wifi,version}', '6') sobre un producto que no tiene la clave wifi, PostgreSQL la crea automáticamente con version adentro.
Quieres agregar "color": "gris" a un producto sin perder ninguna de sus otras claves. ¿Qué SET usas?
¿Cuál de estas es una razón en contra de guardar un dato dentro de una columna JSONB?
¿Qué hace UPDATE productos SET atributos = atributos #- '{almacenamiento,tipo}'?
Cheat Sheet
| Expresión | Para qué sirve |
|---|---|
col->'k' · col->>'k' | Valor de la clave k como jsonb · como text. |
col#>'{a,b}' · col#>>'{a,b}' | Lo mismo, siguiendo una ruta de varios niveles. |
col->'arr'->>0 | Elemento de un arreglo (la posición empieza en 0). |
(col->>'k')::int | Convertir a número antes de comparar u ordenar. |
col @> '{"k":"v"}' | ¿El documento contiene este fragmento? Respeta el tipo. |
col ? 'k' · ?| · ?& | ¿Existe la clave? · ¿alguna de varias? · ¿todas? (solo primer nivel). |
jsonb_set(col, '{ruta}', '"v"') | Cambiar un valor; no crea rutas intermedias. |
col || '{...}' | Agregar claves o pisar las existentes. |
col - 'k' · col #- '{a,b}' | Quitar una clave de primer nivel · de una ruta. |
jsonb_each_text · jsonb_array_elements_text | Desarmar un documento o un arreglo en filas. |
jsonb_build_object · jsonb_agg · to_jsonb | Armar objetos, arreglos y documentos desde filas. |
jsonb_typeof · jsonb_array_length · jsonb_pretty | Tipo de un valor · largo de un arreglo · formato legible. |
USING GIN (col) | Índice para @>, ?, ?|, ?&. |
((col->>'k')) | Índice de expresión para filtrar por una clave concreta. |
¿Sabías que...?
📜 Un formato nacido en la web
JSON viene de JavaScript Object Notation y se popularizó a mediados de los 2000 como alternativa más ligera que XML para que los navegadores y los servidores intercambiaran datos.
🐘 Dos tipos, dos épocas
PostgreSQL agregó el tipo json en 2012 y el tipo jsonb, con índices, en 2014. Desde entonces muchos equipos cubren con él gran parte de lo que antes los empujaba a una base de documentos.
🧱 Documento binario, no texto
La "b" de jsonb significa binario: el texto se interpreta una sola vez al guardarlo, y por eso no conserva espacios, el orden de las claves ni las claves repetidas.