DevToolBoxFREE
Blog

PostgreSQL JSONB Guide: Querying, Indexing, and Full-Text Search

13 min readby DevToolBox

PostgreSQL JSONB (Binary JSON) is one of the most powerful features in modern PostgreSQL. Unlike the JSON type which stores text verbatim, JSONB stores parsed binary data — enabling fast querying, indexing, and efficient storage. This guide covers everything from basic operations to advanced indexing strategies and performance optimization.

JSON vs JSONB: Which to Use?

PostgreSQL has two JSON types: json and jsonb. For almost all use cases, use jsonb.

Featurejsonjsonb (recommended)
Storage formatText (verbatim)Binary (parsed)
Write speedFasterSlightly slower
Read speedSlower (re-parse)Faster
GIN indexingNoYes
Duplicate keysPreservedLast value wins
Key orderingPreservedSorted
Operators-> and ->>All operators

Creating Tables and Inserting JSONB Data

JSONB columns store any valid JSON value. Keys are sorted and duplicates are removed on storage.

-- Create a table with JSONB column
CREATE TABLE products (
    id          SERIAL PRIMARY KEY,
    name        TEXT NOT NULL,
    metadata    JSONB,
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- Insert JSONB data
INSERT INTO products (name, metadata) VALUES
    ('Widget A', '{"price": 9.99, "category": "widgets", "tags": ["sale", "new"], "specs": {"weight": 0.5, "color": "red"}}'),
    ('Gadget B', '{"price": 24.99, "category": "gadgets", "tags": ["popular"], "specs": {"weight": 1.2, "color": "blue"}, "stock": 150}'),
    ('Doohickey C', '{"price": 4.99, "category": "widgets", "tags": ["sale"], "specs": {"weight": 0.1, "color": "green"}, "rating": 4.5}');

Querying JSONB: Operators Reference

PostgreSQL provides rich operators for accessing and filtering JSONB data.

-- Access a top-level key (returns JSONB)
SELECT metadata -> 'price' FROM products;
-- Returns: 9.99, 24.99, 4.99 (as JSONB)

-- Access a key as text (use ->>)
SELECT metadata ->> 'category' FROM products;
-- Returns: 'widgets', 'gadgets', 'widgets' (as text)

-- Nested access
SELECT metadata -> 'specs' -> 'color' FROM products;
SELECT metadata #> '{specs,color}' FROM products;        -- path operator
SELECT metadata #>> '{specs,color}' FROM products;       -- path as text

-- Filter rows by JSONB value
SELECT name FROM products WHERE metadata ->> 'category' = 'widgets';

-- Filter by nested value
SELECT name FROM products WHERE (metadata -> 'specs' ->> 'weight')::float > 0.5;

-- Check if key exists
SELECT name FROM products WHERE metadata ? 'stock';

-- Check if ANY key in array exists
SELECT name FROM products WHERE metadata ?| ARRAY['stock', 'rating'];

-- Check if ALL keys exist
SELECT name FROM products WHERE metadata ?& ARRAY['price', 'category'];

-- Containment: does JSONB contain this sub-object?
SELECT name FROM products WHERE metadata @> '{"category": "widgets"}';

-- Is JSONB contained by?
SELECT name FROM products WHERE '{"category": "widgets"}' <@ metadata;

JSONB Operators Quick Reference

OperatorExampleDescription
->metadata -> 'key'Get JSONB value by key
->>metadata ->> 'key'Get text value by key
#>metadata #> '{a,b}'Get JSONB by path
#>>metadata #>> '{a,b}'Get text by path
@>metadata @> '{"k":"v"}'Containment (uses GIN)
<@'{"k":"v"}' <@ metadataIs contained by
?metadata ? 'key'Key exists
?|metadata ?| ARRAY['a','b']Any key exists
?&metadata ?& ARRAY['a','b']All keys exist
||metadata || '{"new":1}'Concatenate/merge
-metadata - 'key'Remove key
#-metadata #- '{a,b}'Remove by path

Modifying JSONB Data

Update, add, and remove fields within JSONB columns without replacing the entire value.

-- Update a specific key (PostgreSQL 14+: subscript syntax)
UPDATE products
SET metadata['price'] = '19.99'
WHERE name = 'Widget A';

-- jsonb_set: update nested key (older syntax, all versions)
UPDATE products
SET metadata = jsonb_set(metadata, '{price}', '19.99')
WHERE name = 'Widget A';

-- Update nested path
UPDATE products
SET metadata = jsonb_set(metadata, '{specs,color}', '"black"')
WHERE name = 'Widget A';

-- Add a new key
UPDATE products
SET metadata = metadata || '{"discount": 0.1}'::jsonb
WHERE name = 'Widget A';

-- Remove a key
UPDATE products
SET metadata = metadata - 'stock'
WHERE name = 'Gadget B';

-- Remove multiple keys
UPDATE products
SET metadata = metadata - ARRAY['stock', 'rating'];

-- Remove nested key
UPDATE products
SET metadata = metadata #- '{specs,weight}';

Indexing JSONB for Performance

The right index strategy can make JSONB queries as fast as queries on regular columns. GIN indexes are the workhorse for JSONB.

-- GIN index: most useful for JSONB (supports ?, ?|, ?&, @>, <@)
CREATE INDEX idx_products_metadata ON products USING GIN (metadata);

-- GIN index on a specific key (for equality queries on that key)
CREATE INDEX idx_products_category ON products
    USING GIN ((metadata -> 'category'));

-- B-tree index on extracted value (for range queries)
CREATE INDEX idx_products_price ON products
    USING BTREE ((( metadata ->> 'price' )::numeric));

-- Expression index for a nested text field
CREATE INDEX idx_products_color ON products
    USING BTREE ((metadata #>> '{specs,color}'));

-- Check index usage
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM products WHERE metadata @> '{"category": "widgets"}';

Aggregation and JSONB Functions

PostgreSQL provides powerful functions for aggregating, expanding, and constructing JSONB values.

-- jsonb_agg: aggregate rows into a JSON array
SELECT
    metadata ->> 'category' AS category,
    jsonb_agg(name ORDER BY name) AS product_names,
    COUNT(*) AS count,
    AVG((metadata ->> 'price')::numeric) AS avg_price
FROM products
GROUP BY metadata ->> 'category';

-- jsonb_object_agg: build a JSON object from key-value pairs
SELECT jsonb_object_agg(name, metadata ->> 'price') AS price_map
FROM products;

-- jsonb_array_elements: expand a JSONB array into rows
SELECT
    name,
    jsonb_array_elements_text(metadata -> 'tags') AS tag
FROM products;

-- jsonb_each: expand a JSONB object into key-value rows
SELECT
    name,
    key,
    value
FROM products,
     jsonb_each(metadata -> 'specs');

-- jsonb_build_object: construct JSONB from expressions
SELECT jsonb_build_object(
    'id', id,
    'name', name,
    'price', (metadata ->> 'price')::numeric,
    'in_stock', metadata ? 'stock'
) AS product_summary
FROM products;

Performance Tips

  • Always create a GIN index on JSONB columns you query frequently. Without an index, JSONB queries require sequential scans.
  • Extract frequently-queried fields into regular columns with generated columns: GENERATED ALWAYS AS (metadata ->> 'category') STORED.
  • Use jsonb instead of json — it is faster for reads (binary format, no re-parsing) and supports indexing.
  • Avoid deep nesting. Flat JSONB structures are easier to index and query. Consider normalizing very deep structures.
  • Use @> (containment) for filtering on known sub-objects — it uses the GIN index. Avoid ->> with LIKE for text search; use full-text search instead.

Frequently Asked Questions

When should I use JSONB instead of relational columns?

Use JSONB for: semi-structured data where the schema varies per row, storing configurations or settings, event payloads or metadata, and prototype development where the schema is not yet fixed. Use regular columns when: data is highly structured and uniform, you need complex JOINs, or data changes frequently with strict transactional guarantees.

How do I search for text inside a JSONB string value?

For full-text search inside JSONB strings, use: to_tsvector('english', metadata ->> 'description') @@ to_tsquery('search term'). Create a GIN index on the tsvector expression. For simple substring matching, use metadata ->> 'field' LIKE '%pattern%' but this cannot use a GIN index.

What is the maximum size of a JSONB value?

A single JSONB value in PostgreSQL can be up to 1 GB. However, for practical performance reasons, JSONB columns work best when individual values are under 1 MB. Large JSONB values slow down updates (the entire value must be rewritten) and indexing.

How do I validate JSONB schema in PostgreSQL?

PostgreSQL 16+ supports JSON Schema validation with the jsonb_matches_schema() function. For older versions, use CHECK constraints with custom functions. Example: ALTER TABLE products ADD CONSTRAINT check_metadata CHECK (jsonb_typeof(metadata -> 'price') = 'number');

How does JSONB compare to a dedicated document database like MongoDB?

JSONB gives you document storage within PostgreSQL, combining the flexibility of document databases with ACID transactions, complex JOINs, full-text search, and all other PostgreSQL features. MongoDB offers native document-first querying and horizontal sharding. For most applications that already use PostgreSQL, JSONB is preferred over adding a separate database.

Related Tools

𝕏 Twitterin LinkedIn
Was this helpful?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Try These Related Tools

{ }JSON Formatter🔷JSON to TypeScript OnlineSQLSQL Formatter

Related Articles

GraphQL vs REST API: When to Use Which in 2026

In-depth comparison of GraphQL and REST APIs with code examples. Learn architecture differences, data fetching patterns, caching, and when to choose each approach.

API Rate Limiting: Strategies, Algorithms, and Implementation Guide

Complete guide to API rate limiting. Learn token bucket, sliding window, leaky bucket algorithms with code examples. Includes Express.js middleware, Redis distributed rate limiting, and best practices.