Tipos especiais do PostgreSQL: JSONB, Arrays, UUID

1. Introdução aos tipos especiais do PostgreSQL

O PostgreSQL se destaca no mundo dos bancos de dados relacionais por oferecer tipos de dados que vão muito além do tradicional INTEGER, VARCHAR e DATE. Entre esses tipos especiais, três se destacam pela utilidade e flexibilidade: JSONB, Arrays e UUID. Enquanto um banco relacional puro exige que cada dado seja rigidamente estruturado em colunas e tabelas normalizadas, o PostgreSQL permite armazenar dados semiestruturados, coleções ordenadas e identificadores globalmente únicos com suporte nativo.

A escolha entre usar esses tipos especiais ou os convencionais depende do cenário. JSONB é ideal quando você precisa de flexibilidade de esquema — por exemplo, metadados que variam entre registros. Arrays são úteis para relacionamentos muitos-para-muitos simples ou tags, sem a necessidade de tabelas intermediárias. UUID é a escolha natural para sistemas distribuídos, onde chaves primárias precisam ser únicas globalmente, sem depender de sequências centralizadas.

O impacto no desempenho é significativo: JSONB com índices GIN pode ser tão rápido quanto colunas tradicionais para consultas específicas; Arrays permitem buscas eficientes com operadores como ANY; e UUID, embora maior que inteiros (16 bytes vs. 4 ou 8 bytes), evita contenção de locks em ambientes concorrentes.

2. JSONB: Armazenamento e manipulação de dados semiestruturados

JSONB (JavaScript Object Notation Binary) é a versão binária e indexável do tipo JSON no PostgreSQL. Diferentemente do tipo JSON, que armazena o texto exato e precisa ser reparsed a cada consulta, o JSONB armazena os dados em formato binário decomposto, permitindo indexação e operações eficientes sem reparse.

Operadores essenciais

-- Operador -> : acessa campo como JSON (mantém tipo)
SELECT '{"nome": "João", "idade": 30}'::jsonb -> 'nome';
-- Resultado: "João"

-- Operador ->> : acessa campo como texto
SELECT '{"nome": "João", "idade": 30}'::jsonb ->> 'nome';
-- Resultado: João

-- Operador #> : caminho como array (retorna JSON)
SELECT '{"endereco": {"cidade": "SP"}}'::jsonb #> '{endereco, cidade}';
-- Resultado: "SP"

-- Operador @> : contém (verifica se documento contém subconjunto)
SELECT '{"a":1, "b":2}'::jsonb @> '{"a":1}'::jsonb;
-- Resultado: true

-- Operador ? : existe chave
SELECT '{"a":1, "b":2}'::jsonb ? 'a';
-- Resultado: true

-- Operadores ?| e ?& : alguma ou todas as chaves existem
SELECT '{"a":1, "b":2}'::jsonb ?| ARRAY['a', 'c'];  -- true (a existe)
SELECT '{"a":1, "b":2}'::jsonb ?& ARRAY['a', 'b'];  -- true (ambas existem)

Índices GIN para JSONB

O índice GIN (Generalized Inverted Index) é a ferramenta principal para acelerar consultas em JSONB:

CREATE INDEX idx_dados_jsonb ON tabela USING GIN (dados jsonb_path_ops);

-- Consulta que se beneficia do índice:
SELECT * FROM tabela WHERE dados @> '{"status": "ativo"}';

O operador jsonb_path_ops reduz o tamanho do índice e acelera operações @>.

3. Arrays: Coleções ordenadas dentro de uma coluna

Arrays no PostgreSQL permitem armazenar múltiplos valores do mesmo tipo em uma única coluna, com suporte a multidimensionalidade.

Declaração e sintaxe

-- Array unidimensional
CREATE TABLE produtos (
    id SERIAL PRIMARY KEY,
    nome VARCHAR(100),
    tags TEXT[]  -- array de texto
);

-- Inserção
INSERT INTO produtos (nome, tags) VALUES
    ('Notebook', ARRAY['eletrônico', 'promoção', '2024']),
    ('Mouse', '{periférico, oferta}');

-- Array multidimensional
CREATE TABLE matriz (
    id SERIAL PRIMARY KEY,
    valores INTEGER[][]  -- matriz 2D
);

Operadores e funções

-- ANY: verifica se elemento existe no array
SELECT * FROM produtos WHERE 'eletrônico' = ANY(tags);

-- ALL: compara com todos os elementos
SELECT * FROM produtos WHERE '2024' = ALL(tags);  -- só se todas as tags forem '2024'

-- array_append: adiciona elemento
UPDATE produtos SET tags = array_append(tags, 'novo') WHERE id = 1;

-- array_cat: concatena arrays
SELECT array_cat(ARRAY[1,2], ARRAY[3,4]);

-- unnest: expande array em linhas
SELECT id, unnest(tags) AS tag FROM produtos;

Índices GIN para Arrays

CREATE INDEX idx_produtos_tags ON produtos USING GIN (tags);

-- Consulta eficiente com índice:
SELECT * FROM produtos WHERE tags @> ARRAY['eletrônico'];

O índice GIN em arrays acelera operadores como @> (contém), && (sobreposição) e ANY.

4. UUID: Identificadores globais únicos

UUID (Universally Unique Identifier) é um padrão de 128 bits (16 bytes) que gera identificadores únicos sem necessidade de coordenação centralizada.

Geração de UUID no PostgreSQL

A partir do PostgreSQL 13, a função gen_random_uuid() está disponível nativamente:

CREATE TABLE usuarios (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    nome VARCHAR(100),
    email VARCHAR(100) UNIQUE
);

INSERT INTO usuarios (nome, email) VALUES ('Maria', 'maria@email.com');
-- id gerado automaticamente: algo como 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

