Skip to content

Native INSERT: ~+40% via bulk column encoders + streamed Native blocks - #143

Merged
maximdanilchenko merged 2 commits into
masterfrom
native-insert-perf
Jun 8, 2026
Merged

Native INSERT: ~+40% via bulk column encoders + streamed Native blocks#143
maximdanilchenko merged 2 commits into
masterfrom
native-insert-perf

Conversation

@maximdanilchenko

Copy link
Copy Markdown
Owner

A performance pass on the Native write path: INSERT throughput goes from ~330k to ~450k rows/sec on the mixed-type benchmark (50k rows, M1 Pro, CH 26.5).

Changes

  • Bulk column encoders for the scalars that dominated encoding: String via a compiled write_string_column (varint + UTF-8 into one bytearray, pure fallback included), Date and naive DateTime straight from a stdlib array. Array(String) benefits too. Encode step: ~645k → ~1.46M rows/sec.
  • Stream the body as Native blocks (rows_to_native_stream, an async generator handed to the HTTP backend as a chunked body). ClickHouse inserts one block while the client encodes the next, overlapping the client-side encode with the server-side insert — this is the bulk of the win (load-invariant +39% vs a single-blob insert in a same-process A/B).

maximdanilchenko and others added 2 commits June 8, 2026 21:12
Two changes to the Native write path, targeting a 30% INSERT throughput gain:

- Bulk column encoders for the non-numeric scalars that dominated encoding:
  String via a compiled write_string_column (varint + UTF-8 into one bytearray,
  pure fallback included), Date and naive DateTime straight from a stdlib array.
  Array(String) benefits too (its flat child encodes as String). The encode step
  goes ~645k -> ~1.46M rows/sec on the mixed-type benchmark.

- Stream the INSERT body as a sequence of Native blocks
  (rows_to_native_stream, an async generator passed straight to the HTTP backend
  as a chunked body) instead of one pre-encoded blob. ClickHouse then inserts one
  block while the client encodes the next, overlapping the client-side encode
  with the server-side insert — the bulk of the win.

Result: Native INSERT ~330k -> ~450k rows/sec end-to-end (load-invariant +39% in
a same-process A/B vs a single-blob insert), now ahead of clickhouse-connect and
clickhouse-driver on this workload. The split body is not atomic on a client-side
encoding error (documented); valid uniformly-typed rows never hit that.

Full suite 921 passed (both HTTP backends), pure-Python fallback green.
perf_insert/ holds the iteration log; docs and benchmarks refreshed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…omicity

The streamed multi-block INSERT trades cross-block atomicity for throughput: if
encoding raises partway through a large insert, the blocks already sent are
committed. Make that an explicit, controllable choice instead of a silent one:

- New ChClient(insert_block_size=...) (default 8192). Inserts that fit in one
  block stay all-or-nothing; insert_block_size=0 always sends a single atomic
  block (encode fully, then POST) at the cost of the encode/insert overlap.
- Corrected the rows_to_native_stream docstring (an error can surface mid-stream
  on a single bad value, not only on the first block) and documented the
  trade-off in the README and CHANGELOG.

Tests cover both the multi-block streaming path and the atomic single-block
path. Full suite 929 passed, pure fallback green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@maximdanilchenko
maximdanilchenko merged commit 73cb76f into master Jun 8, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant