sqlpostgresqljsonjsonb

JSONB_EACH en PostgreSQL: expande un objeto JSON en filas clave-valor

Usa jsonb_each para expandir un objeto JSON en filas (clave, valor), recorrer claves dinamicas, filtrar y agregar entradas, y saber cuando usar la variante _text.

3 min de lecturaReferencesql · postgresql · json · jsonb · set-returning

jsonb_each es una funcion que devuelve un conjunto de filas (set-returning function): toma un solo objeto JSON y lo expande en una fila por cada par (key, value). Es la operacion inversa de JSONB_OBJECT_AGG: alli las filas se plegaban en un mapa, aqui el mapa vuelve a ser filas que puedes filtrar, unir y agregar con SQL normal.

Sintaxis y ejemplo basico

La funcion recibe un argumento de tipo jsonb y devuelve dos columnas: key de tipo text y value de tipo jsonb. El lugar natural para llamarla es la clausula FROM, no la lista del SELECT, de modo que cada entrada del objeto se convierte en su propia fila.

SELECT key, value
FROM jsonb_each('{"theme": "dark", "lang": "en"}'::jsonb);

El resultado son dos filas: ('theme', "dark") y ('lang', "en"). Algunos comportamientos que conviene recordar:

  • value sigue siendo jsonb, asi que los valores de texto vuelven entre comillas: "dark", no dark.
  • El orden de las filas no esta garantizado: un objeto es un mapa, no una lista.
  • Si el argumento es un array o un escalar en lugar de un objeto, se produce un error. Para expandir un array usa jsonb_array_elements.
  • Con una entrada NULL la funcion devuelve cero filas, no una fila con NULL.

Expandir una columna JSON en filas

Lo habitual es que el objeto este en una columna, no en un literal. Supongamos que users tiene un campo prefs jsonb con un conjunto arbitrario de ajustes. Para expandirlo en filas, coloca la funcion en el FROM tras una coma (es un join LATERAL implicito):

SELECT u.id, u.email, e.key, e.value
FROM users AS u, jsonb_each(u.prefs) AS e
WHERE u.id = 1;

Cada par de prefs se convierte en su propia fila ligada al usuario. La verdadera fuerza es que las claves pueden ser dinamicas: no necesitas conocer sus nombres de antemano, como tendrias que hacer con prefs->>'theme'. Es ideal para recorrer una forma desconocida o contar que claves aparecen siquiera.

Trampa: si prefs llega a ser NULL, el join implicito descarta en silencio a ese usuario del resultado. Para conservar la fila del usuario, usa un LEFT JOIN LATERAL ... ON true explicito y pasale COALESCE(prefs, '{}'::jsonb).

Filtrar y agregar entradas

Una vez que las entradas del objeto son filas, se aplica todo el arsenal habitual de SQL. Cuenta cuantas claves tiene cada objeto de ajustes:

SELECT u.id, count(*) AS pref_count
FROM users AS u, jsonb_each(u.prefs) AS e
GROUP BY u.id;

O averigua que claves son las mas comunes en toda la tabla, una forma clasica de explorar un JSON "salvaje" que se escribio sin esquema:

SELECT e.key, count(*) AS used_by
FROM users AS u, jsonb_each(u.prefs) AS e
GROUP BY e.key
ORDER BY used_by DESC;

Puedes filtrar por clave o por valor. Encuentra usuarios con al menos un ajuste igual a la cadena "on":

SELECT DISTINCT u.id, u.email
FROM users AS u, jsonb_each(u.prefs) AS e
WHERE e.value = '"on"'::jsonb;

Fijate en la comparacion: como value es jsonb, el literal tambien debe ser JSON valido, de ahi las comillas dentro de '"on"'.

La variante _text: cuando no quieres valores JSON

Si los valores son en realidad solo texto, las comillas a su alrededor solo estorban. Para eso esta jsonb_each_text: devuelve el mismo par, pero value es text, sin comillas envolventes.

-- value comes back as plain text: dark, not "dark"
SELECT key, value
FROM jsonb_each_text('{"theme": "dark", "lang": "en"}'::jsonb);

La diferencia importa cuando necesitas comparar valores o convertirlos a un numero:

-- read a numeric flag straight from a dynamic key
SELECT u.id, (e.value)::int AS limit_value
FROM users AS u, jsonb_each_text(u.prefs) AS e
WHERE e.key = 'rate_limit';

Con jsonb_each lo mismo exigiria un segundo cast para quitar el tipo. Cuidado con la trampa: jsonb_each_text tambien convierte a cadena los objetos y arrays anidados, que vuelven como un texto JSON serializado, y un null de JSON se vuelve NULL de SQL.

Notas sobre otras bases de datos

Expandir un objeto JSON en filas es una fortaleza clara de PostgreSQL. MySQL no tiene un equivalente directo de jsonb_each: normalmente extraes las claves con JSON_KEYS y luego expandes ese array con JSON_TABLE, lo cual es mas verboso. En ClickHouse, los objetos JSON suelen guardarse como tipo Map, y recorres con mapKeys y mapValues mas arrayJoin en vez de una funcion dedicada. Cuando necesitas convertir un mapa de vuelta en filas y trabajarlo con SQL normal, jsonb_each y jsonb_each_text en PostgreSQL siguen siendo la herramienta mas directa.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador