PostgreSQL: Manipulação de Dados JSON e JSONB no PostgreSQL

Última atualização: 2026-08-26

1. O Que Você Vai Aprender


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:

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

SQL
SELECT '{"name": "Alice", "age": 30}'::json;
-- {"name": "Alice", "age": 30}

SELECT '{"name": "Alice", "age": 30}'::jsonb;
-- {"age": 30, "name": "Alice"}

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: Criar uma Tabela com Coluna JSONB

SQL
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:

TEXT 📖 Somente leitura
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 -> 12
-> 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

SQL
SELECT profile -> 'theme' AS theme_json,
       profile ->> 'theme' AS theme_text
FROM users
WHERE username = 'alice';

Output:

TEXT 📖 Somente leitura
 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

SQL
SELECT profile #> '{address,city}' AS city_json,
       profile #>> '{address,city}' AS city_text
FROM users
WHERE profile ? 'address';

Output:

TEXT 📖 Somente leitura
 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

SQL
SELECT username, profile
FROM users
WHERE profile @> '{"theme": "dark"}';

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: Consulta de Existência de Chave — Encontrar Usuários que Configuraram Fuso Horário

SQL
SELECT username, profile ->> 'timezone' AS tz
FROM users
WHERE profile ? 'timezone';

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: Consulta de Múltiplas Chaves — Encontrar Usuários que Configuraram Fuso Horário ou Moeda

SQL
SELECT username
FROM users
WHERE profile ?| array['timezone', 'currency'];

Output:

TEXT 📖 Somente leitura
 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

SQL
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:

TEXT 📖 Somente leitura
INSERT 0 1

▶ Exemplo: Expandir um Objeto em Pares Chave-Valor

SQL
SELECT username,
       (jsonb_each(profile)).key AS config_key,
       (jsonb_each(profile)).value AS config_value
FROM users;

Output:

TEXT 📖 Somente leitura
 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

SQL
-- 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:

TEXT 📖 Somente leitura
-- Comando SQL executado com sucesso

▶ Exemplo: Anexar um Elemento a um Array JSON

SQL
-- 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:

TEXT 📖 Somente leitura
INSERT 0 1

▶ Exemplo: Saída Formatada de JSON

SQL
SELECT jsonb_pretty(profile) FROM users WHERE username = 'alice';
TEXT 📖 Somente leitura
{
    "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

SQL
-- Í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:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Comparar Desempenho de Consulta Com e Sem Índice

SQL
-- 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:

TEXT 📖 Somente leitura
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

SQL
-- Para consultas usando o operador ->>
CREATE INDEX idx_users_theme ON users ((profile ->> 'theme'));
SELECT * FROM users WHERE profile ->> 'theme' = 'dark'; -- Usa índice

Output:

TEXT 📖 Somente leitura
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

SQL
SELECT jsonb_path_query(profile, '$.theme') AS theme
FROM users
WHERE username = 'alice';

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: Consulta JSONPATH com Condição de Filtro

SQL
-- 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:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: Iterar Sobre Elementos do Array

SQL
SELECT name,
       jsonb_path_query(attributes, '$.colors[*]') AS color
FROM products;

Output:

TEXT 📖 Somente leitura
 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

SQL
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:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Restrição CHECK JSONB Garante Qualidade dos Dados

SQL
ALTER TABLE products_v2
ADD CONSTRAINT chk_attributes_schema
CHECK (
  jsonb_typeof(attributes -> 'colors') = 'array'
  AND attributes ? 'colors'
);

Output:

TEXT 📖 Somente leitura
-- Comando SQL executado com sucesso

▶ Exemplo: Consulta de Junção com JSONB

SQL
-- 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:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

9. Fluxo de Armazenamento e Consulta JSON

100%
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.

SQL
-- 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


📝 Exercícios

  1. ⭐ Crie uma tabela app_settings com colunas app_name (TEXT) e settings (JSONB); insira duas linhas e depois use ->> para consultar o valor de um item de configuração.

  2. ⭐⭐ Crie um índice GIN na tabela saas_products; escreva uma consulta que encontre os nomes de todos os produtos cujo specs tenha warranty_years > 2 e use jsonb_pretty para formatar o specs de forma legível.

  3. ⭐⭐⭐ Projete um esquema híbrido JSONB para uma tabela de pedidos: colunas fixas para order_id/customer_id/total_amount/status/created_at e uma coluna JSONB extra armazenando informações de cupom (coupon_code/discount_percent) e notas de entrega (delivery_notes). Escreva: inserir um pedido com extra, usar @> para encontrar pedidos que usaram um cupom específico e usar jsonb_set para anexar gift_wrap: true a um pedido existente.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%