Skip to main content

PostgreSQL JSONB

Learning Focus

PostgreSQL offers two JSON types: json and jsonb. Always default to jsonb — it stores binary-encoded JSON, supports GIN indexing, and queries significantly faster.

json vs jsonb

Aspectjsonjsonb
StorageExact textBinary-encoded
IndexingNot supportedGIN, Hash
Query speedRe-parses each timeFast
Duplicate keysPreservedLast value wins
RecommendationAvoidAlways use

Create and Insert

CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
attributes JSONB
);

INSERT INTO products (name, attributes) VALUES
('Keyboard', '{
"color": "black", "switches": "Cherry MX Red",
"connectivity": ["USB-C", "Bluetooth"],
"dimensions": {"width": 440, "depth": 135}
}'),
('Mouse', '{"color": "white", "dpi": 1600}');

Querying Operators

OperatorReturnsDescription
->JSONGet JSON child by key
->>TEXTGet text child by key
#>JSONGet JSON at path array
#>>TEXTGet text at path array
?booleanDoes key exist?
@>booleanLeft contains right?
<@booleanLeft contained by right?
-- Text value
SELECT attributes ->> 'color' FROM products;

-- Nested value
SELECT attributes -> 'dimensions' ->> 'width' FROM products;

-- Key existence
SELECT name FROM products WHERE attributes ? 'dpi';

-- Containment
SELECT name FROM products WHERE attributes @> '{"color": "black"}';

-- Array containment
SELECT name FROM products
WHERE attributes -> 'connectivity' @> '["Bluetooth"]';

GIN Index

CREATE INDEX idx_products_attrs ON products USING GIN (attributes);

-- Path-specific expression index
CREATE INDEX idx_products_color ON products ((attributes ->> 'color'));

Updating JSONB

-- Set a value
UPDATE products
SET attributes = jsonb_set(attributes, '{color}', '"red"')
WHERE name = 'Mouse';

-- Merge / add keys
UPDATE products
SET attributes = attributes || '{"backlit": true}'
WHERE name = 'Keyboard';

-- Remove a key
UPDATE products
SET attributes = attributes - 'color'
WHERE name = 'Keyboard';

Concept Map

Concept Flow

JSONB Column
├── -> / ->> Operators → SQL WHERE / SELECT
├── #> / #>> Path access → SQL WHERE / SELECT
├── @> Containment → SQL WHERE / SELECT
├── ? Existence → SQL WHERE / SELECT
└── GIN Index → Fast lookup

Common Pitfalls

PitfallConsequencePrevention
Using json typeNo GIN indexingAlways use jsonb
No type cast on extracted textText comparison not numeric(attributes->>'price')::NUMERIC > 100
No GIN index on queried columnFull table scanAdd GIN index for all queried JSONB cols

What's Next