🗄️ Guía de Aula Invertida
— vistas

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.

RC
Ing. Roy Carrasco
Facultad de Ingeniería de Sistemas · UAB

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.

Objetivo del módulo

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.

📦 El caso de esta guía

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.

📝 Actividades de este módulo

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:

EnfoqueCómo se veEl problema
Una columna por atributoTabla 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 productonotebooks, 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 JSONBUna columna atributos con el documento de cada productoPierdes algunas garantías del modelo relacional (lo vemos en la sección ¿JSONB o columnas?).
No es "o lo uno o lo otro"

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.

Selección múltiple

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.

SQL
SELECT '{"a":1,   "b":2, "a":3}'::json  AS como_json,
       '{"a":1,   "b":2, "a":3}'::jsonb AS como_jsonb;
Resultado
        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:

SQL
SELECT '{"nombre":"Ana","id":1,"activo":true}'::jsonb;
Resultado
                 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.

jsonjsonb
AlmacenamientoTexto idéntico al originalBinario ya interpretado
Claves repetidasLas conserva todasSe queda con la última
Orden y espaciosSe conservanNo se conservan
Velocidad al consultarLenta: reinterpreta el texto en cada consultaRápida: ya está procesado
Operadores @> y ?No disponiblesDisponibles
Índices GINNoSí
Regla práctica: usa 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.

Selección múltiple

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).

SQL
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:

SQL
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 JSONEjemplojsonb_typeof devuelve
Número16number
Texto"16"string
Booleanotrueboolean
Nulonullnull
Objeto{"gb": 512}object
Arreglo["HDMI","USB-C"]array
El 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.

Selección múltiple

¿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.

OperadorDevuelveEjemplo
->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 nivelesatributos#>>'{almacenamiento,tipo}'
SQL
SELECT nombre,
       atributos->'marca'   AS con_flecha,
       atributos->>'marca'  AS con_doble_flecha
FROM productos ORDER BY id;
Resultado
       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:

SQL
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';
Resultado
       nombre       | tipo | gb  | primer_puerto | cant_puertos
--------------------+------+-----+---------------+--------------
 Notebook Aurora 14 | SSD  | 512 | USB-C         |            3
⚠️ Los números salen como texto

->> 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):

SQL
-- 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:

SQL
SELECT v, v >= '16'      AS como_texto,
          v::int >= 16  AS como_numero
FROM (VALUES ('8'), ('16'), ('32')) t(v);
Resultado
 v  | como_texto | como_numero
----+------------+-------------
 8  | t          | f
 16 | t          | t
 32 | t          | t
Clasificación

Clasifica el tipo de dato que devuelve cada expresión sobre atributos.

atributos->'puertos'
atributos->>'marca'
atributos ? 'bluetooth'
atributos#>>'{almacenamiento,tipo}'

Selección múltiple

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:

OperadorPregunta 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:

SQL
-- 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;
Resultado
-- 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 ?&

SQL
-- ¿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;
Resultado
-- 1.º y 3.º SELECT:     -- 2.º SELECT:
 Notebook Aurora 14       Auriculares Vento
 Monitor Lumen 27         Mouse Pixel
Dos trampas de ? 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.

Emparejamiento

Une cada expresión con lo que responde.

Selección múltiple

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 rutajsonb_set(doc, '{ruta}', nuevo_valor)
Combinar con otro documento (agrega y pisa claves)doc || '{"clave": valor}'
Quitar una clave del primer niveldoc - 'clave'
Quitar una clave de una ruta anidadadoc #- '{ruta,clave}'

jsonb_set: cambiar un valor

SQL
-- 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

SQL
-- 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.

⚠️ Tres trampas de jsonb_set

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:

SQL
UPDATE productos
SET atributos = jsonb_set(atributos, '{colores}',
                          (atributos->'colores') || '"rojo"')
WHERE nombre = 'Mouse Pixel';
Resultado de consultar atributos->'colores'
 ["negro", "rojo"]
Completar

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, .

Selección múltiple

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

SQL
-- 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;
Resultado
    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:

SQL
-- ¿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;
Resultado
   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

SQL
-- 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;
Resultado
{"id": 3, "nombre": "Mouse Pixel", "precio": 120.00}

["Notebook Aurora 14", "Monitor Lumen 27"]

{"id": 2, "nombre": "Auriculares Vento", "precio": 350.00}
¿Para qué sirve armar JSON?

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:

Resultado de jsonb_pretty en el Monitor
{
    "marca": "LG",
    "puertos": [
        "HDMI",
        "DisplayPort"
    ],
    "pulgadas": 27,
    "resolucion": "2560x1440",
    "garantia_meses": 24
}
Selección múltiple

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.

