Chapter 06 · Clickhouse Js Node Rowbinary
Subchapter 6.5
case-studies/logs-json-wins.mdMarkdown4 KBView on GitHub
TL;DR — This is the honest counter-case. When the result is mostly text
consumed wholesale (an application log table), JSONCompactEachRow +
decodes than the optimized RowBinary reader — and
once you turn on HTTP compression, RowBinary’s raw-wire size advantage
: gzip ties the two, and with . For this shape the skill steers you from RowBinary —
and proving that is what makes its “use RowBinary here” advice (see the
and
studies) trustworthy.
JSON.parseThis is exactly what the SKILL’s format-choice
guidance says: prefer a
JSON* format when the result is “mostly strings / JSON-like values that you
consume wholesale,” because V8’s native JSON.parse is heavily optimized C++
and “pair it with HTTP response compression (gzip / zstd, which crushes
JSON’s repetitive keys).”
Reproduce: npx vitest bench --run tests/logs.bench.ts (against a live
ClickHouse server). Source: tests/logs.bench.ts,
reader: src/examples/logs.ts.
An application log table — four of five columns are text consumed as text:
ts DateTime
level LowCardinality(String) -- transparent in RowBinary -> plain String
service LowCardinality(String)
message String -- templated log line, varying values
trace_id String -- high-cardinality 32-char hex50,000 rows, fetched from a live server. The two LowCardinality columns carry
no dictionary on the RowBinary wire — they decode as plain String.
| Decoder | ops/s | ms/decode | ≈ rows/s | speedup |
|---|---|---|---|---|
JSONCompactEachRow — JSON.parse | 93 | 10.73 | ~4.7 M | 1.0x |
JSONEachRow — JSON.parse | 72 | 13.89 | ~3.6 M | 0.77x |
| RowBinary — optimized (monomorphized) | 66 | 15.07 | ~3.3 M | 0.71x |
| RowBinary — API combinators | 54 | 18.68 | ~2.7 M | 0.57x |
JSONCompactEachRow (arrays, no repeated keys) is the fastest JSON option and
beats even the optimized RowBinary reader by ~1.4x. A RowBinary string is a
varint length + buf.toString("utf8", …) decoded one field at a time in JS;
JSON.parse builds the same JS strings in one optimized C++ pass.
| Format | raw | gzip | zstd |
|---|---|---|---|
| RowBinary | 5.04 MB | 1.46 MB | 1.35 MB |
| JSONCompactEachRow | 6.84 MB | 1.51 MB | 1.32 MB |
| JSONEachRow | 8.84 MB | 1.52 MB | 1.33 MB |
RowBinary is 1.4–1.8x smaller raw, which is the usual argument for it. But
that edge is mostly JSON’s repeated structure (keys, punctuation) — exactly what
a compressor removes. With gzip the three are within ~4% of each other, and
with zstd the JSON responses are slightly smaller than RowBinary. Any
production HTTP path should have compression on, so the wire-size case for
RowBinary on this data effectively vanishes.
Node 24 / V8. Your numbers will vary; run npm run bench on your own hardware.
JSONCompactEachRow is the one to reach for — it drops the per-row
repeated keys, so it parses faster than JSONEachRow and compresses about the
same.JSONCompactEachRow + compression. Match the format to the
shape of the data — and measure.