Como funciona contenção no banco de dados na prática
O PostgreSQL tem dois operadores que todo mundo confunde na hora da query: o @> (contém) e o <@ (está contido). A confusão acontece porque eles são inversos um do outro e o sentido visual não ajuda muito. Vou explicar pelo uso real, não pela teoria.
Contém e está contido — a lógica básica
O operador @> pergunta: o lado esquerdo contém o lado direito? Já o <@ inverte essa lógica: o lado esquerdo está contido no lado direito. O erro mais comum que eu vejo em código novo é simplesmente aplicar o operador invertido e achar que o resultado está errado quando na verdade é só a orientação que está trocada. Isso consome tempo de debug desnecessário. Para arrays:
[1, 2, 3] @> ARRAY[2] retorna verdadeiro — o array maior contém o menor.
ARRAY[2] <@ [1, 2, 3] também é verdadeiro — o mesmo fato, outra direção. Para JSON/JSONB a coisa ganha corpo. O operador verifica se um documento JSON está contido dentro de outro, considerando chaves e valores. Não é uma comparação de igualdade — é verificação de subconjunto.
'{"a": 1, "b": 2}'::jsonb @> '{"a": 1}'::jsonb retorna true.
'{"a": 1}'::jsonb <@ '{"a": 1, "b": 2}'::jsonb retorna true também.
Cenário real onde isso quebra sua query
Eu estava construindo um filtro de busca em um sistema de pedidos com JSONB. A estrutura tinha campos aninhados como usuario.endereco.cidade e o cliente queria filtrar por todos os pedidos cuja cidade fosse São Paulo. A intuição era usar o operador de contenção direta no caminho completo. O problema é que o operador @> com JSONB não resolve caminhos profundamente aninhados da forma como muitas pessoas esperam. Ele verifica a presença das chaves no documento alvo, mas faz uma correspondência exata de valores. Quando você coloca um objeto menor como lado direito, o PostgreSQL exige que todas as chaves presentes no documento menor estejam com os valores idênticos no documento maior. Isso é poderoso, mas gera armadilha frequente.
A solução que funcionou foi combinar o operador com a extração via caminho usando o operador #>: '{"usuario": {"endereco": {"cidade": "São Paulo"}}}'::jsonb #>> '{usuario,endereco,cidade}' = 'São Paulo'
👉 Clique no botão abaixo para saber mais sobre o assunto!
Para filtros mais complexos onde precisava de ambos os lados — conter e ser contido — a abordagem foi criar uma coluna computada com GIN index usando jsonb_path_ops, que é significativamente mais rápida para consultas de contenção recorrentes do que varrer o JSON inteiro a cada query.
Índices que realmente importam
Se você vai rodar contenção JSONB em produção, o índice GIN com a opção jsonb_path_ops é o que faz diferença. Um índice GIN padrão com jsonb_ops funciona mas é mais lento em comparações de contenção pura porque indexa mais metadado do que o necessário. A configuração com path_ops cria um índice mais enxuto e as queries de @> e <@ rodam consideravelmente mais rápido, especialmente em tabelas com milhões de linhas. Para arrays, o índice GIN padrão resolve. Não precisa de configuração especial.
Uma observação importante: conteúdo JSONB mal estruturado ou campos com nulls podem fazer o operador retornar resultados inesperados. Um valor null dentro do JSON de consulta pode ser ignorado pelo operador de contenção, o que significa que você pode estar encontrando documentos que não correspondem exatamente ao que imaginou. Sempre valide o schema dos dados antes de confiar cegamente no resultado.
Quando não usar contenção
Se o seu caso de uso envolve consulta por valores específicos dentro de campos aninhados e você precisa de muita especificidade, contenção JSONB pode não ser a melhor escolha. Nesses cenários, normalizar os dados para colunas tradicionais com índices B-tree costuma ser mais eficiente. A contenção brilha em cenários de filtros flexíveis e semi-estruturados, não em busca exata por campos profundos. O operador @> com JSONB também não aproveita índices de forma tão eficaz quando o documento do lado esquerdo é muito grande — nesse caso, a varredura pode se tornar mais cara do que buscar diretamente nas colunas normalizadas. Se sua tabela tem JSONB com documentos de kilobytes e você consulta frequentemente, considere extrair os campos mais consultados para colunas separadas.
Dica prática de sintaxe
Um artifício que uso para nunca errar o sentido dos operadores é ler em voz alta. A @> B lê como "A contém B". B <@ A lê como "B está contido em A". Os dois exprimem a mesma relação, só que de ângulos diferentes. Quando a query fica complexa com múltiplas condições, essa leitura ajuda a manter a coerência lógica sem ter que decoreba. Outro ponto que passa despercebido: o operador @> com arrays não considera duplicatas. '{1,1,2}'::int4array @> '{1}'::int4array é true, mas também seria true se o array direito fosse {1,1}. A contenção de arrays verifica existência, não multiplicidade. Se você precisa considerar repetições, o operador não é a ferramenta certa e deve buscar outra abordagem.
Documentação oficial: PostgreSQL JSON Functions and Operators