3 Aug 2026 · 4 min read

ClickHouse or TimescaleDB for a plant historian? It depends which question your plant asks

historian · timescaledb · clickhouse · architecture · ot · data

Sooner or later someone suggests replacing the plant historian, and the conversation turns into a benchmark argument between two databases that are genuinely both good. I ran the comparison on ten million readings to get past the marketing.

The short version is that neither wins outright, the winner depends entirely on which question your plant asks most often, and there is a third finding that matters more than the contest.

How the test was set up

Both engines got the same box, and both were loaded from the same generated file — not from two separate scripts, because then I would be measuring my own loaders rather than the databases.

The data matters as much as the engines. It is ten million readings from a thousand simulated tags: temperatures that drift and hold, flows that sit at zero and then step when a pump starts, counters that only ever climb, states that stay put for hours. Testing with random numbers instead would have understated both engines' compression by about five times, which is a mistake worth its own write-up.

Loading

LOADING THE SAME 10,000,000-ROW FILE

ClickHouse2,666,667 rows/sec
3.75s
TimescaleDB242,600 rows/sec
41.2s
0s15s30s45s
The same file, the same box. Loading is the one place the gap is not close.

This one is not close. The identical file went into ClickHouse in under four seconds and took TimescaleDB forty-one. If your problem is getting a decade of tag history out of an old system and into a new one over a weekend, that gap is the whole argument.

Storage, and the setting nobody mentions

WHAT TEN MILLION READINGS COST TO KEEP

TimescaleDB, uncompressed73.2 bytes each
698 MB
ClickHouse, default codec4.38 bytes each
43.8 MB
TimescaleDB, compressed1.68 bytes each
16 MB
ClickHouse, tuned codecs0.67 bytes each
6.66 MB
Same ten million readings, four ways of keeping them. The bottom two are the only ones worth considering, and they are ten times apart from the top.

The raw readings occupy nearly seven hundred megabytes. Compressed TimescaleDB holds them in sixteen. ClickHouse with its default settings uses forty-two — worse than Timescale, which surprised me, because ClickHouse has a reputation for compression.

It turns out the default is the problem. ClickHouse ships with LZ4, which is tuned to be fast rather than small. Time-series data wants different encodings: store the difference between one timestamp and the next rather than the whole timestamp, and use a float encoding built for slowly-changing sensor values. Switching to those took one table definition and about two seconds of rewriting.

SETTING CLICKHOUSE CODECS — WHAT CHANGES

DISK USED
41.8 MB
default
6.35 MB
tuned
QUERY TIME
104 ms
default
107 ms
tuned
Six point six times less disk, and the queries did not notice. There is no trade-off here to think about — the default is simply wrong for a historian.

Six point six times less disk, and the queries ran within three milliseconds of where they were. That is not a trade-off, it is a default that is simply wrong for this job. If you run ClickHouse as a historian and have never set codecs on the table, you are storing roughly seven times more than you need to, and nothing in the product will tell you.

At 0.67 bytes per stored reading, a thousand tags sampled every second for a year comes to about twenty gigabytes.

The queries

This is where it stops being one-sided.

QUERY TIMES, BEST OF THREE, MILLISECONDS

ClickHouseClickHouse tunedTimescale rawTimescale compressed
last value, every tag
104
107
11,204
508
one tag, raw 10 min
77
76
81
57
one tag, 1-min averages
76
76
247
59
whole estate, 1-min avg
167
176
1,202
1,955
worst 20 vibration tags
88
87
311
155
Read it by row, not by column. No configuration wins everything, and the row you care about is whichever question your plant asks most.

Read that by row rather than by column, because the row that matters is whichever question your plant asks most.

"What is everything reading right now." The most routine question in any control room, and the worst case for a plain Postgres table: 11.2 seconds against 104 milliseconds. Turning on compression drags Timescale back to 508 ms, which is fine for a wall display, but ClickHouse is still five times quicker. In fairness to TimescaleDB, it has purpose-built tools for exactly this query, and a real deployment would use them. This is what the obvious query does.

"Show me this instrument for the last ten minutes." The thing an operator does dozens of times a shift, and here compressed TimescaleDB is faster than ClickHouse — 57 ms against 77. Same for bucketing one tag into averages, 59 ms against 76. If your historian mostly serves single trends, the row-store is not the underdog anybody assumes.

"Roll up the whole estate." ClickHouse by a distance, 167 ms against 1.2 seconds. And here compression actually made Timescale worse, 1,202 ms to 1,955 ms, because a query that touches everything has to decompress everything on the way past. Compression pays on selective reads and charges you on full scans.

THE FOUR NUMBERS

11×
FASTER TO LOAD (CLICKHOUSE)
108×
SLOWER LAST-VALUE (TIMESCALE, RAW)
6.6×
DISK WASTED BY DEFAULT CODEC
0.67
BYTES PER READING, BEST CASE
Three of these favour ClickHouse. The fourth is free to fix and most people running ClickHouse have not.

So which one

CHOOSING BY WORKLOAD, NOT BY BENCHMARK

WHAT DOES YOUR PLANT ASK MOST?ten million readings, either wayCLICKHOUSETIMESCALEDBquestions across the whole estateingesting hard, keeping years11x faster to load, 2.5x smallerone instrument's trend, all dayyou already run Postgresfaster on single-tag readsset the codecs either way — that one is free
The honest answer depends on the question your plant asks most often, not on which engine benchmarks better in general.

ClickHouse if you are ingesting hard, keeping years of history, or regularly asking questions that sweep across every tag you own. It loads eleven times faster and, once its codecs are set, stores two and a half times smaller than compressed TimescaleDB.

TimescaleDB if the daily reality is opening one instrument's trend, or if you already run Postgres, or if anything downstream expects to talk to a normal SQL database. That last point deserves more weight than it usually gets in benchmark arguments. Grafana, your reporting tool, the application somebody wrote in 2019, and the colleague who already knows Postgres are all real costs on one side of the ledger and real savings on the other.

There is also a fair criticism of this whole exercise: ten million readings is about three hours of a thousand-tag plant. Both engines are comfortable at that size, and the honest reading of these numbers is that at small scale you should choose on operational fit rather than performance. The gap widens with volume, and that is when the load and storage figures start to decide it.

The one thing I would change on Monday regardless of which you run is the codec setting. Six megabytes against forty-two, for the same rows and the same query speed, is the cheapest win in this entire comparison.

The full rig, every query, the fairness checks and the bug I found in my own data generator are in the lab write-up.

Keep reading

Newsletter

New essays, by email.

SCADA, cloud, AI, and the plant floor — a short email when something new is published. No noise, unsubscribe anytime.