Módulo 1: JSONB Operators e Indexing
JSONB Path queries: la sintaxis para lo que los operadores básicos no expresan
Descripción de la cápsula
Los operadores @>, ? y ->> resuelven el 80% de las queries JSONB que vas a escribir. El otro 20% — el que aparece cuando necesitás "filtrar por elementos de un array donde una propiedad cumple una condición", o "extraé todos los valores anidados que matcheen un predicado", o "verificá que existe al menos un objeto con estas propiedades" — con operadores básicos se vuelve ilegible o directamente imposible. Termina en SQL barroco con jsonb_array_elements + subqueries que nadie quiere mantener.
PostgreSQL 12 introdujo JSON Path queries, una sintaxis dedicada inspirada en XPath/JSONPath. Es a JSONB lo que las regex son a strings: una mini-DSL específica para describir patrones complejos de forma compacta. La curva de aprendizaje es real (sintaxis nueva), pero la productividad después es enorme — queries que antes ocupaban 10 líneas se vuelven 2.
Esta cápsula te enseña el subset de JSON Path que vas a usar: navegación, filtros, predicados, los tres operadores que lo activan en SQL (@@, @?, jsonb_path_query). No vas a aprender el spec completo (es extenso); vas a aprender el 80% que cubre el 95% de los casos. Al terminar vas a poder reconocer cuándo conviene path query vs operadores básicos, y vas a poder escribir filtros sobre arrays de objetos sin pelearte con jsonb_array_elements.
Modelo mental: JSON Path es una mini-DSL embebida en strings
JSON Path no es SQL. Es un mini-lenguaje propio (estandarizado en SQL/JSON) que vivís adentro de un string de PostgreSQL. La query SQL te da el operador (@@, @?, jsonb_path_query) y tú le pasás un path expression como string.
┌──────────────────────┐
│ PostgreSQL SQL │
│ │
│ WHERE payload @@ │
│ '$.amount > 100' │ ← string con sintaxis JSON Path
│ │
└──────────────────────┘
↑
JSON Path
(mini-DSL)
La sintaxis JSON Path arranca con $ (el documento root) y desde ahí navegás con puntos, corchetes y filtros. Vamos a ir construyendo desde cero.
Comparación: misma pregunta, dos formas
Pregunta: "¿el JSON tiene un elemento en tags igual a 'admin'?"
Con operadores básicos:
SELECT * FROM users
WHERE EXISTS (
SELECT 1 FROM jsonb_array_elements_text(data->'tags') AS t
WHERE t = 'admin'
);
-- O más conciso pero con operador específico de array:
SELECT * FROM users WHERE data->'tags' ? 'admin';
Con JSON Path:
SELECT * FROM users WHERE data @? '$.tags[*] ? (@ == "admin")';
Para este caso simple, los operadores básicos son cortos y claros. Pero subimos el nivel: "¿hay algún purchase en el array events con amount > 100?":
Con operadores básicos:
SELECT * FROM users
WHERE EXISTS (
SELECT 1 FROM jsonb_array_elements(data->'events') AS e
WHERE e->>'type' = 'purchase'
AND (e->>'amount')::numeric > 100
);
Con JSON Path:
SELECT * FROM users
WHERE data @? '$.events[*] ? (@.type == "purchase" && @.amount > 100)';
Acá el path query empieza a ganar. Más expresivo, una sola línea, sin subquery.
Los tres operadores que activan JSON Path
| Operador / Función | Devuelve | Cuándo usar |
|---|---|---|
@@ | boolean | Para WHERE: el path expression como predicado completo que devuelve true/false |
@? | boolean | Para WHERE: el path expression devuelve al menos un match |
jsonb_path_query | setof jsonb | En SELECT: devolvé todos los valores que matchean el path |
Hay variantes (jsonb_path_query_first, jsonb_path_query_array, jsonb_path_exists, jsonb_path_match) que son convenience wrappers. Vamos a quedarnos con los tres principales y mencionar los útiles cuando aparezcan.
Diferencia clave: @@ vs @?
Esto confunde al principio. Resumen:
@@(matches): el path expression debe ser un predicado (algo que devuelve booleano) y el operador devuelve si el predicado es true.@?(exists): el path expression puede devolver lo que sea, y el operador devuelve si hay al menos un resultado.
-- @@: el path es un predicado completo
SELECT '{"a": 5}'::jsonb @@ '$.a > 3';
-- true
-- @?: el path no necesita ser predicado, alcanza con que matchee algo
SELECT '{"a": 5}'::jsonb @? '$.a';
-- true (la key 'a' existe)
SELECT '{"a": 5}'::jsonb @? '$.b';
-- false (la key 'b' no existe)
Cuando el path tiene un filtro al final (? (@ == ...)), tanto @@ como @? funcionan; pero la convención es @? para "existe algo que matchea".
Sintaxis JSON Path: lo esencial
Navegación
$ ← el root del documento
$.campo ← acceso a key
$.a.b.c ← acceso anidado
$.array[*] ← todos los elementos del array (wildcard)
$.array[0] ← primer elemento
$.array[0,2] ← elementos 0 y 2
$.array[1 to 3] ← elementos del 1 al 3
$.* ← cualquier key del objeto root
$.** ← descenso recursivo (cualquier nivel) — útil pero costoso
Filtros
? (predicado) ← filtra los elementos donde el predicado es true
@ ← el elemento "actual" dentro del filtro
-- "todos los items donde @ (el item actual) > 10"
$.items[*] ? (@ > 10)
-- "todos los productos donde el precio > 100"
$.products[*] ? (@.price > 100)
Operadores de predicado
| Operador | Significado |
|---|---|
== | Igualdad |
!= | Distinto |
<, <=, >, >= | Comparación numérica |
&& | AND lógico |
|| | OR lógico |
! | NOT |
like_regex "..." | Matching por regex |
starts with "..." | Prefijo |
exists(<path>) | El subpath existe |
is unknown | Comprobación de NULL/ausente |
Strings, números, booleans
"texto" ← strings con doble comilla
42, 3.14 ← números
true, false ← booleans
null ← null JSON
Trampa común: las comillas son dobles dentro del path expression, aunque el path expression viva dentro de un string SQL con comillas simples. El truco es:
SELECT data @@ '$.country == "US"' FROM events;
-- ✅ Comillas simples para el SQL string, dobles dentro del path
Ejemplos progresivos
Setup
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id SERIAL PRIMARY KEY,
data JSONB
);
INSERT INTO users (data) VALUES
('{
"name": "Ana",
"age": 30,
"country": "AR",
"tags": ["admin", "active"],
"events": [
{"type": "login", "ts": "2026-04-01"},
{"type": "purchase", "amount": 150, "ts": "2026-04-15"}
]
}'),
('{
"name": "Bob",
"age": 22,
"country": "US",
"tags": ["active"],
"events": [
{"type": "view", "ts": "2026-04-20"},
{"type": "purchase", "amount": 50, "ts": "2026-04-25"}
]
}'),
('{
"name": "Carla",
"age": 45,
"country": "MX",
"tags": ["editor"],
"events": [
{"type": "purchase", "amount": 300, "ts": "2026-04-10"},
{"type": "purchase", "amount": 200, "ts": "2026-04-22"}
]
}');
Ejemplo 1: filtro simple en root
"Usuarios mayores de 25 años."
SELECT data->>'name'
FROM users
WHERE data @@ '$.age > 25';
-- Output: Ana, Carla
Equivalente con operadores básicos:
SELECT data->>'name' FROM users WHERE (data->>'age')::int > 25;
Para este caso, los operadores básicos son tan claros como JSON Path. La elección es estilística.
Ejemplo 2: existencia en array
"Usuarios que tienen el tag 'admin'."
SELECT data->>'name'
FROM users
WHERE data @? '$.tags[*] ? (@ == "admin")';
-- Output: Ana
Equivalente con ? (operador de existencia de string en array):
SELECT data->>'name' FROM users WHERE data->'tags' ? 'admin';
Para arrays de strings, el operador ? es más limpio. JSON Path empieza a brillar con arrays de objetos.
Ejemplo 3: filtro en array de objetos
"Usuarios que hicieron al menos un purchase con amount > 100."
SELECT data->>'name'
FROM users
WHERE data @? '$.events[*] ? (@.type == "purchase" && @.amount > 100)';
-- Output: Ana, Carla
Equivalente con operadores básicos (ya verboso):
SELECT DISTINCT u.data->>'name'
FROM users u, jsonb_array_elements(u.data->'events') e
WHERE e->>'type' = 'purchase'
AND (e->>'amount')::numeric > 100;
JSON Path es 1 línea, lectura natural. Operadores básicos es 4 líneas con join lateral implícito y cast manual. La diferencia se nota.
Ejemplo 4: extraer valores con jsonb_path_query
"Para cada usuario, devolver los amounts de sus purchases."
SELECT
data->>'name' AS user_name,
jsonb_path_query(data, '$.events[*] ? (@.type == "purchase").amount') AS amount
FROM users;
-- Output:
-- user_name | amount
-- -----------+--------
-- Ana | 150
-- Bob | 50
-- Carla | 300
-- Carla | 200
jsonb_path_query devuelve una fila por valor matching, expandiendo arrays. Esto es lo que con operadores básicos requiere jsonb_array_elements.
Variante útil — jsonb_path_query_array: devuelve los matches en un solo array por fila (sin expandir):
SELECT
data->>'name',
jsonb_path_query_array(data, '$.events[*] ? (@.type == "purchase").amount') AS amounts
FROM users;
-- Output:
-- ?column? | amounts
-- ----------+----------
-- Ana | [150]
-- Bob | [50]
-- Carla | [300, 200]
Ejemplo 5: regex en strings
"Usuarios cuyo nombre empieza con 'A' o 'C'."
SELECT data->>'name'
FROM users
WHERE data @@ '$.name like_regex "^[AC]"';
-- Output: Ana, Carla
like_regex es una de las pocas formas de hacer pattern matching en JSONB sin extraer y comparar a SQL.
Ejemplo 6: descenso recursivo (**)
"Usuarios donde en cualquier nivel del JSONB aparezca un valor purchase."
SELECT data->>'name'
FROM users
WHERE data @? '$.** ? (@ == "purchase")';
-- Output: Ana, Bob, Carla (todos, porque todos tienen al menos un purchase en events)
** es poderoso pero costoso — visita cada nodo del JSONB. Útil para queries one-off de exploración, no para queries calientes.
Ejemplo 7: variables en path queries
JSON Path soporta variables externas pasadas como segundo argumento (un objeto JSONB):
SELECT data->>'name'
FROM users
WHERE jsonb_path_exists(
data,
'$.events[*] ? (@.type == "purchase" && @.amount > $min_amount)',
'{"min_amount": 100}'::jsonb
);
-- Output: Ana, Carla
Esto es útil cuando el path es estático pero los valores vienen de la app — el equivalente moderno a parametrizar queries.
Indexabilidad de JSON Path queries
Esto es importante: no todos los path queries se benefician de GIN igual.
| Tipo de path query | Indexable con GIN |
|---|---|
@@ '$.campo == "valor"' | Limitado (PostgreSQL 16+ con jsonb_ops puede usarlo en algunos casos) |
@? '$.campo' (existencia simple) | Sí en muchos casos |
@@ '$.campo > 100' (comparación numérica) | No directamente — requiere expression index sobre (payload->>'campo')::numeric |
jsonb_path_query | No directamente |
Equivalente con @> | Sí (cuando expresable) |
Regla operativa:
- Para queries que vas a indexar y correr muy frecuentemente, intentá expresarlas con
@>primero. GIN sobre@>es un patrón muy probado y rápido. - Para queries que necesitan lógica que
@>no expresa (filtros con>,like_regex, predicados sobre arrays de objetos), usá JSON Path. Acepta que probablemente vas a necesitar partial index o expression index para acelerarlas.
Un caso típico: la query principal del endpoint usa @> (rápida con GIN). La query de un admin dashboard que se corre 1 vez por minuto usa jsonb_path_query por flexibilidad — está bien que sea más lenta, no es hot path.
Ejemplo trabajado: filtrar transacciones complejas
Vamos a un caso end-to-end realista. Queremos analizar una tabla de transacciones donde cada fila tiene:
metadata JSONBcon{customer: {tier, country}, items: [{sku, qty, price}], discounts: [...]}
Pregunta: "¿qué transacciones tienen al menos un item con qty >= 5 Y price < 50, hechas por customers tier 'gold' en países hispanohablantes?"
Setup
DROP TABLE IF EXISTS transactions;
CREATE TABLE transactions (
id BIGSERIAL PRIMARY KEY,
metadata JSONB NOT NULL
);
INSERT INTO transactions (metadata) VALUES
('{
"customer": {"tier": "gold", "country": "MX"},
"items": [
{"sku": "ABC", "qty": 10, "price": 30},
{"sku": "DEF", "qty": 1, "price": 200}
],
"discounts": []
}'),
('{
"customer": {"tier": "silver", "country": "AR"},
"items": [
{"sku": "XYZ", "qty": 5, "price": 40}
],
"discounts": []
}'),
('{
"customer": {"tier": "gold", "country": "ES"},
"items": [
{"sku": "PQR", "qty": 6, "price": 25},
{"sku": "STU", "qty": 2, "price": 80}
],
"discounts": [{"code": "SAVE10", "amount": 5}]
}'),
('{
"customer": {"tier": "gold", "country": "US"},
"items": [
{"sku": "GHI", "qty": 8, "price": 20}
],
"discounts": []
}');
Query con JSON Path
SELECT id
FROM transactions
WHERE metadata @? '$.customer ? (@.tier == "gold" && (@.country == "MX" || @.country == "ES" || @.country == "AR"))'
AND metadata @? '$.items[*] ? (@.qty >= 5 && @.price < 50)';
-- Output: 1, 3
Lo que hace:
- Primer predicado: filtra transactions donde el customer es tier 'gold' y país hispanohablante.
- Segundo predicado: filtra las que tienen al menos un item con qty alta y precio bajo.
Versión equivalente con operadores básicos (para apreciar el contraste)
SELECT t.id
FROM transactions t
WHERE t.metadata @> '{"customer": {"tier": "gold"}}'
AND t.metadata->'customer'->>'country' IN ('MX', 'ES', 'AR')
AND EXISTS (
SELECT 1 FROM jsonb_array_elements(t.metadata->'items') i
WHERE (i->>'qty')::int >= 5
AND (i->>'price')::numeric < 50
);
Funciona, pero es más larga y mezcla tres estilos: @> para una parte, ->/->> para otra, lateral join implícito para los items. JSON Path mantiene un estilo unificado.
EXPLAIN
EXPLAIN ANALYZE
SELECT id FROM transactions
WHERE metadata @? '$.customer ? (@.tier == "gold")';
-- Sin index:
-- Seq Scan on transactions
-- Filter: (metadata @? '$.customer ? (@.tier == "gold")'::jsonpath)
-- Con CREATE INDEX ON transactions USING gin(metadata):
-- Bitmap Heap Scan on transactions
-- Recheck Cond: (metadata @? '$.customer ? (@.tier == "gold")'::jsonpath)
-- -> Bitmap Index Scan on transactions_metadata_idx
Sí, GIN sobre metadata con jsonb_ops (default) puede acelerar @? en muchos casos. Esto está documentado y disponible desde PostgreSQL 12+. El detalle de qué funciona exacto va en la cápsula 05.
¿Por qué importa esto en el trabajo real?
1. Queries de admin/analytics que con operadores básicos son código de mantenimiento horrible.
Endpoints internos para el equipo de producto donde hay filtros complejos sobre arrays anidados. Sin JSON Path, terminás con CTEs largos llenos de jsonb_array_elements que nadie quiere modificar. Con JSON Path, una línea expresiva.
2. Validación de payloads complejos.
Si tu API recibe payloads JSON y necesitás validar reglas tipo "el campo X debe existir Y debe haber al menos un item en Z que cumpla W", jsonb_path_exists te lo da en una llamada. Útil para CHECK constraints y triggers.
ALTER TABLE orders ADD CONSTRAINT valid_items
CHECK (data @? '$.items[*] ? (@.qty > 0 && @.price > 0)');
3. Ad-hoc data exploration.
En psql cuando estás debuggeando un dataset con JSONB, jsonb_path_query para "extraé todos los valores que matchean este patrón en cualquier nivel" es 10 veces más rápido que escribir una subquery con jsonb_array_elements.
4. Aparece en entrevistas senior.
"Conocés JSON Path queries en PostgreSQL?" — separa al dev que aprendió JSONB en 2020 del que se mantuvo actualizado a PostgreSQL 12+. Saber usarlo (no necesariamente memorizarlo) demuestra profundidad.
Trampas y errores comunes
Error 1 (sintaxis): comillas simples vs dobles
Síntoma: ERROR: syntax error at or near "..." con queries JSON Path.
Por qué pasa: confusión entre las comillas del SQL (simples) y las del path (dobles).
-- ❌ Mal: comillas dentro y fuera son simples
SELECT data @@ '$.country == 'US'' FROM events;
-- ✅ Bien: SQL afuera con simples, path adentro con dobles
SELECT data @@ '$.country == "US"' FROM events;
Error 2 (conceptual): confundir @@ con @?
Síntoma: la query devuelve resultados raros porque elegiste el operador equivocado.
Por qué pasa: ambos toman path expression, ambos devuelven boolean, pero esperan path expressions distintas.
Cómo distinguir: si tu path termina en una comparación (> 100, == "x"), usá @@ o @?. Si tu path es solo un acceso ($.campo) y quieres saber si existe, usá @?. Cuando dudes, usá @? con un filtro: data @? '$.campo ? (@ == "x")'.
Error 3 (performance): usar ** (descenso recursivo) en hot paths
Síntoma: queries con $.** lentas en tablas grandes.
Por qué pasa: ** visita cada nodo del JSONB. En documentos grandes y profundos, es O(n) sobre el tamaño del documento.
Cómo corregir: evitá ** en queries que se ejecutan frecuentemente. Si conocés la estructura, especificá el path. ** solo para exploración ad-hoc.
Error 4 (conceptual): asumir que jsonb_path_query filtra filas
Síntoma: SELECT id, jsonb_path_query(...) FROM tabla WHERE ... te devuelve más filas de las que esperabas.
Por qué pasa: jsonb_path_query es set-returning: por cada fila de input, devuelve N filas de output (una por cada match dentro del JSONB). Si una fila tiene 3 matches, vas a ver esa fila 3 veces.
Cómo corregir:
- Si quieres todos los valores expandidos: usá
jsonb_path_queryy aceptá que las filas se multiplican. - Si quieres un array por fila: usá
jsonb_path_query_array. - Si quieres solo el primer match: usá
jsonb_path_query_first.
-- 1 fila por user, con array de matches
SELECT data->>'name', jsonb_path_query_array(data, '$.events[*].type') FROM users;
-- 1 fila por user, con el primer match
SELECT data->>'name', jsonb_path_query_first(data, '$.events[*].type') FROM users;
-- N filas por user (una por match)
SELECT data->>'name', jsonb_path_query(data, '$.events[*].type') FROM users;
Error 5 (conceptual): esperar que JSON Path indexe automáticamente todo
Síntoma: creaste GIN, escribiste path query "elegante" y EXPLAIN muestra Seq Scan.
Por qué pasa: GIN acelera bien @? con paths simples y @@ con predicados básicos, pero no acelera bien queries con **, comparaciones numéricas (>, <), o like_regex.
Cómo detectar: EXPLAIN ANALYZE siempre. Si ves Seq Scan, no asumas que el path query usa el index.
Cómo corregir: para queries hot que no usan GIN, considerá expression index sobre la expresión específica. Para queries cold, aceptá Seq Scan o limita con otros filtros indexables (WHERE created_at > now() - interval '1 day' AND data @@ ... — el filtro temporal indexable corta el dataset antes de evaluar el path).
Error 6 (sintaxis): olvidar el @ adentro del filtro
Síntoma: ERROR: syntax error in JSON path cuando escribes un filtro.
Por qué pasa: dentro de un ? (...), hay que referirse al elemento actual con @. Olvidarlo es el error más común.
-- ❌ Mal
data @? '$.tags[*] ? (== "admin")'
-- ✅ Bien
data @? '$.tags[*] ? (@ == "admin")'
Ejercicios
Ejercicio 1: traducir queries a JSON Path
Las siguientes queries usan operadores básicos. Reescribilas con JSON Path y @? o @@:
a) WHERE data->>'country' = 'US'
b) WHERE (data->>'age')::int >= 18
c) WHERE data->'tags' ? 'admin'
d) WHERE EXISTS (SELECT 1 FROM jsonb_array_elements(data->'orders') o WHERE (o->>'total')::numeric > 500)
Ver solución
-- a)
WHERE data @@ '$.country == "US"'
-- o
WHERE data @? '$ ? (@.country == "US")'
-- b)
WHERE data @@ '$.age >= 18'
-- c)
WHERE data @? '$.tags[*] ? (@ == "admin")'
-- d)
WHERE data @? '$.orders[*] ? (@.total > 500)'
Notá: para queries simples como (a) y (b), JSON Path no ofrece ventaja sobre operadores básicos — la elección es estilística. Para (d), JSON Path es claramente más limpio.
Ejercicio 2: extraer valores anidados
Dada la tabla users del ejemplo anterior, escribe una query que devuelva una fila por cada purchase, mostrando el nombre del user y el amount del purchase. Usá jsonb_path_query.
Ver solución
SELECT
data->>'name' AS user_name,
jsonb_path_query(data, '$.events[*] ? (@.type == "purchase").amount') AS amount
FROM users;
-- Output:
-- user_name | amount
-- -----------+--------
-- Ana | 150
-- Bob | 50
-- Carla | 300
-- Carla | 200
Lo que pasa internamente: jsonb_path_query es set-returning. Por cada fila de users, devuelve N filas (una por cada match). Carla tiene 2 purchases → aparece 2 veces.
Variante interesante: sumarizar por user:
SELECT
data->>'name' AS user_name,
SUM((amount #>> '{}')::numeric) AS total_purchased
FROM users,
LATERAL jsonb_path_query(data, '$.events[*] ? (@.type == "purchase").amount') AS amount
GROUP BY data->>'name';
#>> '{}' convierte un jsonb scalar a text (path vacío). Después ::numeric para sumar.
Ejercicio 3: filtros complejos con AND/OR
Escribí una query que devuelva los usuarios donde:
- Tengan al menos un evento de tipo 'purchase' con
amount > 100, O - Tengan al menos un evento donde el timestamp empiece con '2026-04-15' o posterior.
Ver solución
SELECT data->>'name'
FROM users
WHERE data @? '$.events[*] ? (@.type == "purchase" && @.amount > 100)'
OR data @? '$.events[*] ? (@.ts >= "2026-04-15")';
Por qué dos @? separados con OR SQL en lugar de un || adentro del path:
Lo intentamos:
WHERE data @? '$.events[*] ? ((@.type == "purchase" && @.amount > 100) || @.ts >= "2026-04-15")'
Esto también funciona, pero la lectura es más densa. La versión con OR SQL es más fácil de mantener cuando los predicados son distintos.
Lección: JSON Path es expresivo, pero no toda la lógica tiene que estar adentro del path. SQL afuera y path adentro suele ser más legible.
Ejercicio 4: regex sobre un campo
Escribí una query que devuelva los usuarios cuyo nombre coincida con la regex ^[AB] (empieza con A o B).
Ver solución
SELECT data->>'name'
FROM users
WHERE data @@ '$.name like_regex "^[AB]"';
-- Output: Ana, Bob
Equivalente con SQL:
SELECT data->>'name' FROM users WHERE data->>'name' ~ '^[AB]';
Para este caso particular, ~ (regex de PostgreSQL) sobre ->> puede ser más natural si el campo es directo. JSON Path con like_regex brilla cuando la regex se aplica adentro de un array o anidado: $.events[*].url like_regex "https://example\\.com".
Ejercicio 5: validación con CHECK constraint
Escribí un ALTER TABLE que agregue un CHECK constraint a una tabla orders con columna data JSONB, validando que:
datadebe tener keycustomer_idcon valor numérico positivo,datadebe tener un arrayitemscon al menos un elemento,- cada item del array debe tener
qty > 0yprice >= 0.
Ver solución
ALTER TABLE orders
ADD CONSTRAINT valid_order_data
CHECK (
data @? '$.customer_id ? (@ > 0)'
AND data @? '$.items[0]'
AND NOT data @? '$.items[*] ? (@.qty <= 0 || @.price < 0)'
);
Lo que hace:
data @? '$.customer_id ? (@ > 0)'— existecustomer_idy es> 0.data @? '$.items[0]'— existe el primer elemento deitems(alcanza para validar "al menos un item").NOT data @? '$.items[*] ? (@.qty <= 0 || @.price < 0)'— no existe ningún item con qty <=0 o price < 0. Esta es la forma idiomática de "todos los items cumplen condición": negar la existencia de uno que no cumpla.
Probar:
INSERT INTO orders (data) VALUES
('{"customer_id": 1, "items": [{"qty": 2, "price": 10}]}'); -- ✅ pasa
INSERT INTO orders (data) VALUES
('{"customer_id": 1, "items": [{"qty": 0, "price": 10}]}'); -- ❌ qty=0
-- ERROR: new row for relation "orders" violates check constraint "valid_order_data"
Por qué importa: validación al nivel de DB es un safety net cuando varios servicios escriben a la misma tabla. JSON Path en CHECK constraints expresa reglas que con operadores básicos serían CASEs gigantes.
Ejercicio 6: cuándo NO usar JSON Path
Para cada caso, decide si usarías JSON Path u operadores básicos. Justificá.
a) "Listar usuarios cuyo país es 'US'" en un endpoint que se llama 1000 veces por segundo, con tabla de 10M filas.
b) "Devolver todos los items donde discount.percentage > 50 para un dashboard que se carga 1 vez por minuto."
c) "Validar que el JSONB enviado por el usuario tenga la key email con un valor que matchee una regex de email."
d) "Listar usuarios que tengan al menos un evento donde type = 'login' AND ts > '2026-01-01'."
Ver solución
a) Operadores básicos. WHERE data @> '{"country": "US"}' con GIN. Hot path, query simple, @> con jsonb_path_ops es el patrón más rápido. JSON Path acá agregaría sintaxis sin beneficio.
b) JSON Path. WHERE data @? '$.items[*] ? (@.discount.percentage > 50)'. Cold path (1/min), query con filtro sobre array de objetos con condición numérica que @> no expresa. Performance no crítica, legibilidad sí.
c) JSON Path. En un CHECK constraint o trigger: data @@ '$.email like_regex "^[a-zA-Z0-9._]+@..."'. Operadores básicos no expresan regex sobre el valor extraído sin múltiples pasos.
d) JSON Path. WHERE data @? '$.events[*] ? (@.type == "login" && @.ts > "2026-01-01")'. Filtro con AND sobre array de objetos — ejemplo clásico donde JSON Path es más limpio. Si es hot path, vas a tener que invertir en partial index o expression index.
Patrón general: JSON Path para flexibilidad y queries complejas sobre arrays. Operadores básicos para queries simples sobre top-level keys o arrays de strings, especialmente cuando vas a indexar con GIN.
Resumen y siguiente paso
En esta cápsula aprendiste:
- JSON Path es una mini-DSL embebida en strings, activada por tres operadores:
@@(predicado true),@?(matchea algo),jsonb_path_query(devuelve valores). - Sintaxis core:
$es root,.campoes acceso,[*]wildcard de array,? (predicado)filtro,@es el elemento actual. - Operadores de predicado:
==,!=,<,>,&&,||,!,like_regex,starts with. - Variables externas con
$nombre+ segundo argumento JSONB. - El gran win de JSON Path es filtros sobre arrays de objetos con condiciones complejas. Para queries simples sobre top-level keys, los operadores básicos son tan claros.
- Indexabilidad parcial: GIN ayuda con
@?y algunos@@simples, pero no acelera comparaciones numéricas nilike_regexpor sí solo. Para hot paths, considerá expression indexes o reescribir a@>cuando sea posible. - Casos típicos donde JSON Path brilla: validación con CHECK constraints, queries de analytics/admin, filtros complejos en arrays.
Antes de avanzar deberías poder:
- Distinguir cuándo
@?y@@son apropiados, y cuándo conviene reescribir a@>para usar GIN - Escribir un filtro sobre un array de objetos sin recurrir a
jsonb_array_elements - Identificar el
@dentro de los filtros y no olvidarlo - Reconocer que
jsonb_path_queryes set-returning y que multiplica filas
Siguiente cápsula — GIN indexes en JSONB. Toda la cápsula 03 y 04 hablaron de "esto se acelera con GIN" y "esto no". Ahora vamos al corazón del módulo: cómo funciona GIN internamente, qué pasa cuando creas CREATE INDEX ... USING gin(...), y la decisión más importante del módulo: jsonb_ops (default, soporta más operadores) vs jsonb_path_ops (más rápido y compacto, solo @>). Esta decisión la vas a recordar 6 meses después en una entrevista.
Recursos
- PostgreSQL 16 Documentation — JSON Path Language — referencia completa de la sintaxis JSON Path. Imprescindible.
- PostgreSQL 16 Documentation —
jsonb_path_queryand friends — todas las funciones path-related con ejemplos. - PostgreSQL Wiki — JSON SQL/JSON Path — historia y motivación del estándar SQL/JSON Path.
- pganalyze — Lukas Fittl — "JSON Path queries in PostgreSQL 12" — análisis aplicado del feature cuando se introdujo, con benchmarks y casos.
- Crunchy Data — "Using PostgreSQL JSON Path Queries" — tutorial práctico con ejemplos comparativos.
- Bruce Momjian — "PostgreSQL Improved JSON Support" — perspectiva del core team sobre JSON Path.
- SQL/JSON Path standard (ISO/IEC 9075-2:2016) — referencia oficial del estándar (sólo si quieres profundizar — no requerido).
Módulo 1 — Advanced PostgreSQL for Backend Guide
Siguiente cápsula: GIN indexes en JSONB — la decisión más importante del módulo.