sqlpostgresqljsonbjson

O operador JSONB @> no PostgreSQL: contencao de documentos e aceleracao com GIN

Como o operador @> verifica se um documento JSONB contem um fragmento, casa objetos aninhados e membros de arrays, usa indice GIN e difere de filtros com ->>.

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

O operador @> faz uma unica pergunta a um valor JSONB: "voce contem este fragmento?". Nao e igualdade nem extracao de campo, e sim um teste de contencao: o documento da esquerda deve incluir tudo o que esta descrito a direita, em qualquer profundidade. Isso e mais compacto que uma cadeia de condicoes ->> e, o mais importante, pode se apoiar em um indice GIN.

O que significa "contem"

payload @> '{"plan":"pro"}' e verdadeiro quando payload tem uma chave plan com o valor "pro" -- mesmo que dezenas de outras chaves estejam ao lado. O operando direito e um fragmento modelo, nao um documento completo.

Suponha que users tenha uma coluna payload jsonb com configuracoes e rotulos:

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

Algumas regras que vale lembrar:

  • A correspondencia e por subconjunto: o documento pode carregar qualquer coisa alem do fragmento.
  • Voce pode declarar varias condicoes de uma vez: payload @> '{"plan":"pro","active":true}' exige ambas as chaves.
  • Os tipos devem coincidir estritamente: '{"active":true}' nao casa com "active":"true" (string contra booleano).
  • O operador <@ e o espelho: a <@ b significa "a esta contido em b".

Objetos aninhados e arrays

A forca do @> e que o fragmento pode ser aninhado e a correspondencia e recursiva. Vamos colocar itens e um endereco de entrega dentro de um pedido:

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

Nao importa que shipping tambem guarde city e zip -- basta que country seja igual a "DE".

Para arrays, @> testa a pertinencia, nao a posicao nem a ordem:

-- 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"]}';

Pegadinha: os escalares de um array casam "como um conjunto", e os objetos dentro de um array tambem sao comparados por contencao. Assim '[1,2]' @> '[2,1]' e verdadeiro (a ordem nao importa), enquanto '[1,2]' @> '[1,2,3]' e falso -- a esquerda nao tem o 3. E lembre: um objeto vazio '{}' esta contido em todo objeto, entao payload @> '{}' e verdadeiro quase sempre.

Indice GIN: onde o @> realmente vence

Sem indice, @> forca uma varredura sequencial. Mas o JSONB pode morar em um indice GIN, e entao as consultas de contencao ficam 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"}';

Um detalhe sobre as classes de operador:

  • A padrao (jsonb_ops) suporta @>, ?, ?|, ?& e indexa tanto chaves quanto valores.
  • A classe jsonb_path_ops (USING gin (payload jsonb_path_ops)) e menor e mais rapida para @>, mas nao suporta os operadores de existencia de chave como ?.
CREATE INDEX idx_users_payload_path
  ON users USING gin (payload jsonb_path_ops);

Confira o plano com EXPLAIN ANALYZE: se voce ver um Bitmap Index Scan no seu indice GIN, o planejador aproveitou o @>.

@> contra a igualdade com ->>

O filtro classico extrai um escalar e o compara:

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

Funciona, mas tem armadilhas:

  • ->> retorna text, entao numeros e booleanos sao convertidos para string; e facil introduzir um erro de tipo sutil.
  • Um B-tree sobre (payload ->> 'plan') precisa ser criado por chave separadamente; @> e atendido por um unico indice GIN sobre a coluna inteira.
  • Para campos aninhados ->> vira uma escada payload -> 'shipping' ->> 'country', enquanto @> continua uma unica declaracao.

Regra pratica: para testar "o documento contem esta forma", use @> mais GIN. Para faixas e desigualdades (amount > 100, created_at < ...) @> e inutil -- ali voce precisa de ->> com um cast ou uma coluna dedicada.

Outros motores nao tem o operador: no MySQL o analogo e a funcao JSON_CONTAINS(payload, '{"plan":"pro"}') que retorna 0/1, enquanto as verificacoes de caminho passam por JSON_EXTRACT/->>. No ClickHouse o JSON costuma ser analisado com funcoes como JSONExtractString, e nao ha um unico operador de contencao.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador