Course topics

By WebNest Studio Engineering Team

PostgreSQL Tutorial

JSON and JSONB in PostgreSQL

PostgreSQL can store semi-structured data directly in a column using two related types: JSON and JSONB. This lets you combine the strong guarantees of a relational schema with the flexibility of a document store for fields that genuinely vary in shape — settings blobs, event payloads, third-party API responses — without resorting to a separate NoSQL database.

The two types look similar on the surface but store data very differently, and that difference has real consequences for performance and what operations are efficient.

Why it matters

Some data does not fit fixed columns: product attributes that differ by category, settings, payloads from other systems. JSONB stores such documents inside an ordinary table and lets you query and index them, giving much of the flexibility of a document database without leaving PostgreSQL.

JSON vs. JSONB

JSON stores an exact textual copy of the input, including whitespace and key order, and re-parses it every time it is queried. JSONB ("JSON binary") parses the input once at write time into a decomposed binary format, discarding insignificant whitespace and duplicate keys. JSONB is slightly slower to insert but substantially faster to query, and — critically — it is the only one of the two that can be indexed efficiently. For nearly every real application, JSONB is the correct choice; plain JSON is mainly useful when you must preserve the exact original text.

Querying with -> and ->>

The -> operator extracts a field as JSON/JSONB (preserving type), while ->> extracts it as text. For nested access you chain them, using ->> only on the final step if you want a text result: data->'address'->>'city'. The #> and #>> operators do the same thing with a path array, which is handy for deeply nested or dynamic paths: data #>> '{address,city}'.

Containment and Indexing

JSONB supports the @> ("contains") and ? ("has key") operators, which let you ask questions like "does this document contain this sub-object" or "does this object have this key" directly. These operators can use a GIN index built on the JSONB column, making containment queries fast even over millions of rows — something plain JSON cannot do.

Examples

Storing and querying JSONB with -> and ->>

SQL
CREATE TABLE events (
    id      BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    payload JSONB NOT NULL
);

INSERT INTO events (payload) VALUES
('{"type": "signup", "user": {"id": 42, "plan": "pro"}}');

SELECT
    payload->>'type'          AS event_type,
    payload->'user'->>'plan'  AS plan
FROM events;
Output
 event_type | plan
------------+------
 signup     | pro

Containment query accelerated by a GIN index

SQL
CREATE INDEX idx_events_payload ON events USING GIN (payload);

SELECT id FROM events
WHERE payload @> '{"user": {"plan": "pro"}}';
Output
CREATE INDEX
 id
----
  1

Common Mistakes

  • Defaulting to JSON instead of JSONB, then discovering it cannot be indexed efficiently and re-parses on every read.
  • Using ->> when a nested JSON/JSONB result (not text) is needed for further chaining, breaking the chain of -> operators.
  • Storing data as JSONB that is actually always the same shape — a case where real columns with proper types and constraints would be simpler, faster, and safer to query.
  • Forgetting to create a GIN index before relying on @> or ? containment queries at scale, resulting in slow sequential scans.

Exercise

This exercise uses PostgreSQL's JSONB type and operators, so it needs a PostgreSQL server. Create a products table with a JSONB column named attributes. Insert a pen whose attributes are colour blue and pack size 10, and a bag whose attributes are colour red. Write one query that returns the name and the colour, as text, of every product, and a second that finds the products whose attributes contain colour blue, using the containment operator.

Expected result: the first query returns Pen with blue and Bag with red; the second returns Pen.

SQL · starter code
-- TODO: products (id, name, attributes jsonb)

-- TODO: insert the pen and the bag

-- TODO: name and colour (as text) of every product

-- TODO: products whose attributes contain {"color": "blue"}
Show hints
  • ->> extracts a value as text: attributes ->> 'color'.
  • Containment is written attributes @> '{"color": "blue"}'.
Show solution
SQL · solution
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  attributes JSONB NOT NULL DEFAULT '{}'
);

INSERT INTO products (id, name, attributes) VALUES
  (1, 'Pen', '{"color": "blue", "pack_size": 10}'),
  (2, 'Bag', '{"color": "red"}');

SELECT name, attributes ->> 'color' AS color
FROM products;

SELECT name
FROM products
WHERE attributes @> '{"color": "blue"}';

Quick quiz

1. What is the difference between the -> and ->> operators?

  • A. There is none
  • B. -> returns the value as JSON; ->> returns it as text
  • C. ->> returns JSON
  • D. -> works only on arrays
Show answer

B. Use ->> when comparing with a text value or returning the value to an application.

2. Which index type speeds up JSONB containment queries with @>?

  • A. B-tree
  • B. Hash
  • C. GIN
  • D. No index can
Show answer

C. A GIN index indexes the keys and values inside each document.

3. How does JSONB differ from JSON?

  • A. JSONB is stored in a parsed binary form, which is faster to query and can be indexed
  • B. JSONB keeps the original text exactly
  • C. JSON is faster to query
  • D. JSONB cannot hold arrays
Show answer

A. JSON keeps the exact text, including whitespace and key order, and is reparsed on every use.

Interview questions

What is the difference between JSON and JSONB in PostgreSQL?

Show answer

JSON stores the document as text exactly as received, preserving whitespace, key order and duplicate keys, and parses it each time it is queried. JSONB stores a decomposed binary form: slightly slower to write, but much faster to query, and it supports indexing and operators such as containment. It does not keep whitespace, key order or duplicate keys. JSONB is the right choice in almost every case.

When should data go in a JSONB column and when in ordinary columns?

Show answer

Use ordinary columns for data that every row has, that is filtered, joined or constrained, since columns have types, constraints, foreign keys and good statistics for the query planner. Use JSONB for attributes that vary from row to row, for sparse or evolving data, and for payloads stored as received. Putting core fields in JSON gives up integrity checks and makes queries slower and harder to write.

Key Points to Remember

  • JSON preserves exact text and re-parses on read; JSONB stores a parsed binary form and is faster to query.
  • -> returns JSON/JSONB, ->> returns text; #> and #>> do the same via a path array.
  • JSONB supports @> (contains) and ? (has key), which can be indexed with GIN for fast containment search.
  • JSONB is the default choice for semi-structured data; use JSON only when exact text preservation is required.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering