← All cheatsheets
Data Engineer · #044 · September 5, 2026 · 2 min read

How do you query JSON in SQL?

The Postgres jsonb arrow operators decoded: reading keys, filtering, exploding arrays into rows, the casts that make SUM work, and the text trap hidden inside ->>.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

Every events table and API dump lands as a json column that everyone tiptoes around. Here are the Postgres jsonb operators decoded, and the text trap that makes queries run and still be wrong. The print-ready A4 PDF is at the bottom.

Read a key

  • data->'user': json out.
  • data->>'name': TEXT out.
  • data#>>'{a,b}': deep path, text out.

Filter on it

  • WHERE d->>'k' = 'x': match a value.
  • d @> '{"pro":true}': contains.
  • d ? 'key': key exists.
  • A missing key returns NULL, not an error.

Arrays

  • jsonb_array_elements: array becomes rows.
  • d->'tags'->0: first element.
  • jsonb_array_length: count items.

Cast and compute

  • (d->>'amt')::NUMERIC: then SUM works.
  • (d->>'ts')::DATE: then date math works.
  • jsonb_typeof(d): debug what a column really holds.

Speed and build

  • GIN index on jsonb: fast @> filters.
  • jsonb_build_object: json back out.
  • Hot keys queried constantly: promote them to real columns.
  • json vs jsonb: use jsonb. Always.

Events table to clean rows

SELECT
  e.data->>'user_id'            AS user_id,
  (e.data->>'amount')::NUMERIC  AS amount,
  t.value->>0                   AS tag
FROM events e,
  jsonb_array_elements(e.data->'tags') t
WHERE e.data->>'type' = 'purchase';

jsonb_array_elements in FROM is an implicit lateral join: one output row per array item. This is the pattern that turns an API dump into something a dashboard can read.

The trap: ->> always returns text

You wroteWhat happensWrite instead
SUM(d->>'amt')error, or text concatSUM((d->>'amt')::NUMERIC)
d->>'price' > 100'9' beats '100' as text::NUMERIC before comparing
d->'name' = 'Ana'json vs text: never equald->>'name' = 'Ana'

The query runs, the number is wrong: the worst kind of bug. One arrow returns json, two arrows return text, and text sorts alphabetically. Cast at the boundary.

Frequently asked questions

What is the difference between -> and ->> in Postgres?
One arrow returns json, two arrows return text. data->'user' gives you a json value you can keep navigating into; data->>'name' gives you TEXT you can compare and display. For deep paths, data#>>'{a,b}' reads a nested key as text in one step. Mixing them up is why d->'name' = 'Ana' never matches: json and text are never equal.
How do I turn a JSON array into rows in SQL?
jsonb_array_elements(data->'tags') in the FROM clause, which acts as an implicit lateral join: one output row per array item. For quick reads, d->'tags'->0 grabs the first element and jsonb_array_length counts the items.
Why does SUM not work on my JSON column?
Because ->> always returns text, and SUM wants a number. Cast at the boundary: SUM((d->>'amt')::NUMERIC). The same applies to comparisons, where '9' beats '100' alphabetically, and to dates, which need (d->>'ts')::DATE before date math works.
Should I use json or jsonb in Postgres?
jsonb, always. It stores a parsed binary form, supports the containment operator @> and can be indexed with a GIN index, which makes filters fast. Plain json stores raw text and reparses on every read. And when a key is queried constantly, promote it to a real column.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

More cheatsheets