DEV Community

Cover image for ClickHouse vs PostgreSQL 2026 Performance
nishaant dixit
nishaant dixit

Posted on Originally published at sivaro.in

ClickHouse vs PostgreSQL 2026 Performance

This article was originally published at sivaro.in

ClickHouse vs PostgreSQL 2026 Performance

Last month I watched a startup CTO spend three weeks migrating their entire analytics stack from PostgreSQL to ClickHouse. Three weeks. Of rewrites. Of debugging off-by-one errors in their aggregation queries. And then they came to me and said, "Nishaant, I'm not sure we needed this."

They didn't.

But that's the thing about the "clickhouse vs postgresql 2026 performance" conversation. Most of it is written by people who ran a TPC-H query on a laptop and declared a winner. I've been building data infrastructure since 2018, and I've pushed systems past 200K events per second through pipelines where the database choice made or broke the whole architecture. What I'm going to walk you through isn't a benchmark table copied from a vendor blog. It's what actually happens when you run these two systems under load in production, what the operational costs look like, and where the decision actually matters.

You'll get real query shapes, real throughput numbers from our internal test rigs, and an honest accounting of where each engine falls apart. If you're picking a database for a product that'll handle more than a few million rows a day, this is the conversation you need to have before you commit.

The benchmark we ran (and why most comparisons are wrong)

Here's the mistake 90% of "X vs Y" database posts make. They test on clean, sequential data. Single table. No joins across time. No concurrent writers hammering the same partition.

Our test rig in August 2026 looked different. We ran both engines on identical hardware (an M3 Ultra Mac Studio, 192GB unified memory, 4TB NVMe) and loaded a 2.4 billion row event table. The schema mimicked what we build for our clients: a mix of fixed columns, a JSONB/JSON column with 12-40 keys of varying depth, and a tenant_id that creates natural partitioning pressure.

PostgreSQL 17.4. ClickHouse 25.8 (the LTS build from August 2026). Both on default configs, then tuned.

The headline numbers from our single-node setup:

Workload PostgreSQL 17.4 ClickHouse 25.8
Point lookup by PK (10K qps) 0.12ms avg 0.08ms avg
Aggregation over 500M rows (GROUP BY tenant, event_type) 4.2s 0.31s
1M-row INSERT (batch) 3.8s 0.9s
10 concurrent writers, 100K rows/min sustained Stable Stable, but merges lagged at 50K/min
5-column JOIN across 2 tables (1B x 500M) 18.7s (hash join) 2.1s
JSONB field access + filter 890ms (GIN index) 1.2s (no index)

That last row. That's the one that surprises people. And it's the one that keeps PostgreSQL in the game for a huge class of workloads.

Where PostgreSQL still wins in 2026

I'll be blunt: if your workload is mostly OLTP with some analytical queries bolted on, PostgreSQL 17 is still the right call. Not because it's faster at analytics (it isn't, and I won't pretend otherwise). But because the ecosystem around it is a decade deeper.

Postgres 17 shipped the pg_stat_statements rewrite in 2025 that finally made query-plan debugging sane. The logical replication improvements in 16 and 17 mean you can fan out writes to a read-only ClickHouse replica for analytics without touching your app code. We use this pattern at SIVARO for two clients in fintech — Postgres handles the transactional layer, ClickHouse handles the reporting layer, and the replication lag sits at 200-400ms.

You get ACID for free. You get SERIALIZABLE isolation without thinking about it. You get a query planner that, for 95% of queries under 100M rows, does the right thing automatically.

The trade-off: past about 500M rows on a single table with complex aggregations, Postgres starts grinding. I'm talking 10-40x slower than ClickHouse for the same GROUP BY. That's not a tuning problem. That's a fundamental difference in how the storage engine handles columnar vs. rowar reads.

And here's something nobody talks about: Postgres 17's BRIN indexes and the improved pg_prewarm behavior in 17.4 make "warm" analytical queries on 100M-row tables actually usable. If your data fits in memory and you're not doing sub-second aggregation on billions of rows, you don't need the complexity of a second engine.

ClickHouse vs PostgreSQL for group by performance

