Cuando guardas datos semiestructurados en una columna JSONB, toda lectura empieza con dos operadores: -> y ->>. Son la base de cualquier consulta JSON en PostgreSQL: uno desciende por la estructura y el otro extrae un valor listo para usar. Veamos en que se diferencian, como acceder a campos de objeto y elementos de array, y como encadenarlos.
-> devuelve jsonb, ->> devuelve text
Ambos operadores obtienen un valor por clave, pero devuelven tipos distintos. -> entrega jsonb (un objeto, un array o un escalar en forma JSON), mientras que ->> entrega text plano. Esa unica diferencia condiciona todo lo demas.
SELECT
prefs -> 'theme' AS as_jsonb,
prefs ->> 'theme' AS as_text
FROM users;
Una regla sencilla:
- Usa
-> cuando vas a bajar mas y le seguira otro -> o ->>.
- Usa
->> al final del camino, cuando quieres un valor escalar para mostrar o comparar.
La clave es una cadena (el nombre de un campo del objeto), y una clave inexistente devuelve NULL en vez de un error.
Campos de objeto y elementos de array
Si a la derecha del operador hay una cadena, PostgreSQL busca un campo del objeto. Si hay un entero, indexa un array, y la indexacion empieza en cero. Un indice negativo cuenta desde el final.
SELECT
meta -> 'tags' -> 0 AS first_tag_json,
meta -> 'tags' ->> 0 AS first_tag_text,
meta -> 'tags' ->> -1 AS last_tag
FROM users;
Lo mismo aplica sobre tus propias tablas. Supongamos que orders tiene un payload con un array de lineas:
SELECT
id,
payload -> 'items' -> 0 ->> 'sku' AS first_sku
FROM orders
WHERE status = 'paid';
Encadenado y conversion de tipos
Un camino largo no es mas que varios operadores seguidos. Desciende con ->, manteniendote en jsonb, y en el ultimo paso cambia a ->> para obtener text. Como ->> siempre devuelve texto, convierte de forma explicita con :: para numeros, fechas y booleanos.
SELECT
profile -> 'address' ->> 'city' AS city,
(profile ->> 'age')::int AS age_int
FROM users
WHERE (profile ->> 'age')::int >= 18;
Un ejemplo real de agregacion: extraer un importe de los metadatos JSON y calcular el ticket medio por pais.
SELECT
u.country,
round(avg((o.payload ->> 'amount')::numeric), 2) AS avg_amount
FROM orders o
JOIN users u ON u.id = o.user_id
GROUP BY u.country
ORDER BY avg_amount DESC;
Trampas habituales
- Comparar sin convertir casi siempre es un error:
payload ->> 'amount' > '99' compara cadenas lexicograficamente, asi que '9' queda por encima de '100'. Convierte a numero: (payload ->> 'amount')::numeric > 99.
-> sobre un escalar devuelve jsonb, asi que prefs -> 'theme' = 'dark' falla por error de tipos; usa ->> para comparar o escribe prefs -> 'theme' = '"dark"'::jsonb.
- Una clave inexistente o un indice fuera de rango devuelve
NULL en silencio, asi que un error tipografico en un nombre de campo no lanza error y corrompe el resultado sin avisar.
->> sobre un objeto o un array devuelve su texto JSON completo, no un solo valor; toma texto solo sobre escalares.
MySQL no tiene este encadenado con flechas: usa JSON_EXTRACT(doc, '$.address.city') o el atajo doc->>'$.address.city' (su ->> tambien devuelve texto). ClickHouse ofrece funciones como JSONExtractString(col, 'city') y JSONExtractInt(col, 'age'). La idea es universal, pero la sintaxis de flechas -> / ->> es propia de PostgreSQL.
Cuando guardas datos semiestructurados en una columna
JSONB, toda lectura empieza con dos operadores:->y->>. Son la base de cualquier consulta JSON en PostgreSQL: uno desciende por la estructura y el otro extrae un valor listo para usar. Veamos en que se diferencian, como acceder a campos de objeto y elementos de array, y como encadenarlos.-> devuelve jsonb, ->> devuelve text
Ambos operadores obtienen un valor por clave, pero devuelven tipos distintos.
->entregajsonb(un objeto, un array o un escalar en forma JSON), mientras que->>entregatextplano. Esa unica diferencia condiciona todo lo demas.-- prefs is a jsonb column on users: {"theme": "dark", "lang": "en"} SELECT prefs -> 'theme' AS as_jsonb, -- "dark" (with quotes, type jsonb) prefs ->> 'theme' AS as_text -- dark (no quotes, type text) FROM users;Una regla sencilla:
->cuando vas a bajar mas y le seguira otro->o->>.->>al final del camino, cuando quieres un valor escalar para mostrar o comparar.La clave es una cadena (el nombre de un campo del objeto), y una clave inexistente devuelve
NULLen vez de un error.Campos de objeto y elementos de array
Si a la derecha del operador hay una cadena, PostgreSQL busca un campo del objeto. Si hay un entero, indexa un array, y la indexacion empieza en cero. Un indice negativo cuenta desde el final.
-- meta = {"tags": ["new", "vip", "eu"], "score": 7} SELECT meta -> 'tags' -> 0 AS first_tag_json, -- "new" as jsonb meta -> 'tags' ->> 0 AS first_tag_text, -- new as text meta -> 'tags' ->> -1 AS last_tag -- eu (last element) FROM users;Lo mismo aplica sobre tus propias tablas. Supongamos que
orderstiene unpayloadcon un array de lineas:-- payload = {"items": [{"sku": "A1", "qty": 2}, {"sku": "B7", "qty": 1}]} SELECT id, payload -> 'items' -> 0 ->> 'sku' AS first_sku FROM orders WHERE status = 'paid';Encadenado y conversion de tipos
Un camino largo no es mas que varios operadores seguidos. Desciende con
->, manteniendote enjsonb, y en el ultimo paso cambia a->>para obtenertext. Como->>siempre devuelve texto, convierte de forma explicita con::para numeros, fechas y booleanos.-- profile = {"address": {"city": "Berlin", "zip": "10115"}, "age": "34"} SELECT profile -> 'address' ->> 'city' AS city, (profile ->> 'age')::int AS age_int FROM users WHERE (profile ->> 'age')::int >= 18;Un ejemplo real de agregacion: extraer un importe de los metadatos JSON y calcular el ticket medio por pais.
-- payload = {"amount": "149.90", "currency": "EUR"} SELECT u.country, round(avg((o.payload ->> 'amount')::numeric), 2) AS avg_amount FROM orders o JOIN users u ON u.id = o.user_id GROUP BY u.country ORDER BY avg_amount DESC;Trampas habituales
payload ->> 'amount' > '99'compara cadenas lexicograficamente, asi que'9'queda por encima de'100'. Convierte a numero:(payload ->> 'amount')::numeric > 99.->sobre un escalar devuelvejsonb, asi queprefs -> 'theme' = 'dark'falla por error de tipos; usa->>para comparar o escribeprefs -> 'theme' = '"dark"'::jsonb.NULLen silencio, asi que un error tipografico en un nombre de campo no lanza error y corrompe el resultado sin avisar.->>sobre un objeto o un array devuelve su texto JSON completo, no un solo valor; toma texto solo sobre escalares.MySQL no tiene este encadenado con flechas: usa
JSON_EXTRACT(doc, '$.address.city')o el atajodoc->>'$.address.city'(su->>tambien devuelve texto). ClickHouse ofrece funciones comoJSONExtractString(col, 'city')yJSONExtractInt(col, 'age'). La idea es universal, pero la sintaxis de flechas-> / ->>es propia de PostgreSQL.