Skip to main content

PostgreSQL JSONB Operations

Introduction

PostgreSQL introduced the JSONB data type in version 9.4, revolutionizing how we can store and query semi-structured data within a relational database. Unlike regular JSON, which stores data as exact text copies, JSONB (Binary JSON) stores data in a decomposed binary format, making it more efficient for processing and querying.

In this guide, we'll explore how to effectively work with JSONB data in PostgreSQL, from basic operations to advanced querying techniques.

Understanding JSON vs JSONB

Before diving into operations, it's important to understand the two JSON-related data types in PostgreSQL:

FeatureJSONJSONB
StorageStores exact text copy with whitespaceDecomposed binary format
Insertion speedFaster (no conversion overhead)Slower (needs conversion)
Processing speedSlower (needs parsing)Faster (pre-parsed)
IndexingLimitedSupports GIN indexing
Duplicate keysPreserves all keys and orderRemoves duplicates, last value wins

For most applications, JSONB is the recommended choice due to its query performance advantages, especially for data that will be frequently queried.

Creating Tables with JSONB Columns

Let's start by creating a table with a JSONB column:

sql
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
details JSONB
);

Inserting JSONB Data

You can insert JSON data directly into a JSONB column:

sql
INSERT INTO products (name, details)
VALUES (
'Smartphone',
'{"brand": "TechX", "model": "TX-5", "specs": {"ram": "8GB", "storage": "128GB", "cpu": "OctaCore 2.5GHz"}}'
);

You can also insert JSON from a parsed object using array syntax:

sql
INSERT INTO products (name, details)
VALUES (
'Laptop',
'{"brand": "DevBook", "model": "Pro 13", "specs": {"ram": "16GB", "storage": "512GB", "cpu": "QuadCore 3.2GHz"}}'
);

Basic JSONB Queries

Retrieving the Entire JSON Document

To get the complete JSON document:

sql
SELECT details FROM products WHERE name = 'Smartphone';

Output:

{"brand": "TechX", "model": "TX-5", "specs": {"ram": "8GB", "storage": "128GB", "cpu": "OctaCore 2.5GHz"}}

Accessing Top-Level Keys

Use the -> operator to access specific keys as JSON:

sql
SELECT details->'brand' AS brand FROM products WHERE name = 'Smartphone';

Output:

"TechX"

Use the ->> operator to access specific keys as text:

sql
SELECT details->>'brand' AS brand FROM products WHERE name = 'Smartphone';

Output:

TechX

Accessing Nested Objects

For nested objects, chain the operators:

sql
SELECT details->'specs'->>'ram' AS ram FROM products WHERE name = 'Smartphone';

Output:

8GB

Filtering with JSONB

Filtering by a Top-Level Property

Find products of a specific brand:

sql
SELECT name, details->>'model' AS model 
FROM products
WHERE details->>'brand' = 'TechX';

Output:

name       | model
-----------+-------
Smartphone | TX-5

Using the Contains Operator (@>)

The @> operator checks if the left JSONB contains the right JSONB:

sql
SELECT name 
FROM products
WHERE details @> '{"specs": {"ram": "16GB"}}';

This will return products that have 16GB of RAM.

Checking for Key Existence

The ? operator checks if a string exists as a top-level key:

sql
SELECT name 
FROM products
WHERE details ? 'brand';

This returns all products where "brand" is a top-level key in the details.

For nested keys, use the @> operator:

sql
SELECT name 
FROM products
WHERE details @> '{"specs": {}}' AND details->'specs' ? 'cpu';

Modifying JSONB Data

Updating an Entire JSONB Field

Replace the entire JSONB document:

sql
UPDATE products 
SET details = '{"brand": "TechX", "model": "TX-5", "specs": {"ram": "8GB", "storage": "256GB", "cpu": "OctaCore 2.8GHz"}, "colors": ["Black", "Silver"]}'
WHERE name = 'Smartphone';

Updating Specific Properties

To update specific properties without replacing the entire document, use the jsonb_set function:

sql
UPDATE products 
SET details = jsonb_set(
details,
'{specs,storage}',
'"256GB"'::jsonb
)
WHERE name = 'Smartphone';

The above example updates only the storage specification to "256GB".

Adding New Properties

To add new properties, use jsonb_set with create_if_missing set to true:

sql
UPDATE products 
SET details = jsonb_set(
details,
'{warranty}',
'"2 years"'::jsonb,
true
)
WHERE name = 'Smartphone';

Removing Properties

Use the - operator to remove a property:

sql
UPDATE products 
SET details = details - 'warranty'
WHERE name = 'Smartphone';

To remove a nested property:

sql
UPDATE products 
SET details = details #- '{specs,cpu}'
WHERE name = 'Smartphone';

Working with JSON Arrays

Accessing Array Elements

Use the array index (zero-based) with the -> operator:

