Product1 distinct publisher3 min readPublished
A post on timescale.com does the arithmetic behind a familiar outage, where the diff shows one added column while the planner is suddenly estimating ten times as many distinct series and ingest slows.
The Product Desk · Product desk

build
Four control planes, one Postgres: a team's case against polyglot persistence1 distinct publisher
product
One dashboard, two answers: 41,900 units, then 43,100, with no code change1 distinct publisher
product
Half the incident clock goes to search, and telemetry tools cannot read the answer1 distinct publisher
build
Notion's agent stack is live, not slideware, and it only changes one of your decisions1 distinct publisher
Compiled by The Product DeskSomething wrong?How this is made
The word cardinality covers two different costs that arrive on different schedules, which is most of why this failure catches people out. One is the count of distinct series, the product of the distinct values across every dimension the engine indexes [2]. The other, in Postgres, is the physical index, and a composite B-tree stores one entry per row rather than one per distinct combination, so its size tracks rows multiplied by entry width, where that width is the indexed column widths plus per-entry overhead [8].
Take the post's own fleet. Ten thousand assets with a hundred sensors each is a million series, and adding a firmware dimension with ten revisions multiplies that to ten million, so one column created nine million series and not one new row [13]. The same column also widened every index entry on every row already in the table [8]. The nine million is what the planner has to estimate over, and it is why ingest slows before anyone thinks to look at disk. The notation makes the shape plain: (tag_id, value, timestamp) identifies a series by one dimension, while (tag_id, device, location, unit, firmware, value, timestamp) identifies it by five [11].
The post is careful where vendor posts usually are not. It concedes that whether one indexed column really costs more than a million rows depends on the baseline, since the per-row cost scales with the rows you already have [9], and the text available stops mid-sentence at "on a small enough table" without naming the crossover [c9b]. It also frames its remedies as things you can do without leaving Postgres [10], which is a convenient place for a Postgres vendor to land. The mechanism travels anyway, to tags in InfluxDB line protocol and labels in Prometheus, because all three treat each unique combination of indexed attributes as a series of its own [4].
The review approved a column, but what teams actually added was a dimension, and dimensions multiply [3]. The wide table is not the mistake; it stays correct for its whole life when the indexed attribute set is fixed and known up front, which holds for a product with a defined metric set and fails for an industrial fleet where every integration brings a descriptor someone wants to filter on [5].
Two axes will sort most proposed columns before they ship: bounded or unbounded, and filtered-on or display-only. Bounded and filtered belongs on the reading, which is the wide table working as designed [4]. Bounded and display-only belongs in a metadata table. Unbounded and filtered is the expensive quadrant, and it wants bucketing before it wants an index, since a continuously valued tag such as a GPS coordinate has no ceiling at all [6]. Unbounded and display-only should never be indexed at all. The same grid catches the linear case that gets waved through, where a sensor that moves from reporting 0 or 1 to reporting 0 through 100 has taken its dimension from two values to a hundred [12].
The line worth adding to the schema review template asks two things of every new column: how many distinct values it will ever hold, and whether anything filters on it. That is cheaper than an on-call engineer reading EXPLAIN output to work out when the planner stopped using the index [1].
Ranked by verification strength, evidence, and original report placement.
In Postgres a composite B-tree holds one entry per row, not per distinct combination, so index size is roughly rows multiplied by entry width, where entry width is the sum of the indexed column widths plus per-entry overhead.
The post frames the problem as a schema decision you own rather than an engine ceiling, so the fix is a schema change and not a new database, and it offers what you can do about it without leaving Postgres.
In the post's opening scenario, someone adds a firmware_ver column to a sensor table as one line of DDL that clears review in a minute; two weeks later the ingest job is missing its window, a dashboard query that used to return in milliseconds takes seconds, and the on-call engineer is digging through EXPLAIN output to work out when the planner stopped using the index, while the ingest rate barely moved.
Cardinality is defined strictly as the number of distinct values one dimension can take, and the number of distinct series a database tracks is the product of those counts across every dimension it indexes.
Growth splits into two axes: linear, where adding values to a dimension already indexed raises the series count in proportion, and multiplicative, where adding a new indexed dimension multiplies the series count by that dimension's distinct count.
Mainstream time-series onboarding attaches every attribute you might filter on directly to the reading, as tags in InfluxDB line protocol, labels in Prometheus, and columns on a wide table in Postgres, and the engine then treats every unique combination of those values as its own series; at small scale this avoids designing a metadata table, a join on the read path, and a surrogate key.
Distinct publishers with included, body-backed reporting in this cluster.
1 article · August 28, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Arithmetic you can check, a benchmark you cannot
Two different grades of evidence sit in one post. The definition-and-product rule, the growth axes and the two index shapes are self-checking: multiply 10,000 assets by 100 sensors, add a ten-value firmware dimension, and ten million series falls out without a new row. The engine-mechanics claims — one B-tree entry per row, correlated predicates collapsing row estimates — are conventional Postgres behaviour stated plainly enough to be contested. But the headline's comparative ranking rests on a benchmark that the text we have never reaches, and no field measurement or named deployment appears anywhere.
No deployment record to read
Nothing in this reporting records anyone doing anything. There is no release, no named user, no usage figure, no benchmark run — the industrial fleet with its firmware fields and shift codes is an illustration, and the customer examples advertised in the header never arrive in the text.
The title asserts a ranking the body only conditions
The mechanism is not overstated — multiplicative growth is arithmetic, and the post is unusually careful about the difference between adding values and adding dimensions. The overreach is narrower and sits in the headline: 'costs more than a million more rows' is a measured comparison, and the post itself immediately concedes it depends on the baseline before the text runs out ahead of the numbers. A sound argument is wearing a stronger claim than the evidence on offer supports.
The vendor's fix is the vendor's product
Read the conclusion as positioning and it lines up exactly: a Postgres time-series vendor tells you the problem is a schema decision you own, not an engine ceiling, so the answer is a migration inside Postgres rather than a move to a different database — while InfluxDB and Prometheus are the engines described as offering only tag removal or data deletion. The post is also bundled under an IIoT marketing header promising trends and customer examples. None of that makes the arithmetic wrong; it does mean the framing of which failure mode is tolerable comes from an interested party.
Sure of the mechanics, unsure of the measurement
We can state with confidence what this post argues and that its central arithmetic survives inspection. We cannot say how the column-versus-rows comparison resolves at any real baseline, because the section that would answer it is missing, and with one interested publisher and no second account there is no way to test the operational claims about bloat, autovacuum contention or planning overhead against anyone's production experience.