JSONB vs JSON, containment operators, GIN indexes, and when JSONB is the right tool vs. a schema design shortcut.
P-5 — JSON and JSONB: Working With Semi-Structured Data
Who this module is for: You have normalised relational schemas down. Sometimes, though, data does not fit neatly into fixed columns — product attributes vary by category, user preferences are open-ended, API responses have unpredictable shapes. PostgreSQL's JSONB gives you a relational database that also handles document-style data. This module covers when to reach for it, how to query it efficiently, and when using it is a mistake that will cost you later.
JSON vs JSONB — Choose JSONB
PostgreSQL has two JSON types:
JSON — stores the raw text of the JSON document, preserving whitespace, duplicate keys, and key ordering. Validation only on insert; no GIN/containment indexing support (though you can still add a plain B-tree expression index on a specific field, e.g. ((data->>'x'))). Slower to process because it re-parses on every operation.
JSONB — stores JSON in a decomposed binary format. Duplicate keys are removed (last value wins), key ordering is not preserved, but: supports indexing, supports all operators, and processes faster. Use JSONB for everything.
-- JSONB is almost always the right choice
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
attributes JSONB -- variable product attributes
);
Inserting JSONB
-- Insert JSON objects directly
INSERT INTO products (name, attributes) VALUES
('Laptop', '{"ram_gb": 16, "storage_gb": 512, "color": "silver", "ports": ["USB-C", "HDMI"]}'),
('T-Shirt', '{"size": "M", "color": "blue", "material": "cotton", "tags": ["casual", "summer"]}'),
('Headphones', '{"wireless": true, "battery_hours": 30, "color": "black"}');
-- Using jsonb_build_object for programmatic construction
INSERT INTO products (name, attributes) VALUES (
'Monitor',
jsonb_build_object(
'resolution', '4K',
'refresh_rate', 60,
'hdr', true,
'connections', jsonb_build_array('HDMI', 'DisplayPort', 'USB-C')
)
);
Querying JSONB — The Operator Reference
Extracting values
-- -> returns JSONB (useful for nested objects/arrays)
SELECT attributes -> 'color' FROM products;
-- Returns: "silver" (as JSONB with quotes)
-- "blue"
-- "black"
-- null (Monitor has no 'color' key)
-- ->> returns TEXT (useful for comparisons and display)
SELECT attributes ->> 'color' FROM products;
-- Returns: silver (as plain text, no quotes)
-- blue
-- black
-- (null)
-- Navigate nested paths
SELECT attributes -> 'specs' -> 'cpu' FROM products; -- nested JSONB
SELECT attributes #> '{specs, cpu}' FROM products; -- path array syntax
SELECT attributes #>> '{specs, cpu}' FROM products; -- as TEXT
-- Array element by index (0-based)
SELECT attributes -> 'ports' -> 0 FROM products; -- first port
SELECT attributes -> 'tags' ->> 1 FROM products; -- second tag as text
Filtering with JSONB operators
-- @> containment: does the JSONB contain this sub-document?
SELECT name FROM products
WHERE attributes @> '{"color": "black"}';
-- Returns: Headphones (has color=black)
SELECT name FROM products
WHERE attributes @> '{"wireless": true}';
-- Returns: Headphones
-- Filter by array element
SELECT name FROM products
WHERE attributes @> '{"ports": ["HDMI"]}';
-- Returns: Laptop (its ports array contains "HDMI")
-- ? key exists
SELECT name FROM products WHERE attributes ? 'wireless';
-- Returns products that have a 'wireless' key
-- ?| any key exists
SELECT name FROM products WHERE attributes ?| ARRAY['wireless', 'hdr'];
-- Returns products that have 'wireless' OR 'hdr'
-- ?& all keys exist
SELECT name FROM products WHERE attributes ?& ARRAY['color', 'ram_gb'];
-- Returns products that have BOTH 'color' AND 'ram_gb'
-- Compare extracted value as text
SELECT name FROM products
WHERE (attributes ->> 'battery_hours')::INTEGER > 20;
-- Cast to INTEGER for numeric comparison
Modifying JSONB
-- Add or update a key
UPDATE products
SET attributes = attributes || '{"in_stock": true}'
WHERE name = 'Laptop';
-- || merges two JSONB objects; right side wins on key conflicts
-- Set a nested value
UPDATE products
SET attributes = jsonb_set(attributes, '{specs, ram_gb}', '32')
WHERE name = 'Laptop';
-- jsonb_set(target, path_array, new_value)
-- Remove a key
UPDATE products
SET attributes = attributes - 'color'
WHERE name = 'T-Shirt';
-- - operator removes a key
-- Remove a nested key
UPDATE products
SET attributes = attributes #- '{specs, old_field}'
WHERE name = 'Laptop';
-- Add an element to an array
UPDATE products
SET attributes = jsonb_set(
attributes,
'{ports}',
(attributes -> 'ports') || '"Thunderbolt"'
)
WHERE name = 'Laptop';
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterSee it in production
Discussion
0Join the discussion