Para versões anteriores, é necessário instalar a extensão:

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
SELECT uuid_generate_v4();  -- UUID versão 4 (aleatório)

Desempenho de índices com UUID vs. inteiros sequenciais

UUIDs aleatórios (v4) causam fragmentação em índices B-tree porque não são sequenciais — cada inserção ocorre em posição aleatória da árvore. Isso pode degradar o desempenho em tabelas muito grandes. Alternativas:

  • UUID v1 (baseado em timestamp): mais sequencial, mas revela informações do servidor.
  • UUID v7 (timestamp com aleatoriedade): melhor equilíbrio, disponível via extensões.
  • Inteiros sequenciais (SERIAL ou IDENTITY): menores (4-8 bytes) e sequenciais, mas não adequados para sistemas distribuídos.
-- Comparação de tamanho:
SELECT pg_column_size(gen_random_uuid()) AS uuid_tamanho,  -- 16 bytes
       pg_column_size(1::bigint) AS bigint_tamanho;        -- 8 bytes

5. Casos de uso práticos e modelagem

JSONB para metadados flexíveis

CREATE TABLE logs_eventos (
    id UUID DEFAULT gen_random_uuid(),
    timestamp TIMESTAMPTZ DEFAULT NOW(),
    evento VARCHAR(50),
    metadados JSONB  -- campos variáveis por tipo de evento
);

-- Inserção de eventos com estruturas diferentes
INSERT INTO logs_eventos (evento, metadados) VALUES
    ('login', '{"usuario": "joao", "ip": "192.168.1.1", "navegador": "Chrome"}'),
    ('compra', '{"produto": "Notebook", "valor": 3500.00, "parcelas": 12}');

-- Consulta eficiente com índice
CREATE INDEX idx_logs_metadados ON logs_eventos USING GIN (metadados jsonb_path_ops);
SELECT * FROM logs_eventos WHERE metadados @> '{"evento": "login"}';

Arrays para tags e categorias

CREATE TABLE artigos (
    id UUID DEFAULT gen_random_uuid(),
    titulo VARCHAR(200),
    tags TEXT[],
    categorias INTEGER[]
);

INSERT INTO artigos (titulo, tags, categorias) VALUES
    ('SQL Avançado', ARRAY['banco de dados', 'postgresql', 'tutorial'], ARRAY[1, 3, 5]);

-- Busca artigos com tag específica
SELECT * FROM artigos WHERE 'postgresql' = ANY(tags);

UUID para chaves primárias em sistemas distribuídos

-- Tabela de usuários em sistema multi-região
CREATE TABLE usuarios_distribuidos (
    id UUID DEFAULT gen_random_uuid(),
    nome VARCHAR(100),
    regiao VARCHAR(20),
    criado_em TIMESTAMPTZ DEFAULT NOW(),
    PRIMARY KEY (id)
);

-- Inserção simultânea em diferentes regiões sem conflito
INSERT INTO usuarios_distribuidos (nome, regiao) VALUES
    ('Alice', 'US-East'),
    ('Bob', 'EU-West');

6. Limitações e boas práticas

Quando evitar JSONB

  • Se você consulta frequentemente campos internos específicos, uma coluna tradicional é mais rápida e permite constraints (NOT NULL, CHECK, FK).
  • JSONB não suporta chaves estrangeiras diretamente.
  • Atualizações parciais em JSONB reescrevem todo o documento — evite para campos grandes.

Cuidados com Arrays

  • Arrays violam a primeira forma normal (1NF) se usados para dados atômicos.
  • Para relacionamentos muitos-para-muitos com consultas complexas, uma tabela associativa é mais adequada.
  • Evite arrays muito grandes (> 1000 elementos) — eles degradam desempenho de indexação.

Fragmentação de índices com UUID

  • UUID v4 (aleatório) causa fragmentação em índices B-tree.
  • Considere UUID v7 (sequencial) ou use SERIAL se a distribuição global não for necessária.
  • Monitore o tamanho dos índices e considere reindexação periódica.

7. Exemplos combinados e consultas avançadas

JSONB com arrays aninhados

CREATE TABLE pedidos (
    id UUID DEFAULT gen_random_uuid(),
    cliente JSONB,
    itens JSONB  -- array de objetos
);

INSERT INTO pedidos (cliente, itens) VALUES
('{"nome": "Carlos", "id_cliente": 123}',
 '[{"produto": "Camiseta", "qtd": 2, "preco": 49.90},
   {"produto": "Calça", "qtd": 1, "preco": 129.90}]');

-- Expandir itens do pedido
SELECT id,
       cliente ->> 'nome' AS cliente,
       jsonb_array_elements(itens) ->> 'produto' AS produto,
       (jsonb_array_elements(itens) ->> 'qtd')::INT AS quantidade
FROM pedidos;

Conversão entre tipos

-- array para JSON
SELECT array_to_json(ARRAY[1, 2, 3]);  -- [1,2,3]

-- JSONB array para linhas
SELECT jsonb_array_elements('[1, 2, 3]'::jsonb);  -- retorna 3 linhas

-- Unnest com ordinalidade
SELECT id, unnest(tags) WITH ORDINALITY AS tag, ordinalidade
FROM produtos;

Índices compostos e estratégias de otimização

-- Índice composto: UUID + campo JSONB
CREATE INDEX idx_pedidos_cliente_data
ON pedidos ((cliente ->> 'id_cliente'), (cliente ->> 'data_pedido'));

-- Índice parcial para consultas frequentes
CREATE INDEX idx_pedidos_ativos
ON pedidos USING GIN (itens)
WHERE (cliente ->> 'status') = 'ativo';

Referências