ClickBench on LessDB

ClickBench on LessDB

Benchmark methodology, harness, and results for running the ClickHouse ClickBench workload against LessDB and a dockerized ClickHouse reference.

Harness

bench/src/clickbench.rs (binary less-bench) implements the full workload:

Orchestration

scripts/clickbench-run.sh (run on the benchmark instance) drives, in order:

  1. dataset download (hits.parquet, ~15 GB) — --skip-download to reuse;
  1. MergeTree: load + optimize + 43-query run;
  1. SharedMergeTree: same pipeline, object-store parts
  2. (--clickbench-shared);

  1. read stress: 8 concurrent workers hammering the query set for 60 s
  2. (--clickbench-stress);

  1. mixed load + query stress: the query workers plus a background
  2. stream of the dataset into a throwaway hits_load table with WAL + inline merges (--clickbench-stress-mixed);

  1. crash-recovery under load: load with WAL on, kill -9 after 25 s,
  2. verify recovery (WAL replay + torn-part sweep), resume to completion;

  1. ClickHouse reference: docker run clickhouse/clickhouse-server,
  2. INSERT INTO hits SELECT * FROM file('hits.parquet'), 43 queries × 3 via clickhouse-client --time (--ch-only to rerun this phase alone; a fully-loaded table is reused across reruns).

Environment

optimus0 (192.168.0.2): 28 cores / 62 GB RAM, shared with unrelated workloads and the self-hosted CI runners. Dataset: 99,997,497 rows, 105 columns, 14 GB parquet.

Results (2026-08-28)

Full reports: bench/results/clickbench-20260828-optimus0.md/json (MergeTree), clickbench-20260828-optimus0-shared.md/json (SharedMergeTree), clickbench-clickhouse-20260828-optimus0.md, clickbench-compare-20260828-optimus0.md, clickbench-compare-merge-vs-shared-20260828-optimus0.md.

phaseresult
MergeTree load391.1 s (~265k rows/s, flat ~0.6 GB RSS, 382 parts)
MergeTree OPTIMIZEbounded passes → 25 parts, peak ~4.8 GB RSS
43 queries (MergeTree)43/43 pass, total 47,211 ms (medians); 6 order-sensitive queries flagged by checksum
SharedMergeTree load396.3 s (object-store parts, same RSS profile)
SharedMergeTree OPTIMIZE431.2 s → 25 parts
43 queries (SharedMergeTree)43/43 pass, total 380,229 ms
ClickHouse (docker, defaults)total 12,388 ms (3-run averages)
read stress (8 workers, 60 s)131 queries, 2.0 q/s, p50 3.55 s, p95 10.12 s, p99 13.88 s
mixed load + query stress (8 workers, 60 s)129 queries, 2.1 q/s, p50 3.80 s, p95 10.25 s, p99 12.56 s — while ingesting 8.4M rows @ ~135k rows/s
crash-recoverykill -9 mid-load → 5,242,880 rows recovered; resume to completion

MergeTree vs ClickHouse: LessDB is 3.81x slower overall (47.2 s vs 12.4 s). SharedMergeTree is 8.05x slower than local MergeTree on queries (380.2 s vs 47.2 s) while load/merge stay at parity.

Findings and headroom

  1. Page-level parquet skipping is disabled (DataFusion Mask selection
  2. bug, apache/datafusion#8092 — the offset index is not written; see less-storage writer properties). Row-group statistics skipping remains on. This is the largest single query-side lever.

  1. SharedMergeTree scans materialize whole parts per query: with the
  2. default 256 MB block cache, every query re-fetches ~8.6 GB of parts (25 × ~350 MB), hence the 8x query gap. The fix is lazy range reads through object_store (fetch row-group ranges on demand) plus a block cache sized to the hot part set — the roadmap item now has a measured number attached to it.

  1. Late materialization for wide tables (ClickBench projects a few of
  2. 105 columns per query) and general DataFusion execution overhead make up the rest of the gap to ClickHouse.

  1. Six ClickBench queries (LIMIT/OFFSET without a total order, float
  2. aggregates) are order-sensitive; the harness flags them via checksum_ok instead of hiding the variance.

  1. Along the way the harness drove four real fixes: bounded iterative
  2. merges (max_merge_rows), ClickHouse-style identifier case handling, torn-part sweep at open, and offset-index-free part writing.

Rerunning

ssh 192.168.0.2
cd ~/lessdb-clickbench
scripts/clickbench-run.sh --skip-download        # everything except the download
scripts/clickbench-run.sh --ch-only              # ClickHouse reference alone