PostgreSQL: Manipulação de Dados JSON e JSONB no PostgreSQL
Última atualização: 2026-08-26
1. O Que Você Vai Aprender
- Entender a diferença entre JSON e JSONB e as vantagens do JSONB
- Usar operadores JSON para extrair e filtrar dados
- Usar funções JSONB para consultar, modificar e gerar dados JSON
- Criar índices GIN para acelerar consultas JSONB
- Usar JSONPATH (padrão SQL/JSON) para consultas complexas
- Padrões de design híbrido combinando JSONB com dados relacionais
2. A História
Bob é engenheiro de back-end em uma plataforma SaaS. A plataforma precisa armazenar configurações de usuário e atributos de produto, mas esses campos diferem para cada cliente:
- A configuração de usuário do Cliente A tem
theme,language,notifications - A configuração de usuário do Cliente B tem
timezone,currency,dashboard_layout - Os atributos de produto variam ainda mais: roupas têm
size/color, eletrônicos têmwarranty/voltage
Com um modelo relacional tradicional, cada novo campo exigiria um ALTER TABLE. Bob escolhe armazenar esses campos dinâmicos em uma única tabela usando JSONB — flexível e eficiente.
3. Conceito: JSON vs JSONB
(1) Comparação dos Dois Tipos JSON
| Dimensão | JSON | JSONB |
|---|---|---|
| Armazenamento | Armazenado como texto, preservado literalmente | Armazenado como binário, analisado e depois armazenado |
| Velocidade de escrita | Mais rápida (sem análise) | Mais lenta (precisa de análise e conversão) |
| Velocidade de consulta | Mais lenta (analisado a cada consulta) | Muito rápida (já analisado em árvore) |
| Suporte a índice | Sem índice nativo | Suporta índice GIN |
| Espaços em branco/ordem | Preserva espaços em branco e ordem das chaves originais | Não preservado; chaves ordenadas alfabeticamente |
| Chaves duplicadas | Todas as chaves duplicadas mantidas | Apenas o último valor mantido |
| Recomendado para | Apenas armazenamento, sem consulta | A grande maioria dos casos |
▶ Exemplo: JSON Preserva Espaços, JSONB Não
SELECT '{"name": "Alice", "age": 30}'::json;
-- {"name": "Alice", "age": 30}
SELECT '{"name": "Alice", "age": 30}'::jsonb;
-- {"age": 30, "name": "Alice"}
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Criar uma Tabela com Coluna JSONB
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
username TEXT NOT NULL,
profile JSONB NOT NULL DEFAULT '{}'
);
INSERT INTO users (username, profile) VALUES
('alice', '{"theme": "dark", "language": "en", "notifications": true}'),
('bob', '{"timezone": "UTC-5", "currency": "USD", "dashboard_layout": "grid"}');
Output:
INSERT 0 1
4. Conceito: Operadores JSON
(1) Operadores Básicos de Extração
| Operador | Operando direito | Tipo de retorno | Descrição | Exemplo |
|---|---|---|---|---|
-> |
int | JSON/JSONB | Elemento do array por índice | '[1,2,3]'::jsonb -> 1 → 2 |
-> |
text | JSON/JSONB | Valor do objeto por chave | '{"a":1}'::jsonb -> 'a' → 1 |
->> |
int | text | Elemento do array por índice (texto) | '[1,2,3]'::jsonb ->> 1 → "2" |
->> |
text | text | Valor do objeto por chave (texto) | '{"a":1}'::jsonb ->> 'a' → "1" |
▶ Exemplo: Extrair Campos Aninhados
SELECT profile -> 'theme' AS theme_json,
profile ->> 'theme' AS theme_text
FROM users
WHERE username = 'alice';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(2) Operadores de Extração por Caminho
| Operador | Operando direito | Tipo de retorno | Descrição |
|---|---|---|---|
#> |
text[] | JSON/JSONB | Valor por caminho (formato JSON) |
#>> |
text[] | text | Valor por caminho (formato texto) |
▶ Exemplo: Extração por Caminho
SELECT profile #> '{address,city}' AS city_json,
profile #>> '{address,city}' AS city_text
FROM users
WHERE profile ? 'address';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(3) Operadores de Contenção e Existência (Apenas JSONB)
| Operador | Descrição | Exemplo |
|---|---|---|
@> |
Se o esquerdo contém o direito | '{"a":1,"b":2}'::jsonb @> '{"a":1}' → true |
<@ |
Se o esquerdo está contido no direito | '{"a":1}'::jsonb <@ '{"a":1,"b":2}' → true |
? |
Se a chave existe | '{"a":1}'::jsonb ? 'a' → true |
| `? | ` | Se alguma chave existe |
?& |
Se todas as chaves existem | '{"a":1}'::jsonb ?& array['a','b'] → false |
▶ Exemplo: Consulta de Contenção — Encontrar Todos os Usuários com Tema Escuro Ativado
SELECT username, profile
FROM users
WHERE profile @> '{"theme": "dark"}';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Consulta de Existência de Chave — Encontrar Usuários que Configuraram Fuso Horário
SELECT username, profile ->> 'timezone' AS tz
FROM users
WHERE profile ? 'timezone';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Consulta de Múltiplas Chaves — Encontrar Usuários que Configuraram Fuso Horário ou Moeda
SELECT username
FROM users
WHERE profile ?| array['timezone', 'currency'];
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
5. Conceito: Funções JSONB
(1) Funções de Consulta e Extração
| Função | Tipo de retorno | Descrição |
|---|---|---|
jsonb_path_query(data, path) |
setof jsonb | Consulta por JSONPATH, retorna todas as correspondências |
jsonb_array_elements(data) |
setof jsonb | Expande array em conjunto de linhas |
jsonb_each(data) |
setof (key, value) | Expande objeto em pares chave-valor |
jsonb_object_keys(data) |
setof text | Retorna todas as chaves de nível superior |
jsonb_typeof(data) |
text | Retorna o tipo de um valor JSON |
▶ Exemplo: Expandir um Array JSON em Linhas
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
attributes JSONB NOT NULL DEFAULT '{}'
);
INSERT INTO products (name, attributes) VALUES
('T-Shirt', '{"colors": ["red", "blue", "green"], "sizes": ["S", "M", "L"]}'),
('Laptop', '{"colors": ["silver", "black"], "warranty_years": 2}');
SELECT product_id, name,
jsonb_array_elements_text(attributes -> 'colors') AS color
FROM products;
Output:
INSERT 0 1
▶ Exemplo: Expandir um Objeto em Pares Chave-Valor
SELECT username,
(jsonb_each(profile)).key AS config_key,
(jsonb_each(profile)).value AS config_value
FROM users;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(2) Funções de Modificação
| Função | Descrição |
|---|---|
jsonb_set(target, path, new_value) |
Define o valor em um caminho especificado |
jsonb_insert(target, path, new_value [, before]) |
Insere um novo valor em um caminho especificado |
target - key |
Remove uma chave de nível superior |
target - path_array |
Remove um caminho especificado |
jsonb_pretty(data) |
Saída formatada de forma legível |
▶ Exemplo: Modificar Configuração de Usuário
-- Adiciona ou atualiza um campo
UPDATE users
SET profile = jsonb_set(profile, '{language}', '"zh"')
WHERE username = 'alice';
-- Adiciona um campo aninhado
UPDATE users
SET profile = jsonb_set(profile, '{address,city}', '"New York"')
WHERE username = 'alice';
-- Remove um campo
UPDATE users
SET profile = profile - 'notifications'
WHERE username = 'alice';
Output:
-- Comando SQL executado com sucesso
▶ Exemplo: Anexar um Elemento a um Array JSON
-- Anexa ao final (o caminho deve apontar para um array existente, insere após o último elemento)
UPDATE products
SET attributes = jsonb_set(
attributes, '{colors}',
(attributes -> 'colors') || '"yellow"'
)
WHERE name = 'T-Shirt';
Output:
INSERT 0 1
▶ Exemplo: Saída Formatada de JSON
SELECT jsonb_pretty(profile) FROM users WHERE username = 'alice';
{
"theme": "dark",
"language": "zh",
"address": {
"city": "New York"
}
}
6. Conceito: Índices JSONB
(1) Índice GIN Acelera Consultas JSONB
| Tipo de índice GIN | Operadores suportados | Descrição |
|---|---|---|
jsonb_ops (padrão) |
@> ? `? |
?&` |
jsonb_path_ops |
@> |
Índice menor e mais rápido — suporta apenas consultas de contenção |
▶ Exemplo: Criar um Índice GIN
-- Índice GIN padrão (suporta @>, ?, ?|, ?&)
CREATE INDEX idx_users_profile ON users USING gin (profile);
-- Índice GIN path ops (menor, mais rápido apenas para @>)
CREATE INDEX idx_users_profile_path ON users USING gin (profile jsonb_path_ops);
Output:
CREATE TABLE
▶ Exemplo: Comparar Desempenho de Consulta Com e Sem Índice
-- Sem índice: varredura sequencial
EXPLAIN ANALYZE
SELECT * FROM users WHERE profile @> '{"theme": "dark"}';
-- Após criar índice GIN: varredura de índice bitmap
CREATE INDEX idx_users_profile ON users USING gin (profile);
EXPLAIN ANALYZE
SELECT * FROM users WHERE profile @> '{"theme": "dark"}';
Output:
CREATE TABLE
| Método de consulta | Usa índice GIN? | Descrição |
|---|---|---|
profile @> '{"theme":"dark"}' |
Sim | Consulta de contenção — melhor caso do GIN |
profile ->> 'theme' = 'dark' |
Não | Extração e depois comparação — precisa de índice de expressão B-tree |
profile ? 'theme' |
Sim | Consulta de existência de chave |
▶ Exemplo: Índice de Expressão B-tree Acelera Consultas de Extração
-- Para consultas usando o operador ->>
CREATE INDEX idx_users_theme ON users ((profile ->> 'theme'));
SELECT * FROM users WHERE profile ->> 'theme' = 'dark'; -- Usa índice
Output:
CREATE TABLE
7. Conceito: JSONPATH (Padrão SQL/JSON)
(1) Sintaxe JSONPATH
O PostgreSQL 12+ suporta o padrão SQL/JSON JSONPATH, semelhante ao XPath, para consultas JSON complexas.
| Sintaxe | Descrição | Exemplo |
|---|---|---|
$.key |
Chave do objeto raiz | $.theme |
$.array[*] |
Itera sobre array | $.colors[*] |
$.nested.key |
Acesso aninhado | $.address.city |
? (condition) |
Filtro | $.items[*] ? (@.price > 100) |
@ |
Elemento atual | @.name |
▶ Exemplo: Consultar com jsonb_path_query
SELECT jsonb_path_query(profile, '$.theme') AS theme
FROM users
WHERE username = 'alice';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Consulta JSONPATH com Condição de Filtro
-- Produtos com garantia > 1 ano
SELECT name,
jsonb_path_query(attributes, '$.warranty_years') AS warranty
FROM products
WHERE jsonb_path_exists(attributes, '$.warranty_years ? (@ > 1)');
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Iterar Sobre Elementos do Array
SELECT name,
jsonb_path_query(attributes, '$.colors[*]') AS color
FROM products;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| Função | Tipo de retorno | Descrição |
|---|---|---|
jsonb_path_query(data, path) |
setof jsonb | Retorna todas as correspondências |
jsonb_path_query_array(data, path) |
jsonb | Retorna correspondências como array JSON |
jsonb_path_query_first(data, path) |
jsonb | Retorna a primeira correspondência |
jsonb_path_exists(data, path) |
booleano | Se existe alguma correspondência |
8. Conceito: Design Híbrido com JSONB e Dados Relacionais
(1) Quando Usar JSONB, Quando Usar Colunas Relacionais
| Cenário | Recomendado | Motivo |
|---|---|---|
| Campos frequentemente consultados/ordenados/unidos | Coluna relacional + índice B-tree | Melhor desempenho |
| Campos com estrutura fixa que participam da lógica de negócios | Coluna relacional | Tipo seguro, restrições completas |
| Campos cuja estrutura varia por cliente | JSONB + índice GIN | Flexível, sem necessidade de ALTER TABLE |
| Consultas ocasionais de informações suplementares | JSONB | Não polui a estrutura da tabela principal |
| Campos dinâmicos que precisam de restrições de tipo precisas | JSONB + restrição CHECK | Equilibra flexibilidade e segurança |
▶ Exemplo: Design Híbrido — Tabela de Produtos
CREATE TABLE products_v2 (
product_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL, -- Coluna fixa, indexada
price NUMERIC(10,2) NOT NULL, -- Coluna fixa, indexada
stock INT NOT NULL DEFAULT 0, -- Coluna fixa, indexada
attributes JSONB NOT NULL DEFAULT '{}', -- Atributos dinâmicos
metadata JSONB DEFAULT '{}' -- Metadados raramente consultados
);
CREATE INDEX idx_products_category ON products_v2 (category);
CREATE INDEX idx_products_attrs ON products_v2 USING gin (attributes jsonb_path_ops);
Output:
CREATE TABLE
▶ Exemplo: Restrição CHECK JSONB Garante Qualidade dos Dados
ALTER TABLE products_v2
ADD CONSTRAINT chk_attributes_schema
CHECK (
jsonb_typeof(attributes -> 'colors') = 'array'
AND attributes ? 'colors'
);
Output:
-- Comando SQL executado com sucesso
▶ Exemplo: Consulta de Junção com JSONB
-- Encontra pedidos onde o produto tem um atributo específico
SELECT o.order_id, o.customer_id, p.name
FROM orders o
JOIN products_v2 p ON o.product_id = p.product_id
WHERE p.attributes @> '{"warranty_years": 2}';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
9. Fluxo de Armazenamento e Consulta JSON
flowchart TD
A[Entrada de Texto JSON] --> B{Tipo Alvo?}
B -->|json| C[Armazenar como está<br/>Sem sobrecarga de análise]
B -->|jsonb| D[Analisar e Converter<br/>para árvore binária]
D --> E[Armazenar como JSONB<br/>Chaves ordenadas, sem espaços]
E --> F{Tipo de Consulta?}
F -->|@> contém| G[Varredura de Índice GIN<br/>Caminho rápido]
F -->|->> extrair + comparar| H[Índice de Expressão B-tree<br/>ou Varredura Sequencial]
F -->|jsonpath| I[Motor JSONPATH<br/>PG 12+]
G --> J[Retornar Resultados]
H --> J
I --> J
C --> K[Analisar a cada consulta<br/>Lento, sem índice]
K --> J
10. Prática: Sistema de Configuração de Usuário e Atributos de Produto de Plataforma SaaS
Bob precisa implementar uma solução completa de armazenamento de dados para plataforma SaaS, suportando configuração flexível de usuário e gerenciamento de atributos de produto.
-- Passo 1: Criar tabelas principais com JSONB
CREATE TABLE saas_users (
user_id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
plan TEXT NOT NULL DEFAULT 'free',
config JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP DEFAULT now()
);
CREATE TABLE saas_products (
product_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
price NUMERIC(10,2) NOT NULL,
specs JSONB NOT NULL DEFAULT '{}',
tags JSONB NOT NULL DEFAULT '[]'
);
-- Passo 2: Inserir dados de exemplo
INSERT INTO saas_users (email, name, plan, config) VALUES
('alice@corp.com', 'Alice', 'pro',
'{"theme":"dark","language":"en","notifications":{"email":true,"sms":false},"sidebar":["dashboard","reports"]}'),
('bob@corp.com', 'Bob', 'enterprise',
'{"theme":"light","language":"zh","notifications":{"email":true,"sms":true},"sidebar":["dashboard","admin","billing"]}');
INSERT INTO saas_products (name, category, price, specs, tags) VALUES
('Pro Widget', 'widget', 49.99,
'{"weight_kg":0.5,"colors":["red","blue"],"warranty_years":3}',
'["popular","new"]'),
('Mega Gadget', 'gadget', 199.99,
'{"weight_kg":2.0,"colors":["silver","black"],"voltage":"220V"}',
'["premium","bestseller"]');
-- Passo 3: Criar índices
CREATE INDEX idx_saas_users_config ON saas_users USING gin (config);
CREATE INDEX idx_saas_products_specs ON saas_products USING gin (specs jsonb_path_ops);
CREATE INDEX idx_saas_products_tags ON saas_products USING gin (tags);
CREATE INDEX idx_saas_products_category ON saas_products (category);
-- Passo 4: Exemplos de consulta
-- Encontra usuários com notificações por email ativadas
SELECT name, config ->> 'theme' AS theme
FROM saas_users
WHERE config @> '{"notifications":{"email":true}}';
-- Encontra produtos disponíveis em vermelho
SELECT name, price
FROM saas_products
WHERE specs -> 'colors' @> '["red"]';
-- Encontra produtos com tags específicas
SELECT name
FROM saas_products
WHERE tags @> '["premium"]';
-- Atualiza configuração de usuário (adiciona novo campo)
UPDATE saas_users
SET config = jsonb_set(config, '{timezone}', '"America/New_York"')
WHERE email = 'alice@corp.com';
-- Remove um campo de configuração
UPDATE saas_users
SET config = config - 'language'
WHERE email = 'bob@corp.com';
-- Expande tags de produtos para análise
SELECT name, jsonb_array_elements_text(tags) AS tag
FROM saas_products;
-- Formata a configuração do usuário de forma legível
SELECT name, jsonb_pretty(config) FROM saas_users WHERE plan = 'pro';
❓ Perguntas Frequentes
P: Qual devo escolher, JSON ou JSONB? R: Para a grande maioria dos casos, escolha JSONB. JSONB consulta mais rápido, suporta índices e tem operadores mais ricos. Use JSON apenas quando precisar preservar o formato de texto original (espaços em branco, ordem das chaves, chaves duplicadas) ou para apenas armazenamento sem consulta.
P: JSONB pode substituir tabelas relacionais? R: Não completamente. Campos frequentemente consultados/ordenados/unidos devem usar colunas relacionais. JSONB é adequado para dados suplementares de estrutura variável; um design híbrido é a melhor prática.
P: Qual a diferença entre jsonb_set e jsonb_insert? R: jsonb_set substitui o valor em um caminho existente, criando-o se o caminho não existir. jsonb_insert insere um novo elemento em uma posição especificada em um array (o parâmetro before controla início/fim) e não substitui se a chave já existir.
P: Como escolher entre um índice GIN e um índice de expressão B-tree? R: Use GIN para consultas de contenção (@>) e índice de expressão B-tree para consultas de igualdade (->> 'key' = 'value'). Os dois podem coexistir, cobrindo diferentes padrões de consulta.
P: Como escolher entre JSONPATH e operadores tradicionais? R: Operadores são mais concisos para consultas simples; JSONPATH é mais poderoso para consultas aninhadas complexas e condições de filtro. JSONPATH é o padrão SQL/JSON, portanto é mais portável.
P: Um campo JSONB pode ter uma restrição CHECK? R: Sim. Use jsonb_typeof(), o operador ?, etc. em uma restrição CHECK para validar a estrutura JSONB — por exemplo CHECK (jsonb_typeof(attributes -> 'colors') = 'array').
P: Grandes quantidades de atualizações JSONB causam problemas de desempenho? R: Sim. Uma atualização JSONB substitui o valor inteiro (MVCC cria uma nova versão) e atualizar frequentemente valores JSONB grandes produz muitas tuplas mortas. Recomenda-se dividir campos frequentemente atualizados em colunas relacionais e deixar JSONB armazenar dados suplementares de baixa frequência de atualização.
📖 Resumo
- JSONB é JSON armazenado em binário — consulta mais rápido, suporta índices — recomendado como escolha padrão
->retorna tipo JSON,->>retorna tipo texto,#>/#>>extraem por caminho- O operador de contenção
@>combinado com um índice GIN é a melhor combinação para consultas JSONB jsonb_set/jsonb_insert/-lidam com adicionar/modificar/remover;jsonb_array_elements/jsonb_eachlidam com expansão- JSONPATH (PG 12+) fornece capacidades de consulta complexa do padrão SQL/JSON
- Design híbrido: campos fixos usam colunas relacionais, campos dinâmicos usam JSONB, com restrições CHECK para garantir qualidade
📝 Exercícios
-
⭐ Crie uma tabela
app_settingscom colunasapp_name(TEXT) esettings(JSONB); insira duas linhas e depois use->>para consultar o valor de um item de configuração. -
⭐⭐ Crie um índice GIN na tabela
saas_products; escreva uma consulta que encontre os nomes de todos os produtos cujospecstenhawarranty_years > 2e usejsonb_prettypara formatar ospecsde forma legível. -
⭐⭐⭐ Projete um esquema híbrido JSONB para uma tabela de pedidos: colunas fixas para
order_id/customer_id/total_amount/status/created_ate uma coluna JSONBextraarmazenando informações de cupom (coupon_code/discount_percent) e notas de entrega (delivery_notes). Escreva: inserir um pedido comextra, usar@>para encontrar pedidos que usaram um cupom específico e usarjsonb_setpara anexargift_wrap: truea um pedido existente.