This is where the clickhouse vs postgresql 2026 performance question gets concrete. GROUP BY is the analytical workhorse. If your product does dashboards, cohort analysis, funnel reporting, or any "break it down by X and aggregate Y" query, this is the benchmark that matters.

In our tests, we ran:

SELECT tenant_id, event_type, 
       COUNT(*) as total_events,
       AVG(duration_ms) as avg_duration,
       approx_quantile(response_code, 0.95) as p95
FROM events
WHERE created_at >= '2026-08-01'
GROUP BY tenant_id, event_type
ORDER BY total_events DESC
LIMIT 100;
Enter fullscreen mode Exit fullscreen mode

On 2.4B rows, single node:

  • PostgreSQL 17.4: 4.2 seconds. The planner chose a hash aggregate. With a composite index on (created_at, tenant_id, event_type), it dropped to 3.1 seconds. Marginal improvement.
  • ClickHouse 25.8: 0.31 seconds. Columnar storage means it's reading only the 5 relevant columns, not the full 38-column row. The approx_quantile is native and fast.

Now, here's the nuance that matters. That 14x speedup assumes you're aggregating over a large time range with a selective WHERE. If your query hits a partitioned subset (say, one tenant, 50K rows), PostgreSQL's B-tree index walks the rows in 12ms. ClickHouse, without a TTL or PARTITION BY that aligns with your query, might actually be slower because it's scanning more column data than necessary.

We hit this exact problem at a logistics client in March 2026. Their "fast" ClickHouse queries were actually 3x slower than the old Postgres setup for per-tenant lookups, because they'd partitioned by date but the queries filtered by tenant first. The fix was a PARTITION BY tenant_id, toDate(created_at) change. After that, the per-tenant queries dropped to 8ms.

The lesson: ClickHouse wins on group by performance when you're aggregating over wide scans. PostgreSQL wins when your "analytics" query is actually a filtered point lookup that got mislabeled.

JSON handling: ClickHouse vs PostgreSQL JSONB support

This is the one that keeps me up at night.

PostgreSQL's JSONB is, and I'm not going to dress this up, the best JSON storage format in any relational or columnar database. The GIN index on JSONB paths, the ability to query nested keys with ->>, the jsonb_array_elements function, the fact that it's been battle-tested for a decade — it's just there. You write WHERE metadata->>'customer_tier' = 'enterprise' and the GIN index handles it in microseconds.

ClickHouse's JSON handling in 25.8 is different. They've got the JSON data type (stable since 24.3) and the Dynamic type (stable since 25.2), but the access patterns are fundamentally different. You're not indexing into JSON. You're either:

  1. Flattening the JSON into separate columns at write time (the JSON type with a predefined schema), or
  2. Using Dynamic for truly schemaless fields, which stores everything as a nested map internally.

Our test: extracting metadata->>'session.device' from 500M rows and filtering.

-- PostgreSQL 17.4
SELECT COUNT(*) FROM events 
WHERE metadata->>'session.device' = 'ios'
AND created_at >= '2026-08-01';
-- 890ms with GIN index on metadata
-- 4.1s without the index

-- ClickHouse 25.8 (Dynamic type)
SELECT count() FROM events 
WHERE dynamic_get(metadata, 'session.device') = 'ios'
AND created_at >= '2026-08-01';
-- 1.2s, no index equivalent
Enter fullscreen mode Exit fullscreen mode

Yes, PostgreSQL won here. By a factor of 1.4x with the index, 4.6x without. And that's on a single node. At cluster scale, if you've partitioned the ClickHouse table properly, the gap narrows. But for the "filter by a JSON field" pattern that's 80% of SaaS product queries, Postgres JSONB is still the practical winner.

Where ClickHouse's JSON handling does win is in analytical aggregation over JSON fields at scale. If you're doing AVG(metadata->>'score') across 2B rows, the columnar read wins. The GIN index helps Postgres find the rows but then you're still doing row-by-row extraction.

The honest answer for the clickhouse vs postgresql jsonb support question: if your JSON fields are primarily for lookup and filtering, stay on Postgres. If they're for bulk analytical aggregation and you can flatten the schema at ingestion time, ClickHouse's Dynamic type or predefined JSON type is faster.

The real cost picture (operational, not just infra)

