Contém E Está Contido
Contém e está contido: operadores que todo mundo usa errado
A maioria das pessoas conhece o @> como operador de containment do PostgreSQL, mas a maioria erra na hora de usar. Já vi query que demorava 40 segundos porque alguém não entendia a diferença entre contém e está contido — e aí virava uma pesquisa sequencial em vez de um index scan. O operador @> lê "contém". O @ lê "está contido". É isso. A confusão começa porque o sentido visual dos símbolos é invertido para quem tá acostumado com matemática. No SQL, o operando da esquerda é o que possui, e o da direita é o que é verificado. Então:
[1, 2, 3] @> ARRAY[1, 2] — verdadeiro, porque o array da esquerda contém os elementos da direita. ARRAY[1, 2] @ [1, 2, 3] — verdadeiro, porque o array da esquerda está contido no da direita. Eles são o oposto um do outro. Inverter a ordem dos operandos inverte o operador e o resultado continua o mesmo. Isso parece óbvio até você escrever uma query em produção e esquecê-la por três meses.
onde isso é útil na prática
O uso mais comum é filtrar colunas do tipo array. Você tem uma tabela de produtos com uma coluna tags TEXT[] e quer encontrar todos os produtos que têm pelo menos uma tag em comum com uma lista. A solução limpa é: SELECT * FROM produtos WHERE tags @> ARRAY['eletrônico', 'promoção']; Isso retorna produtos que têm ambas as tags. Se quiser pelo menos uma, aí o && (overlap) é mais adequado. Muita gente confunde os dois e acaba filtrando a coisa errada. Eu já perdi duas horas num debug desses — o filtro parecia certo na lógica, mas a query trazia resultados que não deveriam estar lá porque eu estava usando @> quando deveria ser &&.

o problema que ninguém conta
Arrays multidimensionais não se comportam como você espera. Testei isso recentemente em um migração onde tínhamos dados legados com INT[][] e queria verificar se um array de uma dimensão estava contido. O operador simplesmente falhava com erro de tipo, mesmo que semanticamente fizesse sentido. A solução foi explicitamente transformar com array_prepend ou fazer cast manual para a dimensão correta antes de aplicar o operador. Se você trabalha com dados que vieram de outra base ou de scripts antigos, quase certeza que vai se depurar com isso. Se a coluna é array, o GIN index é o caminho natural. CREATE INDEX idx_tags ON produtos USING gin (tags); — com isso, queries com @> e && usam o índice. Sem index, o PostgreSQL faz scan completo. Em tabelas pequenas não importa. Em tabelas com milhões de linhas, a diferença é entre 50ms e 8 segundos. Um detalhe importante: o @ (está contido) não tem suporte nativo a índice GIN da mesma forma. O banco vai indexar o lado esquerdo com GIN, mas se você inverter a consulta colocando o array grande à esquerda, o plano pode mudar drasticamente. Eu já vi query que virou seq scan porque o optimizer achou que fazer hash anti-join era mais barato, quando na verdade um nested loop com index scan seria muito melhor. Forçar o plano com SET enable_hashjoin = off; resolveu no momento, mas a solução real foi reformular a query para manter o array pequeno à direita do @>.
ranges, não só arrays
Os mesmos operadores funcionam com tipos range. [10, 20] @> 15 retorna verdadeiro. Isso é útil para perguntas do tipo "este intervalo de datas contém alguma data específica" ou "uma faixa de preço contém o valor do produto". A desvantagem é que ranges têm regras próprias de sobreposição que não valem para arrays, então o que funciona para um não funciona para o outro sem ajuste. Não use @> para verificar se um valor simples está dentro de um array. Se você quer saber se 5 está em [1, 2, 3, 5, 6], o correto é 5 @ ARRAY[1, 2, 3, 5, 6] ou simplesmente 5 = ANY(ARRAY[1, 2, 3, 5, 6]). Usar @> com escalar à esquerda funciona, mas é contra-intuitivo e deixa a intenção da query menos clara para quem for dar manutenção depois. Outro problema: arrays com NULL. ARRAY[1, NULL, 3] @> ARRAY[1] retorna true, mas ARRAY[1, NULL, 3] @> ARRAY[NULL] retorna null, não true. Isso quebra filtros em WHERE sem aviso. Se sua coluna pode ter NULLs dentro do array, o comportamento do containment muda de forma silenciosa. A mitigação é usar CASE WHEN ou verificar explicitamente com IS NOT DISTINCT FROM.
quando não usar
Se você está trabalhando com conjuntos grandes e dinâmicos, onde a cardinalidade varia muito, às vezes vale mais a pena normalizar para uma tabela de relacionamentos e usar JOIN ao invés de arrays com containment. A manutenção fica mais fácil, as queries ficam mais previsíveis, e o planner do PostgreSQL lida melhor com JOINs do que com operações de array em cenários complexos. Eu migrei um sistema inteiro de arrays para tabela de tags-relacionamento e o tempo médio de query caiu de 2 segundos para 80ms porque eliminatei a sobrecarga de materialização de array em cada linha. O operador @> e seu oposto @ são ferramentas úteis quando você entende exatamente o que estão fazendo. O perigo é tratar como mágica e esquecer que o banco executa tudo de forma diferente dependendo de onde os operandos estão posicionados e se existem índices adequados.

Galeria
-
Integridade e Controles Internos: contém ou está contido? -
Contem E Está Contido - RETOEDU -
CONJUNTO | PERTENCE E NÃO PERTENCE | ESTÁ CONTIDO E CONTÉM - YouTube