sqlpostgresqljsonbjson

jsonb_array_length no PostgreSQL: contar elementos de um array JSON com seguranca

Como jsonb_array_length conta elementos de um array JSON, por que gera o erro non-array, como proteger com jsonb_typeof e filtrar linhas por tamanho.

3 min de leituraReferencesql · postgresql · jsonb · json · arrays

A funcao jsonb_array_length responde a uma unica pergunta: quantos elementos ha em um array JSON. Parece trivial, mas na pratica ela falha alegremente com cannot get array length of a non-array assim que um objeto, um numero ou um null aparece na coluna. Vamos ver como contar o tamanho com seguranca, filtrar linhas pelo tamanho do array e como isso difere de cardinality em um array SQL nativo.

Contar elementos, o basico

Suponha que os pedidos tenham uma coluna data jsonb onde a chave items guarda um array de itens do carrinho. data->'items' retorna o proprio array JSONB (a seta ->, nao ->>), e jsonb_array_length toma o tamanho dele:

SELECT id,
       jsonb_array_length(data->'items') AS item_count
FROM orders;

Alguns fatos que economizam tempo:

  • O argumento precisa ser um array JSONB ([...]), nao um objeto nem um escalar.
  • O tamanho e contado apenas no nivel superior: um array aninhado conta como um elemento.
  • Um array vazio [] retorna 0 -- uma resposta valida, nao um erro.
  • Para o tipo json (sem b) existe a gemea json_array_length.

O erro non-array e como se proteger

A dor principal aparece quando items falta em algumas linhas ou guarda algo que nao e um array. Entao a consulta inteira explode:

-- ERROR: cannot get array length of a non-array
SELECT jsonb_array_length(data->'items') FROM orders;

Ha tres causas, e todas surgem em dados reais:

  • A chave items nao existe -- a seta retorna NULL de SQL, e aqui nao ha erro, voce apenas recebe NULL.
  • items guarda um objeto {...} ou uma string -- e isso que quebra a funcao.
  • O valor e um null de JSON ('null'::jsonb), que tambem nao e um array.

A protecao confiavel e verificar o tipo com jsonb_typeof antes de chamar o tamanho. O jeito mais limpo e envolver em um CASE ou combinar com um filtro:

SELECT id,
       CASE
         WHEN jsonb_typeof(data->'items') = 'array'
           THEN jsonb_array_length(data->'items')
         ELSE 0
       END AS item_count
FROM orders;

Pegadinha: no PostgreSQL WHERE e SELECT nao garantem a ordem de avaliacao, entao WHERE jsonb_typeof(...) = 'array' AND jsonb_array_length(...) > 2 poderia em teoria tentar calcular o tamanho antes da verificacao de tipo. Na pratica o planejador costuma colocar primeiro a verificacao barata, mas um CASE ou uma subconsulta com ordem explicita e mais seguro. Nao confie no curto-circuito do AND para silenciar erros com efeitos colaterais.

Filtrar linhas pelo tamanho do array

Uma tarefa tipica e encontrar pedidos com um carrinho "grande". Primeiro exclua os nao-arrays, depois compare o tamanho:

SELECT id, user_id, amount
FROM orders
WHERE jsonb_typeof(data->'items') = 'array'
  AND jsonb_array_length(data->'items') >= 3;

O mesmo truque vale para agregados. Por exemplo, o tamanho medio do carrinho apenas sobre pedidos validos:

SELECT avg(jsonb_array_length(data->'items')) AS avg_items
FROM orders
WHERE jsonb_typeof(data->'items') = 'array';

Se o array esta mais fundo, estenda o caminho com setas; a protecao por tipo continua a mesma:

SELECT u.id, u.email
FROM users AS u
WHERE jsonb_typeof(u.data->'profile'->'tags') = 'array'
  AND jsonb_array_length(u.data->'profile'->'tags') = 0;

Isso seleciona usuarios cujas tags sao exatamente zero (um array vazio), mas especificamente um array, nao uma chave ausente.

jsonb_array_length contra cardinality

Nao confunda um array JSON com um array nativo do PostgreSQL como text[] ou int[]. Eles usam funcoes de tamanho diferentes:

  • jsonb_array_length(x) -- para um valor array JSONB.
  • cardinality(x) -- para um array SQL; retorna o numero total de elementos.
  • array_length(x, 1) -- o tamanho do array SQL na primeira dimensao; sobre um array vazio retorna NULL, nao 0.
-- SQL array stored in a real array column
SELECT cardinality(roles) AS role_count
FROM employees;          -- assumes roles text[]

-- JSON array stored inside a jsonb column
SELECT jsonb_array_length(data->'items') AS item_count
FROM orders;

Regra pratica: se os dados chegam como documento e a forma e fluida, isso e JSONB -- conte com jsonb_array_length protegido por jsonb_typeof. Se o array e uniforme e filtrado com frequencia, e candidato a uma coluna de array nativa com cardinality, que e mais concisa e nunca tropeca em escalares.

Outros motores diferem, entao nao presuma a mesma semantica. No MySQL o tamanho do array JSON vem de JSON_LENGTH(data, '$.items'), e ele nao gera o erro non-array de jeito nenhum -- mas tambem nao mede o tamanho de um array. Sobre um escalar (um numero, uma string, um booleano) JSON_LENGTH retorna 1, e sobre um objeto retorna o numero de chaves de nivel superior, nao "o tamanho do valor". Assim, uma linha onde items e {"a":1,"b":2} retorna silenciosamente 2, e uma linha onde e 5 retorna 1 -- sem erro, mas tambem nao o tamanho do carrinho. No ClickHouse voce usa JSONLength(data, 'items') para JSON, ou trabalha com colunas nativas Array(...) e a funcao length(). Porte consultas entre motores com cuidado -- o comportamento sobre um nao-array e sobre uma chave ausente difere em cada um.

Pratique com exercícios reais

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

Abrir o treinador