Indexando e consultando JSONB no PostgreSQL com GIN + pg_trgm e Prisma
Por John Pires · 08 de março de 2026

Motivacao e trade-offs
Problema prático: consultas lentas em campos JSONB
Armazenar objetos flexíveis em colunas JSONB é prático: modelagem ágil, evoluções sem migração e suporte nativo do PostgreSQL. O trade-off aparece quando esses objetos precisam ser pesquisados por conteúdo — filtros por campos aninhados, buscas por substring em textos e consultas por estruturas parciais. Sem índices adequados, operações @> (containment), ->> (extrair texto) e buscas por padrões podem provocar varreduras completas (sequential scans) e latência alta.
Quais são as alternativas e quando escolher cada uma
Alternativas comuns são: extrair campos para colunas relacionais (normalização), usar colunas geradas (stored generated columns) e criar índices especializados sobre JSONB. Normalizar é ideal quando o campo é consultado frequentemente com semântica relacional. Colunas geradas são um meio-termo: mantém JSONB mas expõe partes para indexação. Índices GIN e pg_trgm servem quando a flexibilidade do JSONB deve ser preservada e as buscas são sobre conteúdo textual ou estrutura de objetos.
Dica: escolha normalização quando a consistência de modelo e integridade referencial importam; escolha JSONB+índice quando a flexibilidade é prioridade e as consultas são textuais ou por contenção.
Fundamentos técnicos
Como o PostgreSQL indexa JSONB
PostgreSQL oferece suporte nativo a JSONB e cria dois tipos relevantes de índices para esse tipo: índices GIN com jsonb_ops/jsonb_path_ops para acelerar operações de @> (containment) e índices de expressão (expression indexes) sobre valores extraídos do JSONB. jsonb_path_ops é mais compacto e rápido para consultas de contenção, mas não suporta todas as operações. Para buscas textuais (substring, LIKE), precisamos de pg_trgm, que opera sobre texto — logo, aplicamos trigram em expressões que extraem strings do JSONB.
pg_trgm e operator classes
A extensão pg_trgm introduz operadores e funções como similarity() e classes de índice gin_trgm_ops. Para usar trigrams em campos JSONB, criamos índices sobre expressões que convertem partes do JSONB em texto: por exemplo (data->>'title') gin_trgm_ops. Sem especificar a classe de operador certa, o índice GIN não será usado para operações LIKE/ILIKE ou funções de similaridade.
Exemplo 1: caso simples
Contexto do exemplo
Vamos modelar uma tabela articles com uma coluna meta JSONB que guarda título, tags e descrição. Objetivo: pesquisar por substring no título e filtrar por tag usando @>. O exemplo mostra criação de extensão, tabela, índices e consultas com EXPLAIN ANALYZE.
Criação da tabela e índices (SQL)
O bloco abaixo cria a extensão, tabela, popula alguns registros, e adiciona dois índices: um GIN padrão para contenção JSONB e um índice GIN de trigram sobre o título extraído.
-- Cria extensão pg_trgm (precisa de superuser ou owner com privilégio adequado)
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Tabela com coluna JSONB
CREATE TABLE articles (
id serial PRIMARY KEY,
meta jsonb NOT NULL
);
-- Índice GIN para consultas por contenção (@>)
CREATE INDEX idx_articles_meta_gin ON articles USING gin (meta jsonb_path_ops);
-- Índice trigram sobre expressão que extrai title como texto
CREATE INDEX idx_articles_meta_title_trgm ON articles USING gin ((meta ->> 'title') gin_trgm_ops);
-- Insere alguns dados
INSERT INTO articles (meta) VALUES
('{"title":"Performance tuning in PostgreSQL","tags":["postgres","perf"],"body":"..."}'),
('{"title":"JSONB indexing patterns","tags":["postgres","jsonb"],"body":"..."}'),
('{"title":"Prisma and PostgreSQL integrations","tags":["prisma","nodejs"],"body":"..."}');
Depois de criar os índices, as consultas por contenção e por título com ILIKE podem usar índices. Veja o exemplo de consulta com explicação.
-- Busca por tag usando contenção (usa idx_articles_meta_gin)
EXPLAIN ANALYZE
SELECT id FROM articles WHERE meta @> '{"tags": ["postgres"]}';
-- Busca por substring no title (deve usar idx_articles_meta_title_trgm)
EXPLAIN ANALYZE
SELECT id FROM articles WHERE (meta ->> 'title') ILIKE '%postgres%';
O primeiro EXPLAIN deve mostrar uso de GIN Index Scan com jsonb_path_ops; o segundo mostrará Bitmap Index Scan ou Index Scan usando o índice trigram se o plano estimar que ele é seletivo.
Exemplo 2: caso avancado do mundo real
Requisitos reais e abordagem
Suponha um sistema de buscas para um marketplace onde anúncios têm um JSONB com campos dinâmicos: título, descrição, atributos (array de pares), localização e histórico. Requisitos: (1) pesquisa por texto parcial no título/description, (2) filtragem por atributos (containment), (3) ordenação por relevância (similarity), (4) compatibilidade com Prisma como ORM.
Modelagem no Prisma e migração com SQL manual
Prisma não expõe todos os tipos de índices complexos no schema. Solução prática: declarar o campo JSON na model, gerar migrations e, em seguida, adicionar um migration SQL manual que cria extensões/índices GIN/pg_trgm. Abaixo um exemplo de schema.prisma e o SQL a ser inserido na migration.
// schema.prisma (fragmento)
model Listing {
id Int @id @default(autoincrement())
data Json @db.JsonB
}
Depois de gerar a migration (prisma migrate dev --name init), edite o arquivo SQL gerado e adicione:
-- SQL na migration: cria ext e índices especializados
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_listing_data_gin
ON "Listing" USING gin (data jsonb_path_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_listing_title_trgm
ON "Listing" USING gin ((data ->> 'title') gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_listing_description_trgm
ON "Listing" USING gin ((data ->> 'description') gin_trgm_ops);
Usar CONCURRENTLY evita locking que bloqueia writes em tabelas grandes. Após a migration, consultas complexas podem ser executadas por Prisma via raw queries ou combinando filtros JSON se simples.
Consultas com Prisma usando $queryRaw e parâmetros
Para aproveitar similaridade e evitar injeção, use parâmetros preparados com Prisma.$queryRaw. Exemplo de busca por texto que combina containment por atributo e similaridade no título:
// Node.js com Prisma Client
const search = '%bike%';
const attributeFilter = '{"attributes": [{"key": "condition", "value": "new"}]}'
const rows = await prisma.$queryRaw(
`SELECT id, data,
similarity(data ->> 'title', $1) AS sim
FROM "Listing"
WHERE data @> $2::jsonb
AND (data ->> 'title') ILIKE $1
ORDER BY sim DESC
LIMIT 20`,
search,
attributeFilter
);
Esse padrão usa o índice GIN para contenção e o índice trigram para a busca por título. A ordenação por similaridade usa a função do pg_trgm.
Debugging e troubleshooting
EXPLAIN ANALYZE: interpretar planos e identificar ausência de índice
Quando uma consulta não usa o índice esperado, rode EXPLAIN (ANALYZE, BUFFERS, VERBOSE) para entender o plano. Procure por Sequential Scan ou Filter que utiliza operadores não sargable (não indexáveis). Se o filtro usa funções não indexáveis ou casting implícito, o índice pode ser ignorado.
Erros comuns e armadilhas
Listo abaixo problemas práticos que impedem uso eficiente de índices:
- Extensão não instalada: sem pg_trgm, índices trigram não existem.
- Operador incorreto: usar jsonb_ops quando você precisa de jsonb_path_ops para containment específico, ou esquecer gin_trgm_ops na expressão trigram.
- Comparações de tipo: usar casting inadequado faz o planner evitar o índice; por exemplo, comparar JSONB com string sem cast explícito.
- Tabelas pequenas: planner pode preferir seq scan se estimação de custo achata; índices só ajudam com seletividade adequada.
- Concorrência no CREATE INDEX: esquecer CONCURRENTLY em produção pode bloquear writes longamente.
Se o índice não é usado, verifique a operação exata (ILIKE vs LIKE vs similarity) e a operator class usada no índice; às vezes é necessário recriar o índice com a classe correta.
Soluções práticas para problemas específicos
Se a consulta usa funções no lado esquerdo (por ex. lower(meta->>'title') ILIKE ...), crie um índice sobre a mesma expressão (lower(meta->>'title') gin_trgm_ops). Para ordens por similaridade, crie índices trigram; para filtros @>, crie GIN com jsonb_path_ops. Se a consulta continua lenta, atualize estatísticas com ANALYZE e considere REINDEX ou aumentar work_mem temporariamente para construir índices.
Conclusao
Indexar JSONB requer escolher o índice certo para o tipo de consulta: GIN jsonb_path_ops para contenção e GIN + pg_trgm sobre expressões de texto para buscas por substring e similaridade. No ecossistema Prisma, o caminho prático é criar o campo JSONB na model e adicionar índices via migrations SQL manuais (ou criar via runtime com cuidado). Sempre valide com EXPLAIN ANALYZE, ative pg_trgm quando necessário e prefira CREATE INDEX CONCURRENTLY em produção.
Resumo das ações imediatas recomendadas:
- Identifique padrões de consulta (containment vs busca textual).
- Crie
jsonb_path_opspara @> egin_trgm_opspara textos extraídos. - Adicione índices via migration SQL para manter controle de versão.
- Use Prisma.$queryRaw para consultas complexas e parametrizadas quando necessário.
Essas práticas evitam varreduras inteiras na tabela, reduzem latência e mantêm a flexibilidade do JSONB. Em sistemas com alto volume, combine normalização e JSONB corretamente: normalize o que precisa de integridade, mantenha JSONB para campos semi-estruturados e ofereça índices para as consultas críticas.
Se precisar, posso fornecer um script completo de migration pronto para aplicar em um banco existente, com checks e fallback para ambientes que não suportam CONCURRENTLY.
Assuntos: PostgreSQL, Prisma, jsonb, gin, pg_trgm
Leia também
Usando INSERT ... ON CONFLICT (UPSERT) no PostgreSQL com exemplos em Prisma
Aprenda a usar INSERT ... ON CONFLICT (UPSERT) no PostgreSQL com exemplos práticos em Prisma. Entenda conceitos, armadilhas comuns e duas abordagens…
Interpretando EXPLAIN ANALYZE no PostgreSQL: guia prático para iniciantes
Guia prático para iniciantes sobre EXPLAIN ANALYZE no PostgreSQL: aprenda a rodar, interpretar planos, identificar problemas (seq scan, nested loop) e aplicar…
Migrações zero-downtime no PostgreSQL com Prisma: blue-green e backfill
Guia prático para executar migrações zero-downtime em PostgreSQL com Prisma. Cobertura de padrões expand-fill-contract, blue-green deploy, backfill em…
UUID vs SERIAL vs ULID no PostgreSQL: escolher e usar com Prisma
Comparativo prático entre UUID, SERIAL e ULID em PostgreSQL com exemplos e mapeamentos no Prisma. Guia passo a passo, comandos, scripts Node.js e dicas…
Transações no PostgreSQL com Prisma: commit, rollback e locks passo a passo
Guia prático e passo a passo de transacoes no Postgres com Prisma: como usar commit, rollback e locks, exemplos com transferencia entre contas, SELECT FOR…
Migrando um projeto Node + Prisma de SQLite para PostgreSQL: guia passo a passo para iniciantes
Guia prático para migrar um projeto Node + Prisma de SQLite para PostgreSQL. Aprenda conceitos, ajustes no schema, comandos de migração, importação de dados e…