Add an entry for TDengine - #1704
Merged
Merged
Conversation
TDengine TSDB is an open-source (AGPL-3.0) time-series DBMS written in C, aimed at industrial IoT. It is installed from the vendor's Linux tarball (x64 and arm64) and queried over the REST endpoint taosadapter exposes, which is the connection method its docs recommend over the native protocol. Three things make this entry look different from its neighbours, and all three are in tdengine/README.md. The dataset is modelled as a supertable with one subtable per CounterID rather than a flat table. A basic TDengine table lives in exactly one vgroup and is scanned by exactly one thread, so a flat `hits` would have used one CPU core on every machine in the benchmark; with 6,506 subtables and vgroups sized to the core count, as the vendor's capacity planning guide prescribes, a scan uses the machine. INSERT INTO <supertable> ... FILE needs a tbname column and the field list may not name a column twice, so csv-columns.txt maps hits.csv's CounterID field onto tbname and ./load sets the CounterID tag afterwards from information_schema.ins_tables — the subtable name is the tag value. COUNT(DISTINCT x) segfaults taosd once the deduplicated set outgrows pqSortMemThreshold (16 MB) and the spill-to-disk merge sort takes over, in msortComparFn via initSpillSortHandle. All eight ClickBench queries that use it are far above that, so all eight of them deduplicate in a subquery instead — exact rewrites, no HYPERLOGLOG. Q18 also needs its aggregate wrapped in a subquery because GROUP BY without ORDER BY ignores LIMIT and returns every group. 14 of the 43 queries produce no result at 100M rows. Six are refused by a hard ten-million-group ceiling (on the merged result, not per-vnode, and enforced during execution so they scan for minutes first), six run out of query memory, and two do not finish inside the timeout. Grouping on a string column is priced by the width the column is declared with, not by its values: the same 3M distinct 11-byte keys group in 10.4 s as VARCHAR(32), 111.4 s as VARCHAR(2048), and do not complete as VARCHAR(4096), which is how Q29 fails with only 3.0M groups. The queries are left in their natural form rather than approximated, so the nulls say what the system does. Five bugs found doing this are filed upstream and linked from the README: taosdata/TDengine#35449 taosd segfaults on COUNT(DISTINCT) when the deduplicated set spills to disk taosdata/TDengine#35450 GROUP BY ... LIMIT n returns every group when the query has no ORDER BY taosdata/TDengine#35451 AGG(DISTINCT x) beside an aggregate over an expression fails with Out of range taosdata/TDengine#35452 GROUP BY capped at 10,000,000 groups: undocumented, no knob, enforced only after a full scan taosdata/TDengine#35453 GROUP BY on a VARCHAR is priced by the declared width, not the data Also in the README: the load has to be chunked because the client buffers a whole INSERT ... FILE in memory, and the chunking has to track CSV quote parity because thousands of URL and Referer values contain literal newlines; KEEP has to be raised or the July 2013 data is rejected outright; (EventTime, WatchID) as a composite primary key is what keeps the load lossless, since EventTime has only 1.4M distinct values across 100M rows; and TDengine's LIKE is case-insensitive, so Q21-Q24 match more rows, as they do in the MySQL-family entries. Verified with a full 100M-row load through these scripts (count(*) is exactly 99,997,497, 6,506 subtables with no NULL tags, data size 76.2 GB) and, on a 1% sample, by comparing all 43 results query-by-query against clickhouse-local reading the same rows. No results yet — those need runs on the benchmark's own EC2 machines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 31, 2026
This was referenced Aug 31, 2026
Contributor
|
The run of Logs:
|
Contributor
|
Results for Logs:
|
Contributor
|
The run of Logs:
|
Contributor
|
The run of Logs:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an entry for TDengine TSDB OSS 3.4.2.5, an
open-source (AGPL-3.0) time-series DBMS written in C and aimed at industrial
IoT. It is installed from the vendor's Linux tarball (x64 and arm64 both
available, no registration) and queried over the REST endpoint
taosadapterexposes, which is the connection method its docs recommend over the native
protocol. Results are publishable — the licence has no DeWitt clause — but
there are none yet: those need runs on the benchmark's own machines.
tdengine/README.mdcovers all of the below in detail. Summarising herebecause a reviewer will want to know why the entry does not look like its
neighbours.
The dataset is modelled as a supertable, one subtable per
CounterIDThis is the one judgement call in the entry and it is worth checking.
A basic (non-super) TDengine table lives in exactly one vgroup and is scanned
by exactly one thread — with
hitsas a flat table on a 96-core box, aGROUP BYover a regex onRefererkeeps onevnode-querythread busy andleaves the other 95 idle. Benchmarking that would have measured one core on
every machine in the fleet.
So
hitsis a supertable withCounterIDas its tag and one subtable percounter, 6,506 of them, and
./loadsets the database'svgroupsto the corecount, which is how the vendor's
capacity planning guide
sizes it ("each CPU core can serve 1 to 2 vnodes"). This is the data model
TDengine documents for any dataset with a natural series key, and
CounterIDis the natural one — it identifies the web counter that produced the hit, and
it is the leading column of ClickBench's primary key in the ClickHouse entry
too.
Two consequences, both in TDengine's favour and both called out in the README:
a tag is stored once per subtable rather than once per row, so
CounterIDiseffectively free in
./data-size; and a tag filter prunes to matchingsubtables before reading anything, so Q37–Q43 (
CounterID = 62) touch onesubtable, which is the same kind of advantage ClickHouse gets from its primary
key.
SELECT *on a supertable returns tags last and TDengine requires theprimary timestamp first, so Q24 returns the same 105 values per row in a
different column order.
INSERT INTO <supertable> ... FILEneeds atbnamecolumn in the field listand the list may not name a column twice, so
csv-columns.txtmapshits.csv'sCounterIDfield ontotbnameand./loadsets the tagafterwards from
information_schema.ins_tables— the subtable name is thetag value, so nothing has to be inferred and the data is not read twice.
Five bugs, filed upstream
taosdsegfaults onCOUNT(DISTINCT col)once the deduplicated set outgrowspqSortMemThreshold(16 MB) and the spill-to-disk merge sort takes over. Backtrace from a core dump is in the issue:initSpillSortHandle→tsortOpen→msortComparFn, reached with a column-info pointer that does not describe the column being sorted. All eight of ClickBench'sCOUNT(DISTINCT ...)queries are far above the threshold, so all eight took the server down; they now deduplicate in a subquery instead. Exact rewrites, noHYPERLOGLOG.GROUP BY ... LIMIT nreturns every group when there is noORDER BY. Q18 is exactly that shape and returned 830,079 rows forLIMIT 10on a 1% slice; reproduces on a 1,000-row table. Q18 now wraps its aggregate in a subquery.AGG(DISTINCT x)beside an aggregate whose argument is an expression rather than a bare column fails withOut of range [0x80000112]on three rows.count(DISTINCT n), sum(n)is fine;count(DISTINCT n), sum(n+1)is not. No final query hits it, but it cost time while translating them.GROUP BYis capped at 10,000,000 groups (Too many groups/time window in query). Undocumented, no configuration parameter, on the merged result rather than per-vnode partials so sharding does not help, and enforced during execution — a doomed query still scans for two to ten minutes first.GROUP BYon aVARCHARcolumn is priced by the declared width rather than the data. Details below.14 of the 43 queries produce no result at 100M rows
Measured with a full cold sweep —
./stop, drop the page cache,./start,./check, one run of each query, which is whatlib/benchmark-common.shdoesbefore every cold measurement. Six are refused by the group ceiling, six run
out of query memory, two do not finish inside the timeout. The README has the
per-query table with each actual error and duration.
One of those failures needed explaining, because it is not a cardinality
problem: Q29 fails with only 3,007,986 groups. Grouping on a string column is
priced by the width the column is declared with, not by its values. Same
3,000,000 distinct 11-byte keys, changing nothing but the declaration: 10.4 s
at
VARCHAR(32), 12.4 s atVARCHAR(256), 32.9 s atVARCHAR(1024), 111.4 sat
VARCHAR(2048), and atVARCHAR(4096)it stops completing. Declared widthsare not a free choice either, since TDengine rejects a value longer than its
declaration, so
hitsforcesURLtoVARCHAR(8192),ReferertoVARCHAR(3072)andSearchPhrasetoVARCHAR(2048). Filed astaosdata/TDengine#35453.
The queries are left in their natural form rather than approximated, so the
nulls say what the system does. A time-series engine built for windowed
rollups over a bounded set of series doing badly at grouping 100M rows by a
high-cardinality string is a fair result, and the entry says so rather than
working around it.
Loading
INSERT INTO hits (...) FILEis the straightforward SQL path and the one used,but the
taosclient materialises an entire statement in memory before sendinganything, so the 81 GB
hits.csvdies withOut of Memory [0x80000102]afterabout 30 seconds of parsing.
load-csv.pyfeeds it 128 MB at a time through asingle scratch file that each chunk overwrites, so the load needs one chunk of
spare disk rather than a second copy of the dataset.
Chunk boundaries have to respect CSV records rather than lines: 3,740
URLvalues and 2,168
Referervalues inhitscontain literal newlines, andsplitting on
\ncorrupts rows silently, since the fragments still parse asnumbers and strings. A newline ends a record only when an even number of
"characters precede it. Chunks are byte-exact slices, so values reach the server
with their original bytes, including the non-UTF-8 ones.
Two other things the load has to do, both correctness rather than tuning:
KEEPis raised from its default 3650 days, because TDengine rejects any rowwhose primary timestamp is older than
now - KEEPandhitsis from July2013; and
WatchIDjoinsEventTimeas aCOMPOSITE KEY, because TDengineoverwrites on primary-key collision and
EventTimehas only 1,432,857 distinctvalues across 99,997,497 rows — a naive load would keep 1.4% of the dataset.
(EventTime, WatchID)is unique across all of it, and after a full loadSELECT count(*)returns exactly 99,997,497.Configuration
Left at the shipped defaults except for
VGROUPS(above),KEEP(above), and:monitor,telemetryReportingandcrashReportingoff, so no metrics arewritten into a database inside the server being measured and nothing is
reported to the vendor mid-run;
tempDirmoved off/tmp, which on Ubuntu25.04+ is a tmpfs, so sort spills do not either fail on a small instance or
land in RAM on a large one;
queryWaitTimeout1800 s, because the default 900would cut off Q6 (about 870 s) while still being the only thing bounding the
queries that never finish.
taoskeeperandtaos-explorer, two of the fourunits the installer enables, are disabled.
taosdsnapshots its configuration intodataDiron first start and prefersthe snapshot over
taos.cfgafterwards, so./installdeletes the snapshotafter editing — otherwise the edits read back as
cfg_fileintaosd -Cwhilethe old values stay in force.
Verified
Full 100M-row load through these scripts: 81,136,059,858 bytes of CSV in
9,129 s,
count(*)exactly 99,997,497, 6,506 subtables with zero NULL tagvalues,
./data-size76,202,317,209 bytes (about 40 GB of TSDB files plusabout 32 GB of WAL, which
WAL_RETENTION_PERIODdrops an hour later).And on a deterministic 1,000,765-row 1% sample, every one of the 43 queries
compared against
clickhouse-localreading the same rows. The onlyunexplained difference is Q4, where
avg(UserID)overflowsInt64: TDengineaccumulates in something wider and returns the true average, ClickHouse
returns the wrapped sum divided by the count. Q21–Q24 differ only because
TDengine's
LIKEis case-insensitive — they match exactly once the referenceis lowered, and the queries keep
LIKE, which is the same choice theMySQL-family entries make. The eight queries whose ten rows are not uniquely
determined were re-compared with
LIMIT/OFFSETremoved so the whole groupedresult had to agree; all eight match as multisets. Q24's ten rows agree in all
1050 cells when compared by column name, which exercises every column's type
and the non-UTF-8 string values end to end.
shellcheckclean on all seven shell scripts,ruffclean onload-csv.py.