Quando voce guarda dados semiestruturados em uma coluna JSONB, toda leitura comeca com dois operadores: -> e ->>. Eles sao a base de qualquer consulta JSON no PostgreSQL: um desce pela estrutura e o outro extrai um valor pronto para usar. Vamos ver como diferem, como acessar campos de objeto e elementos de array e como encadea-los.
-> retorna jsonb, ->> retorna text
Os dois operadores buscam um valor por chave, mas retornam tipos diferentes. -> entrega jsonb (um objeto, array ou escalar em forma JSON), enquanto ->> entrega text puro. Essa unica diferenca determina todo o resto.
SELECT
prefs -> 'theme' AS as_jsonb,
prefs ->> 'theme' AS as_text
FROM users;
Uma regra simples:
- Use
-> quando for descer mais e ainda vier outro -> ou ->>.
- Use
->> no fim do caminho, quando quiser um valor escalar para exibir ou comparar.
A chave e uma string (o nome de um campo do objeto), e uma chave inexistente retorna NULL em vez de um erro.
Campos de objeto e elementos de array
Se a direita do operador houver uma string, o PostgreSQL procura um campo do objeto. Se houver um inteiro, ele indexa um array, e a indexacao comeca em zero. Um indice negativo conta a partir do fim.
SELECT
meta -> 'tags' -> 0 AS first_tag_json,
meta -> 'tags' ->> 0 AS first_tag_text,
meta -> 'tags' ->> -1 AS last_tag
FROM users;
O mesmo vale sobre suas proprias tabelas. Suponha que orders tenha um payload com um array de itens:
SELECT
id,
payload -> 'items' -> 0 ->> 'sku' AS first_sku
FROM orders
WHERE status = 'paid';
Encadeamento e conversao de tipos
Um caminho longo e apenas varios operadores em sequencia. Desca com ->, permanecendo em jsonb, e no ultimo passo troque para ->> para obter text. Como ->> sempre retorna texto, converta explicitamente com :: para numeros, datas e booleanos.
SELECT
profile -> 'address' ->> 'city' AS city,
(profile ->> 'age')::int AS age_int
FROM users
WHERE (profile ->> 'age')::int >= 18;
Um exemplo real de agregacao: extrair um valor dos metadados JSON e calcular o ticket medio por pais.
SELECT
u.country,
round(avg((o.payload ->> 'amount')::numeric), 2) AS avg_amount
FROM orders o
JOIN users u ON u.id = o.user_id
GROUP BY u.country
ORDER BY avg_amount DESC;
Pegadinhas
- Comparar sem converter quase sempre e um bug:
payload ->> 'amount' > '99' compara strings lexicograficamente, entao '9' fica acima de '100'. Converta para numero: (payload ->> 'amount')::numeric > 99.
-> sobre um escalar retorna jsonb, entao prefs -> 'theme' = 'dark' falha com erro de tipo; use ->> para comparar ou escreva prefs -> 'theme' = '"dark"'::jsonb.
- Uma chave inexistente ou um indice fora do intervalo retorna
NULL em silencio, entao um erro de digitacao no nome de um campo nao gera erro e corrompe o resultado sem aviso.
->> sobre um objeto ou array retorna o texto JSON completo, nao um unico valor; pegue texto apenas em escalares.
O MySQL nao tem esse encadeamento com setas: use JSON_EXTRACT(doc, '$.address.city') ou o atalho doc->>'$.address.city' (o ->> dele tambem retorna texto). O ClickHouse oferece funcoes como JSONExtractString(col, 'city') e JSONExtractInt(col, 'age'). A ideia e universal, mas a sintaxe de setas -> / ->> e propria do PostgreSQL.
Quando voce guarda dados semiestruturados em uma coluna
JSONB, toda leitura comeca com dois operadores:->e->>. Eles sao a base de qualquer consulta JSON no PostgreSQL: um desce pela estrutura e o outro extrai um valor pronto para usar. Vamos ver como diferem, como acessar campos de objeto e elementos de array e como encadea-los.-> retorna jsonb, ->> retorna text
Os dois operadores buscam um valor por chave, mas retornam tipos diferentes.
->entregajsonb(um objeto, array ou escalar em forma JSON), enquanto->>entregatextpuro. Essa unica diferenca determina todo o resto.-- prefs is a jsonb column on users: {"theme": "dark", "lang": "en"} SELECT prefs -> 'theme' AS as_jsonb, -- "dark" (with quotes, type jsonb) prefs ->> 'theme' AS as_text -- dark (no quotes, type text) FROM users;Uma regra simples:
->quando for descer mais e ainda vier outro->ou->>.->>no fim do caminho, quando quiser um valor escalar para exibir ou comparar.A chave e uma string (o nome de um campo do objeto), e uma chave inexistente retorna
NULLem vez de um erro.Campos de objeto e elementos de array
Se a direita do operador houver uma string, o PostgreSQL procura um campo do objeto. Se houver um inteiro, ele indexa um array, e a indexacao comeca em zero. Um indice negativo conta a partir do fim.
-- meta = {"tags": ["new", "vip", "eu"], "score": 7} SELECT meta -> 'tags' -> 0 AS first_tag_json, -- "new" as jsonb meta -> 'tags' ->> 0 AS first_tag_text, -- new as text meta -> 'tags' ->> -1 AS last_tag -- eu (last element) FROM users;O mesmo vale sobre suas proprias tabelas. Suponha que
orderstenha umpayloadcom um array de itens:-- payload = {"items": [{"sku": "A1", "qty": 2}, {"sku": "B7", "qty": 1}]} SELECT id, payload -> 'items' -> 0 ->> 'sku' AS first_sku FROM orders WHERE status = 'paid';Encadeamento e conversao de tipos
Um caminho longo e apenas varios operadores em sequencia. Desca com
->, permanecendo emjsonb, e no ultimo passo troque para->>para obtertext. Como->>sempre retorna texto, converta explicitamente com::para numeros, datas e booleanos.-- profile = {"address": {"city": "Berlin", "zip": "10115"}, "age": "34"} SELECT profile -> 'address' ->> 'city' AS city, (profile ->> 'age')::int AS age_int FROM users WHERE (profile ->> 'age')::int >= 18;Um exemplo real de agregacao: extrair um valor dos metadados JSON e calcular o ticket medio por pais.
-- payload = {"amount": "149.90", "currency": "EUR"} SELECT u.country, round(avg((o.payload ->> 'amount')::numeric), 2) AS avg_amount FROM orders o JOIN users u ON u.id = o.user_id GROUP BY u.country ORDER BY avg_amount DESC;Pegadinhas
payload ->> 'amount' > '99'compara strings lexicograficamente, entao'9'fica acima de'100'. Converta para numero:(payload ->> 'amount')::numeric > 99.->sobre um escalar retornajsonb, entaoprefs -> 'theme' = 'dark'falha com erro de tipo; use->>para comparar ou escrevaprefs -> 'theme' = '"dark"'::jsonb.NULLem silencio, entao um erro de digitacao no nome de um campo nao gera erro e corrompe o resultado sem aviso.->>sobre um objeto ou array retorna o texto JSON completo, nao um unico valor; pegue texto apenas em escalares.O MySQL nao tem esse encadeamento com setas: use
JSON_EXTRACT(doc, '$.address.city')ou o atalhodoc->>'$.address.city'(o->>dele tambem retorna texto). O ClickHouse oferece funcoes comoJSONExtractString(col, 'city')eJSONExtractInt(col, 'age'). A ideia e universal, mas a sintaxe de setas-> / ->>e propria do PostgreSQL.