JSONB é útil para atributos que realmente variam entre linhas: payloads de webhook, metadados de produto, configurações de integração ou propriedades de eventos. Não é uma desculpa para esconder um modelo relacional em uma coluna. A diferença aparece quando a tabela cresce e cada predicado JSON vira um sequential scan.
Dados de exemplo
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now(),
payload jsonb NOT NULL
);
INSERT INTO events (payload) VALUES
('{"type":"payment","amount":89.90,"customer":{"country":"BR"},"tags":["vip","mobile"]}'),
('{"type":"signup","customer":{"country":"US"},"tags":["web"]}');
Leia valores simples e aninhados
-- Valor JSON
SELECT payload -> 'customer' AS customer
FROM events;
-- Valor texto, adequado para comparação
SELECT id, payload ->> 'type' AS event_type
FROM events
WHERE payload ->> 'type' = 'payment';
-- Valor texto aninhado
SELECT id
FROM events
WHERE payload #>> '{customer,country}' = 'BR';
Use -> quando a próxima operação precisa de JSONB e ->> quando você precisa de texto. Misturar os dois é causa frequente de erros confusos de operador.
Containment: o melhor encaixe para um índice GIN geral
O operador de contenção @> pergunta se um documento contém um fragmento JSON. Ele combina naturalmente com um índice GIN de propósito geral.
CREATE INDEX events_payload_gin_idx
ON events USING gin (payload);
SELECT id, created_at
FROM events
WHERE payload @> '{"type":"payment","customer":{"country":"BR"}}';
Use EXPLAIN (ANALYZE, BUFFERS) depois de criar o índice. O PostgreSQL pode corretamente escolher sequential scan numa tabela pequena ou para um predicado pouco seletivo.
Índices de expressão para uma chave muito consultada
Quando uma chave JSON é filtrada o tempo todo, um expression index B-tree pequeno geralmente é melhor que um GIN geral grande.
CREATE INDEX events_type_idx
ON events ((payload ->> 'type'));
SELECT COUNT(*)
FROM events
WHERE payload ->> 'type' = 'payment';
Para uma chave aninhada, indexe exatamente a expressão usada pela query:
CREATE INDEX events_country_idx
ON events ((payload #>> '{customer,country}'));
SELECT id
FROM events
WHERE payload #>> '{customer,country}' = 'BR';
Arrays e existência
-- O documento possui uma chave no nível superior?
SELECT id FROM events WHERE payload ? 'amount';
-- tags contém "vip"?
SELECT id FROM events WHERE payload @> '{"tags":["vip"]}';
Um índice GIN padrão suporta esses operadores. Não suponha que ele acelera toda expressão JSONPath; valide o plano para o operador exato que você usa.
Quando JSONB é o modelo errado
- Um valor é obrigatório em toda linha e participa de joins.
- Você precisa de foreign keys, unicidade ou restrições de faixa nele.
- Você o ordena ou agrega na maior parte das requisições.
- A chave é central ao domínio do produto, não metadado incidental.
Nesses casos, modele o valor como coluna tipada. Mantenha atributos flexíveis e esparsos em JSONB e mova campos estáveis e muito acessados para colunas conforme a aplicação amadurece.
Custo de escrita: índices GIN aceleram leituras, mas adicionam trabalho a cada insert e update. Indexe somente caminhos JSONB que sua carga realmente consulta e monitore tamanho de índice e latência de escrita após o rollout.
O PG Monitoring ajuda mostrando quais predicados JSONB consomem tempo, se um índice mudou o plano e se a manutenção de índice está virando parte do gargalo de escrita.