jsonb in practice

PostgreSQL · Practical · 14 min

jsonb lets one relational database also handle flexible, document-style data without a second system.

PostgreSQL has two JSON types. json stores the text verbatim; jsonb stores a parsed binary form — keys de-duplicated, whitespace dropped — that is faster to query and can be indexed. Use jsonb.

Reading:

  • doc -> 'key' returns jsonb; doc ->> 'key' returns text. Chain them: doc -> 'address' ->> 'city', or use a path: doc #>> '{address,city}'.
  • doc @> '{"plan": "pro"}' tests containment and can use a GIN index.
  • doc ? 'key' tests if a top-level key exists.
  • jsonb_array_elements(doc -> 'items') turns an array into rows; jsonb_to_recordset turns an array of objects into typed rows.
  • SQL/JSON path: jsonb_path_query(doc, '$.items[*] ? (@.qty > 1)'); PostgreSQL 17 adds the standard JSON_TABLE.

Writing: jsonb_set(doc, '{address,city}', '"Gyumri"') replaces a nested value, doc || '{"vip": true}' merges top-level keys, and doc - 'key' removes one. Each update rewrites the whole value, so large documents that change often are costly.

Use jsonb for genuinely variable attributes, payloads from external APIs and settings. Anything you filter, join or constrain regularly deserves a real column — with types, constraints and normal statistics for the planner.

Example

CREATE TABLE account (id int PRIMARY KEY, profile jsonb NOT NULL);
INSERT INTO account VALUES
 (1, '{"plan": "pro", "address": {"city": "Yerevan"}, "tags": ["beta", "vip"]}'),
 (2, '{"plan": "free", "address": {"city": "Gyumri"}}');

SELECT id, profile ->> 'plan' AS plan,
       profile -> 'address' ->> 'city' AS city
FROM account WHERE profile @> '{"plan": "pro"}';    -- 1 | pro | Yerevan

SELECT id FROM account WHERE profile -> 'tags' ? 'vip';   -- 1

UPDATE account SET profile = jsonb_set(profile, '{address,city}', '"Vanadzor"')
WHERE id = 2 RETURNING profile -> 'address';        -- {"city": "Vanadzor"}

CREATE INDEX account_profile_gin ON account USING gin (profile jsonb_path_ops);

Remember

  • -> returns jsonb, ->> returns text
  • @> containment queries can use a GIN index
  • Promote frequently filtered fields to real columns

Watch out for

  • Comparing profile -> 'age' > 30: -> returns jsonb, so cast (profile ->> 'age')::int instead.

Check yourself

  1. What does profile ->> 'plan' return for {"plan": "pro"}?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.postgresql.org/docs/current/functions-json.html