sql
-- First, let's add an array to our data
UPDATE products
SET details = jsonb_set(
details,
'{colors}',
'["Black", "Silver", "Gold"]'::jsonb,
true
)
WHERE name = 'Smartphone';

-- Now access the first color
SELECT details->'colors'->0 AS first_color FROM products WHERE name = 'Smartphone';

Output:

"Black"

Checking Array Contents

Check if an array contains a specific value:

sql
SELECT name 
FROM products
WHERE details->'colors' ? 'Gold';

This returns products where 'Gold' is one of the colors.

Expanding JSON Arrays

Use jsonb_array_elements to expand arrays into rows:

sql
SELECT name, jsonb_array_elements_text(details->'colors') AS color
FROM products
WHERE name = 'Smartphone';

Output:

name       | color
-----------+-------
Smartphone | Black
Smartphone | Silver
Smartphone | Gold

Indexing JSONB

Creating a GIN Index

GIN (Generalized Inverted Index) indexes are perfect for JSONB:

sql
-- Index for the @> operator (contains)
CREATE INDEX idx_products_details ON products USING GIN (details);

-- Index specific to key existence operators (?, ?|, ?&)
CREATE INDEX idx_products_details_ops ON products USING GIN (details jsonb_path_ops);

The first index supports all operators, while the second is more compact and efficient for containment queries (@>).

Practical Examples

Building a Product Search Feature

Let's create a function to search products by any field in the JSONB:

sql
CREATE OR REPLACE FUNCTION search_products(search_term TEXT)
RETURNS TABLE (id INTEGER, name TEXT, match_details JSONB) AS $$
BEGIN
RETURN QUERY
SELECT p.id, p.name, p.details
FROM products p
WHERE
p.name ILIKE '%' || search_term || '%' OR
p.details::text ILIKE '%' || search_term || '%';
END;
$$ LANGUAGE plpgsql;

Creating a Dynamic Filtering System

For a more advanced application, you might need dynamic filtering:

sql
CREATE OR REPLACE FUNCTION filter_products(filter_conditions JSONB)
RETURNS TABLE (id INTEGER, name TEXT, details JSONB) AS $$
BEGIN
RETURN QUERY
SELECT p.id, p.name, p.details
FROM products p
WHERE p.details @> filter_conditions;
END;
$$ LANGUAGE plpgsql;

Example usage:

sql
-- Find all TechX smartphones with 8GB RAM
SELECT * FROM filter_products('{"brand": "TechX", "specs": {"ram": "8GB"}}');

Building a Product Catalog with Nested Features

A more complex example for an e-commerce site:

sql
CREATE TABLE product_catalog (
id SERIAL PRIMARY KEY,
category TEXT,
properties JSONB
);

INSERT INTO product_catalog (category, properties)
VALUES
('Electronics', '{"type": "Smartphone", "inventory": {"in_stock": true, "count": 45}, "features": ["5G", "Water resistant"], "variants": [{"color": "Black", "price": 999}, {"color": "Silver", "price": 1049}]}'),
('Electronics', '{"type": "Laptop", "inventory": {"in_stock": true, "count": 12}, "features": ["SSD", "Backlit keyboard"], "variants": [{"color": "Gray", "price": 1299}, {"color": "Silver", "price": 1299}]}');

Now we can query this data in interesting ways:

sql
-- Find products with specific features
SELECT category, properties->>'type' AS type
FROM product_catalog
WHERE properties->'features' ? 'Water resistant';

-- Find available products under a certain price
SELECT
category,
properties->>'type' AS type,
jsonb_array_elements(properties->'variants')->>'color' AS color,
(jsonb_array_elements(properties->'variants')->>'price')::numeric AS price
FROM product_catalog
WHERE
properties->'inventory'->>'in_stock' = 'true'
AND (jsonb_array_elements(properties->'variants')->>'price')::numeric < 1100;

Performance Considerations

When working with JSONB, keep these tips in mind:

  1. Use GIN indexes for tables with many JSONB queries
  2. Be specific with queries - target exactly what you need rather than scanning entire documents
  3. Consider denormalization for frequently accessed fields
  4. Use JSONB containment (@>) when possible as it's highly optimizable
  5. Monitor query performance with EXPLAIN ANALYZE

Summary

PostgreSQL's JSONB data type provides a powerful way to combine the flexibility of JSON with the reliability and performance of a relational database. Key points to remember:

  • JSONB stores data in a binary format, making it efficient for querying
  • Use -> to access JSON values and ->> to access text values
  • The @> operator is powerful for checking containment
  • GIN indexes optimize JSONB query performance
  • Functions like jsonb_set allow targeted updates without replacing entire documents

With these operations, you can build sophisticated applications that benefit from both structured and semi-structured data models.

Practice Exercises

  1. Create a table to store user profiles with preferences stored as JSONB
  2. Write a query to find users with specific preferences
  3. Update a nested field within a user's preferences
  4. Create a function that aggregates statistics from JSONB data
  5. Build a GIN index and compare query performance with and without it

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)