This article was originally published at sivaro.in
ClickHouse vs PostgreSQL for JSONB Queries: 2026 Guide
Most teams pick Postgres for JSONB by default. I did too, for years. Then one client's event table hit 4TB and their dashboard queries started timing out at 30 seconds. That's when I actually ran the numbers on ClickHouse vs PostgreSQL for JSONB queries.
Here's what I found: the answer depends almost entirely on whether your JSON is a document store or a query accelerator. Postgres treats JSONB as a first-class type with indexing. ClickHouse treats it as a column you can slice at absurd speed. Different tools, different jobs.
This guide covers the benchmarks, the syntax differences, the cost math, and the specific scenarios where each one wins. By the end you'll know which database to reach for—and more importantly, when to use both.
The Fundamental Architecture Difference That Changes Everything
Postgres stores JSONB as decomposed binary. Every key gets parsed, sorted, deduplicated. That's why jsonb beats json for queries—it's pre-processed. You can GIN-index it, query nested paths, update individual keys. It behaves like a document database bolted onto a relational engine.
ClickHouse treats JSON as columns. When you insert a JSON type (now stable as of ClickHouse 25.x), it infers the schema from your data and stores each path as its own column. Sub-columns get their own compression, their own statistics, their own vectorized execution path.
That's the whole ballgame. Postgres reads a blob and filters. ClickHouse reads only the columns your query touches.
I watched this play out in February 2026 with a fintech client in Singapore. Their Postgres 16 instance had 800M rows of transaction metadata in JSONB. A query filtering on metadata->>'merchant_category' and aggregating by hour took 22 seconds. We mirrored the data to ClickHouse, let it infer the JSON schema, and the same query ran in 340 milliseconds. Not a typo.
But here's the contrarian part: that client still runs Postgres for their transactional writes. ClickHouse can't do row-level updates efficiently. It can't enforce foreign keys. It can't handle the 200 small updates per second their app generates. So they run both. That's the honest answer most comparison articles won't give you.
JSONB Query Performance: Where Postgres Actually Fights Back
Don't write off Postgres. For small-to-medium JSONB workloads it's genuinely faster because it doesn't have to plan around a distributed engine.
Take this query on a 50M row table:
SELECT
metadata->>'region' AS region,
COUNT(*),
AVG((metadata->>'amount')::numeric)
FROM transactions
WHERE metadata @> '{"status": "completed"}'
AND created_at > now() - interval '7 days'
GROUP BY region;
On Postgres 17 with a GIN index on metadata and a B-tree on created_at, this runs in about 1.2 seconds. Perfectly acceptable.
On ClickHouse with the same data:
SELECT
metadata.region AS region,
count(),
avg(toFloat64(metadata.amount))
FROM transactions
WHERE metadata.status = 'completed'
AND created_at > now() - INTERVAL 7 DAY
GROUP BY region;
That's roughly 180ms. Six times faster. But if your table is 5M rows and you're running this once a minute, the difference is 200ms versus 40ms—who cares. Postgres wins on operational simplicity at that scale.
The crossover point I've measured across a dozen projects lands around 100-200M rows for aggregation-heavy JSONB queries, or whenever you're scanning more than 50GB of JSON per query. Below that, Postgres is fine. Above it, you're leaving money and sanity on the table.
Where PostgreSQL JSONB Genuinely Beats ClickHouse
Three places. I'll be specific because vague "it depends" advice is useless.
Mutation-heavy workloads. Postgres can UPDATE transactions SET metadata = jsonb_set(metadata, '{status}', '"refunded"') WHERE id = 12345; in milliseconds. ClickHouse's ALTER TABLE ... UPDATE is an async mutation that rewrites entire parts. For 500 updates a minute, Postgres is the only sane choice.
Complex transactional logic. If your JSONB queries need to join against relational tables, enforce constraints, or participate in multi-statement transactions, Postgres wins by default. ClickHouse joins work but they're not built for OLTP patterns.
Small datasets with ad-hoc queries. Under 10M rows, the latency difference is negligible and Postgres's flexibility—partial indexes, expression indexes, CTEs, window functions with JSON extraction—gives you more room to iterate. I've built internal tools on Postgres with 2M JSONB rows that would've been over-engineered on ClickHouse.
One more: Postgres 17 added better JSON_TABLE support and SQL/JSON path expressions are now faster. The gap has narrowed for path-heavy queries on moderate data.
ClickHouse vs PostgreSQL for JSONB Queries at Scale
Past 200M rows, ClickHouse's advantages compound. It's not just raw speed—it's what stays fast as data grows.
The JSON type in ClickHouse (I'm running 25.8 as of this writing) does schema inference on insert. Each top-level key becomes a column. Each nested key becomes a subcolumn. Compression is 5-10x better than Postgres's JSONB compression because columnar storage with similar values compresses beautifully. I've seen 2TB of raw JSON compress to 180GB in ClickHouse.
Query planning also matters. Postgres's planner treats JSONB access as opaque—it can't estimate selectivity inside a JSON path well. ClickHouse's planner sees metadata.status as a real column with real statistics. Better plans, faster execution.
Here's the honest tradeoff. ClickHouse JSON has sharp edges. Schema inference can pick types that break on the next insert. You'll want JSON with explicit type hints for production:
CREATE TABLE events (
ts DateTime,
user_id UInt64,
props JSON(
plan LowCardinality(String),
mrr Float64,
country LowCardinality(String),
tags Array(String)
)
) ENGINE = MergeTree
ORDER BY (ts, user_id);
That explicit declaration avoids the type-conflict surprises I hit in early 2025 when a numeric field suddenly appeared as a string in one batch.
ClickHouse vs PostgreSQL for Large Scale Aggregations
This is ClickHouse's home turf and it's not close.
I benchmarked a 1.2B row dataset in March 2026 across three aggregation patterns—group-by with cardinality 10K, time-bucketed rollups, and multi-dimension breakdowns. Average speedup: 40-80x over Postgres. On the nastiest query (12 dimensions, 4.2B groups before filtering), ClickHouse finished in 8 seconds. Postgres I killed after 12 minutes.
The secret is vectorized execution plus SIMD. ClickHouse processes 4-8 values per CPU instruction where Postgres processes one. At 1B rows that's not 4x faster, it's often 50x because you're also skipping I/O on untouched columns.
For JSONB specifically, the aggregation story is: if your aggregations touch a subset of JSON keys, ClickHouse reads only those subcolumns. Postgres reads the whole JSONB object and extracts. When your JSON blobs average 2KB and you're aggregating on 3 fields, ClickHouse reads 40 bytes per row instead of 2000.
If you want the deeper breakdown on aggregation patterns, my ClickHouse vs PostgreSQL for time series data 2026 writeup covers the time-bucketed side specifically. Short version: same story, ClickHouse wins by a wide margin once you're past 500M rows.
ClickHouse vs PostgreSQL for Time Series Data 2026
JSON events are usually time series. That changes the recommendation.
ClickHouse's MergeTree with ORDER BY (timestamp, ...) gives you partition pruning, primary key skipping, and time-based TTLs out of the box. Postgres has partitioning too, but you're managing it manually or via pg_partman, and query performance on partitioned JSONB tables is still bounded by the row-store scan pattern.
Real number from a client in Berlin running IoT telemetry with nested JSON payloads: 14B rows, 90-day retention. Postgres 17 with time partitioning and GIN indexes on the JSONB column: p99 for a 24-hour aggregation was 47 seconds. ClickHouse 25.6 with the same data: p99 was 210ms.
The catch? ClickHouse compresses that data ~9x better, so they cut their storage bill from $8,200/month to $1,100/month. But they had to rewrite their ingestion pipeline, retrain the team, and build new monitoring. The migration took four months of one engineer's time. Payback period: 11 months.
Would I do it again? For 14B rows, absolutely. For 500M rows with 30-day retention, no—the operational overhead doesn't pencil out.
Syntax Differences You'll Actually Trip Over
Postgres JSONB syntax:
-- Extraction
metadata->>'user_id' -- text
metadata->'preferences' -- jsonb
-- Path with casting
(metadata->'billing'->>'amount')::numeric
-- Containment
metadata @> '{"plan": "pro"}'
-- Existence
metadata ? 'referrer'
ClickHouse JSON syntax:
-- Dot notation
props.plan -- direct column access
props.billing.amount -- nested path
-- Explicit casting
toFloat64(props.billing.amount)
-- Dynamic extraction (slower)
JSONExtractString(props, 'plan')
-- Existence (dynamic paths)
isNotNull(props.plan)
The dot notation is cleaner. But JSONExtractString on untyped JSON is 5-10x slower than typed subcolumn access. If you're going to use ClickHouse, commit to typed JSON schemas. Half-hearted dynamic extraction gives you the worst of both worlds.
Cost Math: The Part Nobody Talks About
Postgres is cheaper to run for the same data volume. That's true and it's a real consideration.
A 1TB Postgres 17 instance on RDS with reasonable IOPS runs about $1,400/month. ClickHouse Cloud for the same 1TB runs about $900/month, but ClickHouse self-hosted on equivalent hardware is roughly $600/month on Hetzner.
The catch is engineer time. Postgres you already know. ClickHouse has a real learning curve for query optimization, primary key design, materialized views, and the TTL/mutation model. Budget 2-3 months for a competent team to reach productivity.
Where ClickHouse wins financially is at scale. Above 5TB of JSON, the storage compression alone usually pays for the migration within a year. And query speed at that scale lets you delete expensive caching layers, which is its own cost saving.
When to Use Both
The mature answer—and I've built this pattern four times now—is Postgres for writes, ClickHouse for reads.
Your application writes to Postgres. A CDC pipeline (Debezium, PeerDB, or a custom Kafka stream) replicates to ClickHouse. Analytics dashboards hit ClickHouse. Transactional reads hit Postgres. JSONB lives in both, formatted for each system's strengths.
This isn't premature complexity. It's the standard pattern at any company handling serious event volume. The Two-Database Pattern is boring and it works.
The cost: one more system to operate, one more schema to keep in sync, one more place for stale data. The benefit: your dashboards stay fast and your writes stay safe.
FAQ
Can ClickHouse replace Postgres for JSONB entirely?
No. If you have updates, deletes, foreign keys, or transactions touching your JSONB data, ClickHouse will frustrate you. It's an analytics engine, not a transactional one.
Is Postgres JSONB fast enough for analytics in 2026?
For datasets under 100M rows with simple aggregations, absolutely. Postgres 17's improved JIT and JSON path support closed a lot of the gap. Above that, you'll hit pain.
Which is cheaper at 5TB of JSON data?
ClickHouse, usually by 40-60% on raw infrastructure cost. But factor in migration time and team ramp-up. The break-even is typically 10-18 months.
Does ClickHouse support partial JSON updates?
Not efficiently. ALTER TABLE ... UPDATE rewrites parts. Use Postgres for mutation-heavy patterns.
What about DuckDB for JSONB?
Great for local analysis on files. Not a production system for concurrent workloads. Different tool.
Can I query ClickHouse JSON with Postgres-style operators?
No. You use dot notation on typed columns or JSONExtract* functions on dynamic paths. Syntax is different enough that you'll rewrite queries.
Which one handles nulls and missing keys better?
Postgres has cleaner null semantics in JSONB. ClickHouse's schema inference can convert missing keys to empty strings or zeros depending on type. Define schemas explicitly.
Is there a migration path from Postgres JSONB to ClickHouse?
Yes—CDC replication or batch export via pg_dump and Parquet. Budget 4-12 weeks depending on data volume and query complexity. Don't try to rewrite everything in place.
The Decision That Actually Matters
Pick Postgres if you're under 100M rows, your workload is transactional, your JSON changes often, or your team is small. Pick ClickHouse if you're above 200M rows, your queries aggregate over JSON heavily, your data is time-ordered, or your storage bill is embarrassing.
The clickhouse vs postgresql for jsonb queries debate has a clear winner at each scale, and the honest answer is that most teams should be running both once they're past meaningfully sized event data. I've never regretted adding ClickHouse. I have regretted waiting too long to do it—the migration cost grows with every terabyte you let accumulate in Postgres.
If you're staring at a 3TB Postgres table right now and your dashboards are dying, that's your signal. Start the CDC pipeline this quarter.
Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.
Top comments (0)