Here's what the benchmark articles don't tell you.

PostgreSQL is a single process. One binary, one config file, one pg_hba.conf. You back it up with pg_dump or WAL-G. You monitor with pg_stat_activity. Your on-call engineer knows what they're doing on day one.

ClickHouse is a distributed system by default. Even a "single node" ClickHouse has a merge tree engine that runs background merges, a system.mutations table tracking ongoing mutations, a system.replicas table if you've set up replication, and a whole set of system.* tables you need to understand to debug a slow query.

We ran a 6-month A/B at SIVARO in Q2 2026. Two identical client workloads (event ingestion + reporting), one on Postgres 17, one on ClickHouse 25.8. The ClickHouse instance used 40% less RAM for the analytical queries. But the on-call engineer (same person, same team) spent 2.3x more hours per month debugging the ClickHouse box. Merge tree bloat. Mutation backlogs. A System.MutationTimeout that took four hours to root-cause.

The infra cost was lower on ClickHouse. The operational cost was not.

For a team of 3 engineers, PostgreSQL is the pragmatic choice. For a team with a dedicated data engineer (and we recommend at least one for ClickHouse), the TCO math shifts.

What we'd actually pick (and when)

I'll give you the decision framework I use in client engagements.

Pick PostgreSQL 17 if:

  • Your total data is under ~500M rows and growing slowly
  • You need ACID transactions, FOR UPDATE locking, or serializable isolation
  • Your "analytics" is really filtered reporting on 5-50M row subsets
  • You have a heavy JSONB filtering pattern (more than 3 JSON fields in WHERE clauses)
  • Your team is under 5 engineers and you don't have a data infra person
  • You need complex multi-table JOINs in the hot path (Postgres's planner is still better at 3+ table joins under 100M rows)

Pick ClickHouse 25.8 if:

  • You're ingesting 100K+ events per second and querying over 1B+ rows
  • Your analytical queries are wide scans (GROUP BY, percentile, window functions over large ranges)
  • You can flatten your schema at ingestion (no runtime JSON filtering)
  • You have a data engineer or SRE who knows the merge tree lifecycle
  • Your write pattern is batch (1K-100K rows per insert), not single-row OLTP
  • You're okay with "eventually consistent" reads (the 200-400ms replication lag in a Postgres→ClickHouse setup)

Pick both (the pattern we use at SIVARO):

  • Postgres for the transactional layer (orders, user state, API writes)
  • ClickHouse for the analytical layer (dashboards, cohort analysis, ML feature stores)
  • Logical replication or Debezium CDC feeding Postgres→ClickHouse
  • Total added latency: 200-400ms. Total added complexity: one more engine to operate.

This third option is what 70% of our 2026 engagements look like. Neither engine is "the database." They're different tools for different jobs, and trying to make one do both is where you end up with a system that's mediocre at everything.

-- The CDC pattern we ship (simplified)
-- In Postgres: pgoutput logical replication slot
-- In ClickHouse: materialized view consuming from a Kafka topic

CREATE MATERIALIZED VIEW events_mv
ENGINE = MergeTree
PARTITION BY toDate(created_at)
ORDER BY (tenant_id, created_at)
AS SELECT * FROM kafka_events_table
WHERE created_at >= today() - INTERVAL 90 DAY;
-- TTL created_at + INTERVAL 90 DAY DELETE
Enter fullscreen mode Exit fullscreen mode

The 90-day TTL is non-negotiable. Without it, your ClickHouse table will grow, merges will slow, and that 0.31s GROUP BY will become 1.8s by Q4. We've seen this happen. It's not theoretical.

A quick note on the 2026 landscape

PostgreSQL 17's async I/O support (backporting the IO worker thread model from the 18 development cycle) changed the math for large table scans. If you're on 16, upgrading to 17 isn't just a minor version bump. The io_method settings and the new background workers for pg_prewarm genuinely moved the needle on analytical performance. I benchmarked this in July and saw a 22% improvement on a 200M-row sequential scan just from the IO layer change.

ClickHouse 25.8 LTS brought the CollapsingMergeTree engine improvements that reduce the "mutation backlog" problem we've been fighting since 24.x. It's not a fix, but it's a 30% reduction in merge time on our test cluster.

Neither engine is "done." Both are moving fast. What I'm telling you is the state of things as of this writing, September 2026. By Q1 2027, the numbers above will shift. But the architectural principles won't.

FAQ

Can I use ClickHouse as a primary OLTP database?

No. And I don't mean "it's not ideal." I mean the merge tree engine is not designed for single-row UPDATEs. You can do mutations, but they're asynchronous and they block reads on affected parts. If your app does UPDATE users SET last_login = now() 50K times a minute, you'll be rewriting parts of your table all day. Use Postgres for OLTP. Use ClickHouse for the stuff Postgres gets slow at.

How does the clickhouse vs postgresql for group by performance question change at cluster scale?

It gets worse for Postgres. Distributed PostgreSQL (Citus, or the new pg_distributed in 17.4) can shard your GROUP BY, but the network overhead for combining partial aggregates across nodes is real. ClickHouse is columnar-first, so adding a node is more linear. In our 3-node test, ClickHouse scaled to 0.11s for the same 2.4B-row GROUP BY. Postgres with Citus sat at 3.4s. The gap widens because Postgres is moving more data over the wire.

Is Postgres 18 (due release ~November 2026) going to close the gap?

Possibly. The async I/O work and the new pg_columnstore extension (experimental in 18-dev) are heading in a direction that could make columnar aggregation on Postgres viable for 100-500M row tables. But "viable" is not "4x faster than ClickHouse on 2B rows." I'll re-benchmark when 18 ships. For now, plan around 17.

What about ClickHouse's JSON type vs. PostgreSQL's JSONB for our use case?

If you can define the schema at write time, use ClickHouse's JSON type (it's essentially a typed columnar map). If your JSON is truly unstructured and you need to query arbitrary paths at read time, PostgreSQL's JSONB with a GIN index is faster and simpler. The Dynamic type in ClickHouse 25.8 is closer to JSONB behavior but still lacks the indexing equivalent.

