sqlpostgresqljsonjsonb

JSONB_EACH no PostgreSQL: expanda um objeto JSON em linhas chave-valor

Use jsonb_each para expandir um objeto JSON em linhas (chave, valor), percorrer chaves dinamicas, filtrar e agregar entradas e saber quando usar a variante _text.

3 min de leituraReferencesql · postgresql · json · jsonb · set-returning

jsonb_each e uma funcao que retorna um conjunto de linhas (set-returning function): ela pega um unico objeto JSON e o expande em uma linha por par (key, value). E a operacao inversa de JSONB_OBJECT_AGG: la as linhas eram dobradas em um mapa, aqui o mapa volta a ser linhas que voce pode filtrar, juntar e agregar com SQL comum.

Sintaxe e exemplo basico

A funcao recebe um argumento do tipo jsonb e retorna duas colunas: key do tipo text e value do tipo jsonb. O lugar natural para chama-la e a clausula FROM, nao a lista do SELECT, de modo que cada entrada do objeto vira a sua propria linha de resultado.

SELECT key, value
FROM jsonb_each('{"theme": "dark", "lang": "en"}'::jsonb);

O resultado sao duas linhas: ('theme', "dark") e ('lang', "en"). Alguns comportamentos que vale lembrar:

  • value continua sendo jsonb, entao valores de texto voltam entre aspas: "dark", nao dark.
  • A ordem das linhas nao e garantida: um objeto e um mapa, nao uma lista.
  • Se o argumento for um array ou escalar em vez de um objeto, ocorre um erro. Para expandir um array use jsonb_array_elements.
  • Com uma entrada NULL a funcao retorna zero linhas, nao uma linha com NULL.

Expandir uma coluna JSON em linhas

Na maioria das vezes o objeto esta em uma coluna, nao em um literal. Suponha que users tenha um campo prefs jsonb com um conjunto arbitrario de configuracoes. Para expandi-lo em linhas, coloque a funcao no FROM apos uma virgula (e um join LATERAL implicito):

SELECT u.id, u.email, e.key, e.value
FROM users AS u, jsonb_each(u.prefs) AS e
WHERE u.id = 1;

Cada par de prefs vira a sua propria linha ligada ao usuario. A verdadeira forca e que as chaves podem ser dinamicas: voce nao precisa saber os nomes de antemao, como precisaria com prefs->>'theme'. Isso e ideal para percorrer uma forma desconhecida ou contar quais chaves chegam a aparecer.

Pegadinha: se prefs chegar a ser NULL, o join implicito descarta esse usuario do resultado em silencio. Para manter a linha do usuario, use um LEFT JOIN LATERAL ... ON true explicito e passe COALESCE(prefs, '{}'::jsonb).

Filtrar e agregar entradas

Uma vez que as entradas do objeto viraram linhas, todo o arsenal comum do SQL se aplica. Conte quantas chaves cada objeto de configuracoes tem:

SELECT u.id, count(*) AS pref_count
FROM users AS u, jsonb_each(u.prefs) AS e
GROUP BY u.id;

Ou descubra quais chaves sao mais comuns em toda a tabela, uma forma classica de inspecionar um JSON "selvagem" que foi escrito sem esquema:

SELECT e.key, count(*) AS used_by
FROM users AS u, jsonb_each(u.prefs) AS e
GROUP BY e.key
ORDER BY used_by DESC;

Voce pode filtrar por chave ou por valor. Encontre usuarios com pelo menos uma configuracao igual a string "on":

SELECT DISTINCT u.id, u.email
FROM users AS u, jsonb_each(u.prefs) AS e
WHERE e.value = '"on"'::jsonb;

Repare na comparacao: como value e jsonb, o literal tambem precisa ser JSON valido, dai as aspas dentro de '"on"'.

A variante _text: quando voce nao quer valores JSON

Se os valores sao na verdade apenas texto, as aspas em volta deles so atrapalham. E para isso que existe jsonb_each_text: ela retorna o mesmo par, mas value e text, sem as aspas externas.

-- value comes back as plain text: dark, not "dark"
SELECT key, value
FROM jsonb_each_text('{"theme": "dark", "lang": "en"}'::jsonb);

A diferenca importa quando voce precisa comparar valores ou converte-los para um numero:

-- read a numeric flag straight from a dynamic key
SELECT u.id, (e.value)::int AS limit_value
FROM users AS u, jsonb_each_text(u.prefs) AS e
WHERE e.key = 'rate_limit';

Com jsonb_each a mesma coisa exigiria um segundo cast para remover o tipo. Atencao a pegadinha: jsonb_each_text tambem transforma em string os objetos e arrays aninhados, que voltam como um texto JSON serializado, e um null de JSON vira um NULL de SQL.

Notas sobre outros bancos

Expandir um objeto JSON em linhas e claramente um ponto forte do PostgreSQL. O MySQL nao tem um equivalente direto de jsonb_each: normalmente voce extrai as chaves com JSON_KEYS e depois expande esse array com JSON_TABLE, o que e mais verboso. No ClickHouse, objetos JSON costumam ser guardados como tipo Map, e voce percorre com mapKeys e mapValues mais arrayJoin em vez de uma funcao dedicada. Quando voce precisa transformar um mapa de volta em linhas e trabalha-lo com SQL comum, jsonb_each e jsonb_each_text no PostgreSQL continuam sendo a ferramenta mais direta.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador