Module P-5·13 min read

JSONB vs JSON, containment operators, GIN indexes, and when JSONB is the right tool vs. a schema design shortcut.

Written by Jatin Jain Saraf · Architect of SupraScan

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 & Register

See it in production

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.