JSON vs JSONB — Data Types, Storage, and When to Use Each
JSON vs JSONB — Data Types, Storage, and When to Use Each
PostgreSQL has two JSON data types: json and jsonb. They store the same documents but differ fundamentally in how they work internally — and the difference determines everything about performance, indexing, and what you can do with the data.
The key difference
| Feature | json |
jsonb |
|---|---|---|
| Storage | Raw text, exactly as written | Binary-decomposed format |
| Duplicate keys | Preserved (last key wins on retrieval) | Last key only stored |
| Key order | Preserved | Not preserved |
| Whitespace | Preserved | Stripped |
| Write speed | Faster (no parsing) | Slightly slower (parses at insert) |
| Read speed | Slower (re-parses every query) | Much faster (already parsed) |
| Indexing | Only full-document expression indexes | GIN indexes on contents |
| Operators | Fewer | Full set including containment (@>, <@) and existence (?) |
The practical rule: use jsonb for almost everything. The only reason to use json is when you need to preserve exact whitespace, key order, or duplicate keys — extremely rare requirements. For querying, indexing, and analytics, jsonb is strictly better.
Example 1: Creating tables with JSONB columns
The most common use case for JSONB in analytics: storing flexible metadata alongside structured columns. A product catalogue where different product types have different attributes, or an events table where each event type has a different payload structure.
Practice table:
product_catalogis set up in the Practice Datasets — Overview lesson. If you haven't run that setup yet, go there first and run the "Additional Practice Tables" block.
Example 2: Basic JSONB operators
PostgreSQL's JSONB operators are the core of JSON querying. Learn these six and you can query almost any JSON structure:
-- The six essential JSONB operators:
-- -> returns JSONB (preserves type for nested access)
-- ->> returns TEXT (for leaf values you want to compare as strings)
-- #> path operator returning JSONB
-- #>> path operator returning TEXT
-- @> contains (does left document contain right document?)
-- ? key existence
-- Navigate to a top-level key as JSONB
SELECT name, attributes -> 'brand' AS brand_json
FROM product_catalog;
-- Navigate to a top-level key as TEXT (useful for comparisons)
SELECT name, attributes ->> 'brand' AS brand_text
FROM product_catalog WHERE attributes ->> 'brand' = 'Apple';
-- Access nested path: attributes -> 'specs' -> 'cpu'
SELECT name, attributes #>> '{specs,cpu}' AS cpu
FROM product_catalog WHERE category = 'Electronics';
-- Containment: does attributes contain this sub-document?
SELECT name FROM product_catalog
WHERE attributes @> '{"brand": "Apple"}';
-- Existence: does attributes have the key 'author'?
SELECT name, category FROM product_catalog
WHERE attributes ? 'author';
-- Multiple key existence: does it have 'brand' AND 'ram_gb'?
SELECT name FROM product_catalog
WHERE attributes ?& ARRAY['brand', 'ram_gb'];
-- Any key existence: does it have 'author' OR 'pages'?
SELECT name FROM product_catalog
WHERE attributes ?| ARRAY['author', 'pages'];
What These Return
| Operator | Input | Output type | Example result |
|---|---|---|---|
-> 'brand' |
JSONB | JSONB | "Apple" (with quotes — it's JSON) |
->> 'brand' |
JSONB | TEXT | Apple (no quotes — it's text) |
@> '{"brand":"Apple"}' |
JSONB | BOOLEAN | true / false |
? 'author' |
JSONB | BOOLEAN | true if key exists |
The -> vs ->> distinction trips up almost everyone: use -> when you need to chain operators (accessing nested keys), use ->> when you want a text value to compare or display.
Example 3: Type extraction and casting from JSONB
JSONB stores all values as JSON types (strings, numbers, booleans, arrays, objects). When you extract them with ->>, you always get TEXT. To use them as numbers for calculations, you must cast:
SELECT
name,
category,
base_price,
-- Extract as text first, then cast to numeric
(attributes ->> 'ram_gb')::INT AS ram_gb,
(attributes ->> 'storage_gb')::INT AS storage_gb,
(attributes ->> 'weight_kg')::NUMERIC AS weight_kg,
-- Computed column from JSONB + base column
ROUND(base_price / NULLIF((attributes ->> 'storage_gb')::INT, 0), 2) AS price_per_gb,
-- Conditional extraction: only for electronics
CASE WHEN category = 'Electronics'
THEN (attributes ->> 'ram_gb')::INT
ELSE NULL END AS ram_gb_electronics_only
FROM product_catalog
WHERE category = 'Electronics'
ORDER BY (attributes ->> 'ram_gb')::INT DESC;
The cast pattern (jsonb_column ->> 'key')::TYPE is standard PostgreSQL. Always check for NULL before casting — if the key doesn't exist, ->> returns NULL and the cast will succeed (NULL remains NULL), but if the key exists with a non-numeric value, the cast will error.
Example 4: JSONB functions for introspection
When working with unknown or variable JSON structures, the JSONB functions for introspecting the document shape are essential:
-- jsonb_object_keys: list all top-level keys
SELECT DISTINCT jsonb_object_keys(attributes) AS key
FROM product_catalog
ORDER BY key;
-- jsonb_each: expand top-level key/value pairs into rows
SELECT name, key, value
FROM product_catalog, jsonb_each(attributes) AS kv(key, value)
ORDER BY name, key;
-- jsonb_typeof: check the type of a JSON value
SELECT
name,
jsonb_typeof(attributes -> 'sizes') AS sizes_type, -- 'array' or 'null'
jsonb_typeof(attributes -> 'ram_gb') AS ram_type, -- 'number'
jsonb_typeof(attributes -> 'brand') AS brand_type -- 'string'
FROM product_catalog;
-- jsonb_array_length: count elements in a JSONB array
SELECT
name,
attributes -> 'sizes' AS sizes_array,
jsonb_array_length(attributes -> 'sizes') AS num_sizes
FROM product_catalog
WHERE attributes ? 'sizes';
-- Row count per key occurrence (which keys are most common?)
SELECT key, COUNT(*) AS occurrences
FROM product_catalog, jsonb_object_keys(attributes) AS key
GROUP BY key
ORDER BY occurrences DESC;
Key Takeaway
Use jsonb not json for every new column. The three-way operator split — -> (JSONB navigation), ->> (text extraction), @> (containment) — covers 90% of all JSON queries. Always cast ->> results to the appropriate type for arithmetic. When the document structure is unknown, jsonb_object_keys and jsonb_each let you explore it dynamically.