01
O esquema existe, só não foi escrito: inventariar chaves e tipos antes de decidir qualquer coisa
A primeira reação ao campo flexível que virou problema é discutir se ele deveria existir. É a discussão errada, porque o campo já tem 40 milhões de linhas e a resposta não muda nada. A pergunta útil é outra: que esquema esses 40 milhões de documentos obedecem de fato? Toda chave que aparece em 90% das linhas é uma coluna que nunca foi declarada. Toda chave que aparece com dois tipos diferentes é uma validação que nunca existiu. Toda chave que aparece em 0,3% das linhas e nunca entra em um WHERE é a única parte do campo que está sendo usada como "flexível" de verdade. Antes de criar índice, constraint ou coluna, é preciso enxergar essa distribuição, e o próprio Postgres a entrega com jsonb_object_keys e jsonb_typeof sobre uma amostra.
-- inventario.sql
-- Inventario das chaves de primeiro nivel do jsonb: presenca e tipos por chave.
-- TABLESAMPLE SYSTEM (1) le cerca de 1% das paginas da tabela: em 40 milhoes de
-- linhas e o bastante para enxergar toda chave que aparece em mais de 0,1% delas,
-- sem varrer a tabela inteira nem segurar lock.
WITH amostra AS (
SELECT atributos
FROM clientes TABLESAMPLE SYSTEM (1)
WHERE jsonb_typeof(atributos) = 'object' -- array ou escalar no lugar do objeto tambem e achado
),
chaves AS (
SELECT chave, jsonb_typeof(atributos -> chave) AS tipo
FROM amostra, LATERAL jsonb_object_keys(atributos) AS chave
)
SELECT chave,
count(*) AS ocorrencias,
round(100.0 * count(*) / (SELECT count(*) FROM amostra), 1) AS presenca_pct,
string_agg(DISTINCT tipo, ', ' ORDER BY tipo) AS tipos
FROM chaves
GROUP BY chave
ORDER BY ocorrencias DESC;
-- Para cada chave com mais de um tipo, um exemplo de cada forma: e isso que vira
-- a lista de correcoes antes de qualquer indice ou constraint.
SELECT chave, tipo, min(atributos ->> chave) AS exemplo, count(*) AS linhas
FROM clientes TABLESAMPLE SYSTEM (1), LATERAL jsonb_object_keys(atributos) AS chave,
LATERAL (SELECT jsonb_typeof(atributos -> chave) AS tipo) t
WHERE chave IN ('cpf', 'limite_credito', 'endereco')
GROUP BY chave, tipo
ORDER BY chave, linhas DESC;O TABLESAMPLE importa mais do que parece. Rodar jsonb_object_keys em 40 milhões de linhas é uma varredura completa com detoast de cada documento, e um jsonb acima de cerca de 2 KB fica comprimido em uma tabela TOAST separada: ler uma chave é descomprimir o documento inteiro. Em 1% das páginas, a consulta termina em segundos, e a distribuição de chaves que importam, as que aparecem em mais de 1% das linhas, sai com precisão suficiente para decidir. O que a amostra não captura são as chaves raras, e isso é aceitável: chave rara não vai ganhar índice nem coluna. O resultado típico em uma tabela como essa tem a cara da tabela abaixo.
| Chave | Presença | Tipos encontrados | O que isso revela |
|---|---|---|---|
| plano | 99,8% | string | Coluna obrigatória que nunca foi declarada; entra em WHERE e GROUP BY de todo relatório |
| cpf | 97,1% | string, number | Validação que nunca existiu: 3% das linhas vieram de uma importação que não converteu para texto |
| endereco | 61,4% | object, string | Duas gerações de cadastro: objeto com cep, rua e cidade, e um texto livre do formulário antigo |
| cep, rua, cidade (na raiz) | 12,9% | string | Terceira geração: o endereço achatado na raiz pelo job de importação do CRM anterior |
| limite_credito | 34,0% | number, string | Comparado com > em consultas; como texto, "900" é maior que "5000" |
| tags | 8,2% | array | Lista aberta por cliente; filtrada por contenção, nunca ordenada ou agregada |
| outras 41 chaves | < 1% cada | variados | A parte realmente flexível do campo: fica no JSON e não ganha nada |
Esse inventário vira a lista de trabalho e a sua ordem. A chave presente em quase todas as linhas e usada em filtro é candidata a coluna. A chave com dois tipos precisa de correção e constraint antes de qualquer índice, porque um índice de expressão com cast falha na primeira linha com tipo errado. A chave com duas formas estruturais, como o endereço, precisa de uma decisão de modelagem, não de um índice. E a cauda longa de chaves raras é a parte do campo que estava certa o tempo todo.