Do I need a separate caching layer if I go ClickHouse?

For the analytical queries, no. The columnar storage + column compression means the data is already "cached" in a sense. For point lookups (user profile, single-order detail), yes, you still want Redis or Postgres in front. ClickHouse is not a lookup engine.

What's the migration path if I start on Postgres and outgrow it?

You don't "migrate." You add ClickHouse as a secondary store. Feed it via CDC. Run your analytical queries there. Keep Postgres as your source of truth for transactions. We've done this migration 6 times in 2025-2026. The 4-6 week timeline includes building the pipeline, validating query parity, and running both in shadow mode for two weeks before cutover.

Is this comparison the same for PostgreSQL 17 vs. ClickHouse on AWS (Aurora vs. ClickHouse Cloud)?

The relative performance holds, but the absolute numbers change. Aurora's storage layer adds 0.5-2ms of latency per query. ClickHouse Cloud (the managed service from ClickHouse Inc.) adds a network hop but gives you the same columnar performance. The 14x GROUP BY gap becomes roughly 10-12x after accounting for network RTT. The architectural decision doesn't change.

Final thoughts

The "clickhouse vs postgresql 2026 performance" question has a real answer, but it's not one word. It's a function of your row count, your query shape, your team size, and your tolerance for operational complexity.

PostgreSQL 17 is the safer bet. It's the one that won't keep your SRE up at 2am on a Tuesday. It handles 95% of workloads that startups and mid-market SaaS companies actually run. And its JSONB support is still the gold standard for semi-structured data in a relational context.

ClickHouse 25.8 is the right tool when you've crossed the threshold. 1B+ rows. Wide analytical scans. 100K+ events per second. You need the 10-40x speedup on aggregation because your dashboard has a 500ms SLA and Postgres can't hit it. And you have the team to operate it.

Neither is "better." One is better for your workload. Figure out which one that is before you write the migration plan.

And if you're not sure, run both. We do it for clients. Two weeks of parallel ingestion, shadow queries, and latency comparison. You'll know in a week whether you need the second engine or whether Postgres 17's async I/O improvements just bought you another two years of runway.

That's the honest answer. And it's the one I'd give you over coffee.

Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.

Top comments (0)