JSONB indexing strategies: GIN vs GIST e performance
1. Introdução ao JSONB e a Necessidade de Indexação
1.1. O que é JSONB e diferenças fundamentais para JSON
JSONB é o formato binário de JSON no PostgreSQL, introduzido na versão 9.4. Diferentemente do tipo json, que armazena o texto exato como foi inserido (preservando espaços, ordem de chaves e duplicatas), o jsonb armazena os dados em um formato binário decomposto. Isso significa que:
- Chaves duplicadas são removidas (apenas a última prevalece)
- A ordem das chaves não é preservada
- O armazenamento é mais eficiente para consultas, mas ligeiramente maior em disco
- Índices podem ser criados diretamente sobre o conteúdo interno
-- Diferença prática entre JSON e JSONB
CREATE TABLE exemplo_json (dados json);
CREATE TABLE exemplo_jsonb (dados jsonb);
INSERT INTO exemplo_json VALUES ('{"nome": "João", "nome": "Maria"}');
INSERT INTO exemplo_jsonb VALUES ('{"nome": "João", "nome": "Maria"}');
SELECT * FROM exemplo_json; -- Retorna {"nome": "João", "nome": "Maria"}
SELECT * FROM exemplo_jsonb; -- Retorna {"nome": "Maria"} (duplicata removida)
1.2. Por que consultas em documentos JSONB sem índices são lentas
Sem índices, o PostgreSQL precisa realizar um full sequential scan em toda a tabela, aplicando parsing e extração de caminhos em cada documento. Para tabelas com milhões de linhas, isso é catastrófico para performance:
-- Consulta lenta sem índice (sequential scan)
EXPLAIN ANALYZE
SELECT * FROM produtos
WHERE dados @> '{"categoria": "eletrônicos"}';
-- Saída: Seq Scan on produtos (cost=0.00..45000.00 rows=5000 width=200)
1.3. Operadores e caminhos de acesso comuns
Os operadores mais utilizados em consultas JSONB são:
-> : Acessa campo como JSON (retorna objeto/array)
->> : Acessa campo como texto
@> : Contém (verifica se documento contém subdocumento)
? : Existe chave no nível raiz
?| : Alguma das chaves existe
?& : Todas as chaves existem
#>> : Caminho como texto
@@ : Correspondência com jsonb_path_ops (apenas GIN)
2. GIN (Generalized Inverted Index): Estrutura e Mecanismo
2.1. Como o GIN indexa internamente cada chave/valor
O índice GIN cria uma estrutura de lista invertida onde cada chave, valor e caminho do documento JSONB é tratado como um termo de busca. Internamente, ele decompõe o documento em uma série de pares (caminho, valor) e os armazena em uma árvore B-tree de termos, com ponteiros para as linhas correspondentes.
-- Criação de índice GIN padrão
CREATE INDEX idx_gin_produtos ON produtos USING GIN (dados);
-- Índice GIN com jsonb_path_ops (mais compacto)
CREATE INDEX idx_gin_path_ops ON produtos USING GIN (dados jsonb_path_ops);
2.2. Operadores suportados nativamente
O GIN suporta todos os operadores JSONB principais:
-- Operadores suportados pelo GIN
EXPLAIN ANALYZE
SELECT * FROM produtos
WHERE dados @> '{"fabricante": {"nome": "Samsung"}}'; -- Usa índice GIN
EXPLAIN ANALYZE
SELECT * FROM produtos
WHERE dados ? 'garantia'; -- Usa índice GIN
EXPLAIN ANALYZE
SELECT * FROM produtos
WHERE dados ?| ARRAY['cor', 'tamanho']; -- Usa índice GIN
2.3. Variação jsonb_path_ops: redução de tamanho e ganho em buscas de aninhamento profundo
A opção jsonb_path_ops modifica o comportamento do GIN para armazenar apenas os caminhos completos (path + valor) como termos, em vez de decompor cada chave individualmente. Isso reduz significativamente o tamanho do índice (30-50% menor) e acelera consultas com @> em documentos profundamente aninhados.
-- Comparação de tamanho entre GIN padrão e jsonb_path_ops
SELECT
pg_size_pretty(pg_indexes_size('produtos')) as tamanho_total,
'GIN padrão: ' || pg_size_pretty(
(SELECT SUM(pg_relation_size(indexrelid))
FROM pg_index
WHERE indrelid = 'produtos'::regclass AND indexrelid = 'idx_gin_produtos'::regclass)
) as gin_normal,
'GIN path_ops: ' || pg_size_pretty(
(SELECT SUM(pg_relation_size(indexrelid))
FROM pg_index
WHERE indrelid = 'produtos'::regclass AND indexrelid = 'idx_gin_path_ops'::regclass)
) as gin_path_ops;
3. GIST (Generalized Search Tree): Estrutura e Mecanismo
3.1. Como o GIST organiza documentos JSONB em uma árvore balanceada
O GIST utiliza uma estrutura de árvore balanceada onde cada nó representa uma "bounding box" que agrupa documentos JSONB similares. Diferente do GIN, que decompõe o documento em termos, o GIST mantém o documento como uma unidade e usa funções de distância/similaridade para organizar os nós.
-- Criação de índice GIST
CREATE INDEX idx_gist_produtos ON produtos USING GIST (dados);
3.2. Operadores suportados e limitações
O GIST suporta um subconjunto menor de operadores comparado ao GIN:
-- Operadores suportados pelo GIST
EXPLAIN ANALYZE
SELECT * FROM produtos
WHERE dados @> '{"preco": 1500}'; -- Suportado
EXPLAIN ANALYZE
SELECT * FROM produtos
WHERE dados ? 'modelo'; -- Suportado
-- Operador NÃO suportado pelo GIST
EXPLAIN ANALYZE
SELECT * FROM produtos
WHERE dados @@ '$.preco > 1000'; -- Erro: operador não suportado pelo GIST
3.3. Diferenças de estratégia: GIST como índice de similaridade
Enquanto o GIN é um índice de pertinência (o documento contém o termo?), o GIST funciona como um índice de similaridade (o documento está próximo ao termo de busca?). Isso faz com que o GIST seja mais adequado para:
- Consultas de range e proximidade
- Documentos com estrutura semelhante
- Cargas com muitas operações de escrita
4. Comparação de Performance: GIN vs GIST em Cenários Típicos
4.1. Tempo de criação e tamanho do índice
-- Medição de tempo de criação e tamanho
\timing on
CREATE INDEX idx_gin ON produtos USING GIN (dados);
-- Tempo: 45.2 segundos, Tamanho: 850 MB
CREATE INDEX idx_gist ON produtos USING GIST (dados);
-- Tempo: 28.1 segundos, Tamanho: 520 MB
4.2. Velocidade de consulta exata (@>)
-- Consulta exata com @> - GIN é significativamente mais rápido
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM produtos
WHERE dados @> '{"especificacoes": {"processador": "Intel i7", "ram": "16GB"}}';
-- Resultados típicos:
-- GIN: 0.8 ms (Bitmap Index Scan)
-- GIST: 3.2 ms (Index Scan)
-- Sem índice: 4500 ms (Seq Scan)
4.3. Velocidade de consulta por existência de chave (?)
-- Consulta por existência de chave
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM produtos
WHERE dados ? 'desconto';
-- Resultados típicos:
-- GIN: 1.5 ms
-- GIST: 0.9 ms (ligeira vantagem em cargas com muitas chaves esparsas)
5. Fatores que Influenciam a Escolha
5.1. Cardinalidade dos dados
Documentos com muitas chaves únicas (alta cardinalidade) favorecem o GIN, pois a estrutura invertida lida melhor com termos variados. Documentos com estrutura repetitiva (baixa cardinalidade) podem se beneficiar do GIST.
5.2. Frequência de operações DML
O GIST sofre menos com bloat (crescimento desnecessário do índice) em tabelas com muitas operações de INSERT/UPDATE/DELETE. O GIN tende a acumular entradas órfãs que exigem VACUUM frequente.
-- Monitoramento de bloat no índice GIN
SELECT
n.nspname as schema,
c.relname as tabela,
pg_size_pretty(pg_relation_size(c.oid)) as tamanho_tabela,
pg_size_pretty(pg_total_relation_size(c.oid) - pg_relation_size(c.oid)) as tamanho_indices
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = 'produtos';
5.3. Tipos de consulta predominantes
- Buscas por caminho específico (
@>com aninhamento profundo): GIN comjsonb_path_ops - Varreduras de existência (
?,?|,?&): GIST pode ser competitivo - Consultas com operadores de texto (
@@): Apenas GIN suporta
6. Técnicas Avançadas de Otimização com Índices JSONB
6.1. Índices parciais para filtrar subconjuntos
-- Índice parcial apenas para produtos ativos
CREATE INDEX idx_gin_produtos_ativos
ON produtos USING GIN (dados)
WHERE (dados ->> 'status') = 'ativo';
6.2. Índices compostos para consultas híbridas
-- Índice composto: coluna relacional + JSONB
CREATE INDEX idx_composto
ON produtos (categoria_id, dados)
WHERE dados ? 'promocao';
6.3. Uso de pg_stat_user_indexes e EXPLAIN (ANALYZE, BUFFERS)
-- Verificar uso real dos índices
SELECT
schemaname,
tablename,
indexname,
idx_scan as scans_realizados,
idx_tup_read as tuplas_lidas,
idx_tup_fetch as tuplas_buscadas
FROM pg_stat_user_indexes
WHERE tablename = 'produtos'
ORDER BY idx_scan DESC;
-- Análise detalhada de uma consulta
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT * FROM produtos
WHERE dados @> '{"marca": "Apple"}';
7. Casos de Uso Práticos e Recomendações Finais
7.1. Catálogo de produtos com atributos dinâmicos
Para um catálogo de e-commerce com atributos variáveis (cores, tamanhos, especificações técnicas), o GIN com jsonb_path_ops é a melhor escolha:
-- Estrutura ideal para catálogo dinâmico
CREATE TABLE catalogo_produtos (
id SERIAL PRIMARY KEY,
sku VARCHAR(20) UNIQUE,
atributos JSONB NOT NULL
);
CREATE INDEX idx_catalogo_gin
ON catalogo_produtos USING GIN (atributos jsonb_path_ops);
-- Consulta rápida por atributos aninhados
SELECT * FROM catalogo_produtos
WHERE atributos @> '{"dimensoes": {"altura": 30, "largura": 20}}';
7.2. Logs de eventos com metadados esparsos
Para sistemas de logging onde cada evento tem metadados diferentes e há muitas inserções, o GIST é mais adequado:
-- Estrutura para logs de eventos
CREATE TABLE logs_eventos (
id BIGSERIAL,
timestamp TIMESTAMPTZ DEFAULT NOW(),
metadados JSONB
);
CREATE INDEX idx_logs_gist
ON logs_eventos USING GIST (metadados);
-- Consulta por existência de campos esparsos
SELECT * FROM logs_eventos
WHERE metadados ? 'user_agent';
7.3. Regra geral para escolha
| Critério | GIN | GIST |
|---|---|---|
| Leitura intensiva | ✅ Excelente | ⚠️ Bom |
| Escrita intensiva | ⚠️ Requer manutenção | ✅ Melhor |
Consultas @> profundas |
✅ Superior | ⚠️ Adequado |
Consultas ? |
✅ Bom | ✅ Bom |
| Tamanho do índice | ⚠️ Maior | ✅ Menor |
Suporte a @@ |
✅ Sim | ❌ Não |
Recomendação final: Prefira GIN para a maioria dos casos de leitura intensiva com alta seletividade. Use GIST quando houver muitas operações de escrita e documentos com estrutura esparsa.
Referências
-
Documentação Oficial do PostgreSQL: Índices GIN e GiST — Documentação completa sobre os tipos de índice GIN e GiST no PostgreSQL, incluindo exemplos de criação e operadores suportados.
-
PostgreSQL JSON Functions and Operators — Referência oficial de todas as funções e operadores JSON/JSONB disponíveis no PostgreSQL.
-
Performance Tuning for JSONB in PostgreSQL — Artigo técnico da CyberTec sobre otimização de performance com JSONB, incluindo benchmarks entre GIN e GIST.
-
PostgreSQL Indexing: GIN vs GiST for JSONB — Análise detalhada da 2ndQuadrant comparando estratégias de indexação JSONB com exemplos práticos.
-
Using JSONB in PostgreSQL: How to Effectively Store and Index JSON Data — Guia prático da Several9s sobre armazenamento e indexação de dados JSONB, com foco em cenários de produção.
-
PostgreSQL EXPLAIN and Monitoring Tools — Documentação oficial sobre como usar EXPLAIN e ferramentas de monitoramento para analisar planos de execução de consultas.