PostgreSQL JSONB in 2026: When a Relational Database Replaces MongoDB
Introduction: From hstore to JSONB
The history of storing semi-structured data in PostgreSQL predates the NoSQL movement going mainstream. In 2006, hstore appeared — an extension for storing key–value pairs in string format. It worked, but didn't support nesting and had limited indexing capabilities.
PostgreSQL 9.2 (2012) introduced the json type — text-based storage with basic validation. The turning point came with PostgreSQL 9.4 (2014), which introduced jsonb — a binary, indexable representation of JSON. Since then, every major release has added new features: jsonb_path_query (SQL/JSON Path in version 12), subscripting and jsonb_set_lax in version 14, improved JSON Schema support in version 16, and in PostgreSQL 17 (2024) and the upcoming version 18 (2025–2026) — further query planner optimizations for JSONB queries.
Today, in 2026, PostgreSQL JSONB is a mature tool that covers most of the scenarios for which developers historically reached for MongoDB. Let's break down when this is justified — and when it isn't.
JSONB vs JSON: Key Differences
PostgreSQL offers two types: json and jsonb. The difference is fundamental.
- json stores data as text, preserving the original key order and whitespace. Validation happens on insert, but parsing occurs on every read. Fast writes, slow reads.
- jsonb stores data in binary format: keys are deduplicated, sorted, and duplicates are removed. Writes are slightly slower due to parsing, but reads and indexing are significantly faster.
The rule is simple: use json only when preserving the original document format is critical (for example, for an audit log of raw requests). In all other cases — always use jsonb.
Indexing JSONB: GIN, GiST, and Partial Indexes
The main advantage of jsonb over json is the ability to index efficiently. PostgreSQL offers several strategies.
GIN Index: Full Document Coverage
GIN (Generalized Inverted Index) is the standard choice for JSONB. It indexes all keys and values in a document, enabling the use of operators @>, ?, ?|, ?&.
-- Create a table with a JSONB field
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
data JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Full GIN index
CREATE INDEX idx_products_data_gin ON products USING GIN (data);
-- Query: find all products with the 'electronics' tag
SELECT id, data->>'name'
FROM products
WHERE data @> '{"tags": ["electronics"]}';
-- Check index usage
EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM products
WHERE data @> '{"category": "laptop"}';
GIN with jsonb_path_ops: Faster but Narrower
The jsonb_path_ops operator class creates a more compact index — only for the @> operator, but with a smaller size and higher search speed.
CREATE INDEX idx_products_path ON products
USING GIN (data jsonb_path_ops);
-- This index is used by the @> operator
SELECT * FROM products
WHERE data @> '{"specs": {"ram": 16}}';
Index on a Specific Field
If you regularly filter by a single field, it's more efficient to create a B-tree expression index:
-- Index on the price field inside JSONB
CREATE INDEX idx_products_price
ON products ((data->>'price')::NUMERIC);
-- This query now uses the B-tree index
SELECT * FROM products
WHERE (data->>'price')::NUMERIC > 1000;
Partial Index
-- Index only for active products
CREATE INDEX idx_active_products
ON products USING GIN (data)
WHERE (data->>'status') = 'active';
JSONB Operators and Functions: A Practical Overview
PostgreSQL provides a rich set of tools for working with JSONB.
Access and Existence Operators
-- Get value by key (returns jsonb)
SELECT data->'specs' FROM products;
-- Get value as text
SELECT data->>'name' FROM products;
-- Access by path
SELECT data#>'{specs,memory}' FROM products;
SELECT data#>>'{specs,memory}' FROM products; -- as text
-- Check for key existence
SELECT * FROM products WHERE data ? 'discount';
-- Does the document contain a sub-document?
SELECT * FROM products
WHERE data @> '{"brand": "Dell"}';
-- JSON Path (PostgreSQL 12+)
SELECT jsonb_path_query(data, '$.specs.ram ? (@ > 8)')
FROM products;
Modifying JSONB
-- Set a value
UPDATE products
SET data = jsonb_set(data, '{specs,ram}', '32')
WHERE id = 1;
-- Delete a key
UPDATE products
SET data = data - 'old_field'
WHERE id = 1;
-- Merge objects (PostgreSQL 9.5+)
UPDATE products
SET data = data || '{"featured": true}'
WHERE id = 1;
-- jsonb_set_lax (PostgreSQL 14+): handle null path
UPDATE products
SET data = jsonb_set_lax(data, '{discount}', 'null', true, 'use_json_null')
WHERE id = 1;
Aggregation and Transformation
-- Expand an array into rows
SELECT id, jsonb_array_elements(data->'tags') AS tag
FROM products;
-- Aggregate rows into a JSONB array
SELECT jsonb_agg(data->>'name') FROM products;
-- Build an object
SELECT jsonb_object_agg(data->>'sku', data->>'price')
FROM products;
Real-World Scenarios for Replacing MongoDB with PostgreSQL JSONB
Flexible Schema Without Migrations
One of the main arguments for MongoDB is the ability to store documents with varying structures. PostgreSQL JSONB handles this just as well. The classic pattern is a hybrid schema: fixed columns for critical attributes and a jsonb column for extensible data.
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
event_type TEXT NOT NULL,
user_id BIGINT REFERENCES users(id),
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
payload JSONB NOT NULL DEFAULT '{}'
);
-- Events with completely different structures can be stored
INSERT INTO events (event_type, user_id, payload) VALUES
('page_view', 1, '{"url": "/home", "referrer": "google.com"}'),
('purchase', 1, '{"order_id": 42, "amount": 299.99, "currency": "USD"}'),
('signup', 2, '{"plan": "pro", "trial": true, "source": "organic"}');
Nested Documents
MongoDB is popular in scenarios with multi-level nesting. PostgreSQL handles this efficiently:
-- Find users with a shipping address in Moscow
SELECT id, data->>'email'
FROM users
WHERE data @> '{"addresses": [{"city": "Moscow", "type": "shipping"}]}';
-- JSON Path for complex conditions
SELECT id
FROM orders
WHERE jsonb_path_exists(
data,
'$.items[*] ? (@.price > 100 && @.quantity > 1)'
);
Working with Arrays
-- Find products with both 'sale' AND 'new' tags
SELECT * FROM products
WHERE data @> '{"tags": ["sale"]}'
AND data @> '{"tags": ["new"]}';
-- Array length
SELECT id, jsonb_array_length(data->'images') AS image_count
FROM products
WHERE jsonb_array_length(data->'images') > 3;
Performance: JSONB vs MongoDB in 2026
Objective benchmarks from 2025–2026 (independent tests on datasets ranging from 1 to 50 million documents) paint the following picture.
- Point lookup by key with a GIN index: PostgreSQL JSONB — 0.3–0.8 ms, MongoDB — 0.2–0.6 ms. Near parity; MongoDB is slightly faster due to its specialized architecture.
- Complex aggregations with JOINs: PostgreSQL wins by 40–60% — the query planner is significantly more efficient for mixed relational/JSON queries.
- Bulk insert: MongoDB is 20–30% faster without indexes; with GIN indexes, the gap narrows to 10–15%.
- Full scan on JSONB without an index: Comparable results — both are slow. Indexes are essential.
- Transactional scenarios with JSONB + relational data: PostgreSQL is 2–3x faster due to the absence of network overhead between services.
Key takeaway: if your application mixes document and relational data, PostgreSQL JSONB offers an advantage not only in performance but also in operational simplicity. One service instead of two.
Integration with Laravel and Go
Laravel: Working with JSONB via Eloquent
Laravel supports JSONB fields natively through casting and where operators.
// Migration
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->jsonb('data')->default('{}');
$table->timestamps();
});
// Model
class Product extends Model
{
protected $casts = [
'data' => 'array',
];
}
// Search by nested field
$laptops = Product::whereRaw(
"data @> ?",
[json_encode(['category' => 'laptop'])]
)->get();
// Update a single key without overwriting the entire object
DB::statement(
"UPDATE products SET data = jsonb_set(data, '{specs,ram}', ?) WHERE id = ?",
['32', $product->id]
);
// GIN index in migration
DB::statement('CREATE INDEX idx_products_gin ON products USING GIN (data)');
Go: Working with JSONB via pgx
In Go, the pgx library is the de facto standard for working with PostgreSQL. JSONB fields are scanned directly into structs via the pgtype interface.
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
)
type ProductData struct {
Name string `json:"name"`
Price float64 `json:"price"`
Tags []string `json:"tags"`
Specs map[string]interface{} `json:"specs"`
}
func getProducts(pool *pgxpool.Pool) ([]ProductData, error) {
rows, err := pool.Query(
context.Background(),
`SELECT data FROM products
WHERE data @> $1
ORDER BY (data->>'price')::NUMERIC DESC
LIMIT 20`,
[]byte(`{"tags": ["sale"]}`),
)
if err != nil {
return nil, err
}
defer rows.Close()
var results []ProductData
for rows.Next() {
var raw []byte
if err := rows.Scan(&raw); err != nil {
return nil, err
}
var pd ProductData
if err := json.Unmarshal(raw, &pd); err != nil {
return nil, err
}
results = append(results, pd)
}
return results, nil
}
func updateSpec(pool *pgxpool.Pool, id int64, ram int) error {
_, err := pool.Exec(
context.Background(),
`UPDATE products
SET data = jsonb_set(data, '{specs,ram}', $1::jsonb)
WHERE id = $2`,
fmt.Sprintf("%d", ram),
id,
)
return err
}
JSONB Limitations and When MongoDB Is Still the Better Choice
An honest analysis requires acknowledging the limitations of PostgreSQL JSONB.
- Horizontal scaling. PostgreSQL scales vertically very well, but horizontal sharding is more complex. MongoDB Atlas or native MongoDB sharding wins at volumes of tens of terabytes and beyond.
- Schema flexibility at the application level. If your team has historically worked with an ODM approach (Mongoose, Doctrine ODM), switching paradigms requires effort.
- Change Streams. MongoDB provides native Change Streams for reactive architectures. PostgreSQL LISTEN/NOTIFY + logical replication covers 80% of use cases, but requires additional configuration.
- Very large JSONB documents. Documents larger than 1 MB in PostgreSQL are stored via TOAST, which introduces overhead. MongoDB is optimized for large documents (up to 16 MB).
- No native document database API. If your product requires the MongoDB Wire Protocol (e.g., compatibility with existing clients), PostgreSQL won't help without additional proxies.
Practical Schema Design Recommendations for JSONB
- Hybrid schema is the gold standard. Move fields you frequently filter or join on into separate columns:
user_id,status,created_at. Everything else goes intodata JSONB. - One GIN index vs. multiple expression indexes. A full GIN index is convenient but heavy. For high-load applications, create indexes on specific expressions:
(data->>'status'),(data->>'user_id')::BIGINT. - Normalize "hot" fields. If you find yourself filtering on
data->>'email'in 80% of queries — move email to a dedicated column with a standard B-tree index. - Document versioning. Add a
schema_version INTfield alongsidedata JSONB. This allows you to safely migrate document structure without downtime. - Check query plans regularly.
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)is your best tool. A GIN index may not be used if statistics are stale — runANALYZE productsafter bulk inserts. - Use JSON Schema Validation (PostgreSQL 16+). The built-in
jsonb_matches_schemafunction lets you validate documents at the database level without additional layers.
-- Example hybrid schema
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'pending',
total NUMERIC(10,2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
metadata JSONB NOT NULL DEFAULT '{}'
);
CREATE INDEX idx_orders_user ON orders (user_id);
CREATE INDEX idx_orders_status ON orders (status);
CREATE INDEX idx_orders_meta ON orders USING GIN (metadata jsonb_path_ops);
-- Query efficiently uses both indexes
SELECT id, total, metadata->>'source'
FROM orders
WHERE user_id = 42
AND status = 'completed'
AND metadata @> '{"promo": true}';
Conclusion
PostgreSQL JSONB in 2026 is not a compromise — it's a fully capable solution for most of the tasks for which developers have historically chosen MongoDB. Flexible schemas, nested documents, arrays, powerful GIN indexing, a rich set of operators, and SQL/JSON Path make PostgreSQL competitive in the document database space.
The key advantage is a single database for both relational and document data. This reduces operational complexity, simplifies transactions, and lets you leverage the full power of SQL for complex analytical queries.
That said, MongoDB remains the better choice for scenarios requiring native horizontal sharding, Change Streams, and very large documents. If your system already runs on PostgreSQL — don't rush to introduce MongoDB just "for flexibility." Chances are, JSONB already solves your problem.
Integration via Laravel Eloquent and Go pgx is mature and predictable. Start with a hybrid schema, create the right indexes, regularly review query plans — and PostgreSQL's NoSQL capabilities will meet your expectations.
Technologies
Tags
Ruslan Ismailov
Senior Web / Backend Developer. Senior web/backend developer with 9 years of experience. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservices, CI/CD. More about me →