SQL
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:

SQL
SET enable_seqscan = off;   -- solo para esta demostración

EXPLAIN SELECT nombre FROM productos
WHERE atributos @> '{"marca":"Lenovo"}';
Resultado (sin los costos)
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:

SQL
EXPLAIN SELECT nombre FROM productos
WHERE atributos->>'marca' = 'Lenovo';
Resultado (sin los costos)
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):

SQL
CREATE INDEX idx_productos_marca
ON productos ((atributos->>'marca'));   -- los paréntesis dobles son obligatorios

EXPLAIN SELECT nombre FROM productos
WHERE atributos->>'marca' = 'Lenovo';
Resultado (sin los costos)
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
El mismo método de siempre

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.

Selección múltiple

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 columnaDentro de un documento JSONB
Tipo estricto: un INTEGER no acepta textoCualquier clave acepta cualquier tipo; 16 y "16" conviven
NOT NULL, UNIQUE, CHECK y DEFAULT por columnaSolo se pueden simular con CHECK sobre expresiones, más incómodos
Clave foránea hacia otra tablaImposible: un id dentro del documento no se verifica contra nada
Estadísticas del planificador por columnaEl planificador sabe poco sobre lo que hay dentro, y puede elegir peores planes
Cambiar un valor reescribe esa columnaCambiar una clave reescribe el documento completo
Criterio de decisión

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.

Clasificación

Decide dónde guardarías cada dato en la tienda: ¿columna normal o documento JSONB?

El precio de cada producto, que se ordena y se suma en los reportes.
Las características técnicas, que cambian según la categoría del producto.
El id de la categoría, que debe existir en la tabla categorias.
La respuesta completa de la pasarela de pagos, guardada tal como llegó.
El correo del cliente, que no puede repetirse entre clientes.

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:

IdeaPostgreSQL (jsonb)MongoDB
Unidad de datosUna fila con una columna jsonbUn documento en una colección
Filtrar por valorWHERE atributos @> '{"marca":"Lenovo"}'find({ marca: "Lenovo" })
Campo anidadoatributos#>>'{almacenamiento,tipo}'find({ "almacenamiento.tipo": "SSD" })
La clave existeWHERE atributos ? 'garantia_meses'find({ garantia_meses: { $exists: true } })
Cambiar un campojsonb_set(atributos, '{ram_gb}', '32')updateOne(filtro, { $set: { ram_gb: 32 } })
Quitar un campoatributos - 'color'updateOne(filtro, { $unset: { color: "" } })
Agregar a un arreglojsonb_set(..., '{colores}', ... || '"rojo"')updateOne(filtro, { $push: { colores: "rojo" } })
Acelerar una búsquedaCREATE INDEX ... ON productos ((atributos->>'marca'))createIndex({ marca: 1 })
Cada herramienta con su ventaja

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.

Selección múltiple

¿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íntomaCausa habitual
invalid input syntax for type jsonEl 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 errorEl tipo no coincide ("16" texto vs. 16 número), o la clave está en otro nivel.
El UPDATE con jsonb_set no cambió nadaLa ruta intermedia no existe: jsonb_set no la crea.
null value in column "atributos" violates not-null constraint tras un jsonb_setPasaste un NULL de SQL como nuevo valor. Con jsonb_set, eso anula todo el documento.
Selección múltiple

Ejecutas WHERE atributos->'marca' = 'Lenovo' y PostgreSQL responde invalid input syntax for type json. ¿Por qué?

Selección múltiple

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?

Completar

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.

0 / 0
actividades correctas en toda la guía
Selección múltiple

¿Qué diferencia hay entre los operadores -> y ->>?

Verdadero o falso

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.

Selección múltiple

Quieres agregar "color": "gris" a un producto sin perder ninguna de sus otras claves. ¿Qué SET usas?

Selección múltiple

¿Cuál de estas es una razón en contra de guardar un dato dentro de una columna JSONB?

Selección múltiple

¿Qué hace UPDATE productos SET atributos = atributos #- '{almacenamiento,tipo}'?

Cheat Sheet

ExpresiónPara 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'->>0Elemento de un arreglo (la posición empieza en 0).
(col->>'k')::intConvertir 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_textDesarmar un documento o un arreglo en filas.
jsonb_build_object · jsonb_agg · to_jsonbArmar objetos, arreglos y documentos desde filas.
jsonb_typeof · jsonb_array_length · jsonb_prettyTipo 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.