sqlpostgresqljsonbjson

El operador JSONB @> en PostgreSQL: contencion de documentos y aceleracion con GIN

Como el operador @> comprueba si un documento JSONB contiene un fragmento, coincide objetos anidados y miembros de arrays, usa un indice GIN y se diferencia de ->>.

3 min de lecturaReferencesql · postgresql · jsonb · json · indexing

El operador @> le hace una sola pregunta a un valor JSONB: "?contienes este fragmento?". No es igualdad ni extraccion de campo, sino una prueba de contencion: el documento de la izquierda debe incluir todo lo descrito a la derecha, a cualquier profundidad. Eso es mas compacto que una cadena de condiciones ->> y, sobre todo, puede apoyarse en un indice GIN.

Que significa "contiene"

payload @> '{"plan":"pro"}' es verdadero cuando payload tiene una clave plan con valor "pro" -- aunque haya decenas de otras claves al lado. El operando derecho es un fragmento plantilla, no un documento completo.

Supongamos que users tiene una columna payload jsonb con ajustes y etiquetas:

SELECT id, email
FROM users
WHERE payload @> '{"plan":"pro"}';

Algunas reglas que conviene recordar:

  • La coincidencia es por subconjunto: el documento puede llevar cualquier cosa mas alla del fragmento.
  • Puedes indicar varias condiciones a la vez: payload @> '{"plan":"pro","active":true}' exige ambas claves.
  • Los tipos deben coincidir estrictamente: '{"active":true}' no coincide con "active":"true" (cadena frente a booleano).
  • El operador <@ es el espejo: a <@ b significa "a esta contenido en b".

Objetos anidados y arrays

La fuerza de @> es que el fragmento puede estar anidado y la coincidencia es recursiva. Pongamos lineas de pedido y una direccion de envio dentro de un pedido:

SELECT id, amount
FROM orders
WHERE payload @> '{"shipping":{"country":"DE"}}';

No importa que shipping tambien guarde city y zip -- basta con que country sea igual a "DE".

Para los arrays, @> comprueba la pertenencia, no la posicion ni el orden:

-- order whose items array includes a SKU object
SELECT id
FROM orders
WHERE payload @> '{"items":[{"sku":"A-100"}]}';

-- tags array containing both values, in any order
SELECT id
FROM users
WHERE payload @> '{"tags":["beta","eu"]}';

Trampa: los escalares de un array coinciden "como un conjunto", y los objetos dentro de un array tambien se comparan por contencion. Asi '[1,2]' @> '[2,1]' es verdadero (el orden da igual), mientras que '[1,2]' @> '[1,2,3]' es falso -- la izquierda no tiene el 3. Y recuerda: un objeto vacio '{}' esta contenido en todo objeto, asi que payload @> '{}' es verdadero casi siempre.

Indice GIN: donde @> realmente gana

Sin indice, @> obliga a un escaneo secuencial. Pero JSONB puede vivir en un indice GIN, y entonces las consultas de contencion se vuelven rapidas:

CREATE INDEX idx_users_payload ON users USING gin (payload);

-- now this can use the index
SELECT id FROM users WHERE payload @> '{"plan":"pro"}';

Un detalle sobre las clases de operador:

  • La predeterminada (jsonb_ops) soporta @>, ?, ?|, ?& e indexa tanto claves como valores.
  • La clase jsonb_path_ops (USING gin (payload jsonb_path_ops)) es mas pequena y rapida para @>, pero no soporta los operadores de existencia de clave como ?.
CREATE INDEX idx_users_payload_path
  ON users USING gin (payload jsonb_path_ops);

Verifica el plan con EXPLAIN ANALYZE: si ves un Bitmap Index Scan sobre tu indice GIN, el planificador aprovecho @>.

@> frente a la igualdad con ->>

El filtro clasico extrae un escalar y lo compara:

SELECT id FROM users
WHERE payload ->> 'plan' = 'pro';

Funciona, pero tiene trampas:

  • ->> devuelve text, asi que numeros y booleanos se convierten a cadena; es facil introducir un error de tipos sutil.
  • Un B-tree sobre (payload ->> 'plan') debe crearse por cada clave por separado; @> se sirve con un solo indice GIN sobre toda la columna.
  • Para campos anidados ->> se vuelve una escalera payload -> 'shipping' ->> 'country', mientras que @> sigue siendo una sola declaracion.

Regla practica: para comprobar "el documento contiene esta forma", usa @> mas GIN. Para rangos y desigualdades (amount > 100, created_at < ...) @> es inutil -- ahi necesitas ->> con un cast o una columna dedicada.

Otros motores carecen del operador: en MySQL el analogo es la funcion JSON_CONTAINS(payload, '{"plan":"pro"}') que devuelve 0/1, mientras que las comprobaciones de ruta pasan por JSON_EXTRACT/->>. En ClickHouse el JSON suele analizarse con funciones como JSONExtractString, y no hay un unico operador de contencion.

Practica con ejercicios reales

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

Abrir el entrenador