Skip to content

docs: add a performance tuning section (#491) - #558

Open
alex-clickhouse wants to merge 4 commits into
poco-read-boxfree-slotsfrom
docs/491-performance-tuning
Open

docs: add a performance tuning section (#491)#558
alex-clickhouse wants to merge 4 commits into
poco-read-boxfree-slotsfrom
docs/491-performance-tuning

Conversation

@alex-clickhouse

Copy link
Copy Markdown
Collaborator

Closes #491.

The issue asked for buffer size configs, compression codecs, POCOs vs boxed object[], and skipping
schema probes. Those knobs were each documented in their own reference table already, so rather than
repeat them this adds a task-oriented section that orders them by payoff, and fills in the parts that
were not written down anywhere.

What is new

Beyond the four items in the issue:

Added Why it earns a place
Materialization path QueryAsync<T> is box-free; MapTo<T> and the ADO accessors are not. 1,372 MB against 3,133 MB on a 1M-row, 105-column read. Nothing said which to reach for.
Batch size The largest single control on insert throughput, and it is not mentioned in a performance context anywhere.
Server GC Up to 49% less time on insert-heavy work, and it is off by default in console and worker apps. Free, and undocumented.
Compression by direction Reads and inserts give opposite answers at the same distance. The existing section covers codecs and levels, not direction.
Cold start 77.6 ms against 3.0 ms, decomposed into three round trips, to show that no driver setting moves it.
How to measure Several of these results reverse between loopback and a real network. A reader who tests locally gets the wrong answer.
GZipCompressor default level warning See below.

Two things worth a maintainer's eye

1. A warning about GZipCompressor's default level. It constructs at CompressionLevel.Fastest
(zlib level 1), and the server ingests that stream slowly enough that a large insert can pass the
120-second HttpClient timeout. This is reachable: InsertOptions.Compressor's own XML doc says
"gzip remains the safer choice behind such a tier", so a proxy-bound user is steered toward gzip
and the default constructor selects exactly the bad level. The default insert compressor is zstd,
so the out-of-box path is unaffected.

The underlying behaviour looks server-side rather than driver-side — replaying captured wire bytes
with curl, no .NET involved, reproduces it. Worth deciding separately whether GZipCompressor
should default to Optimal.

2. The insert-compression guidance may sit oddly next to the existing table. The existing
#tuning-compression "rough guide by deployment" recommends zstd:1 for same-region inserts. The
measurements say uncompressed was fastest at every distance tested (0.1 ms, 8.7 ms, 26.8 ms RTT),
costing 30% / 18% / 4%. Those are not strictly contradictory — the existing table reasons from
bandwidth thresholds and single-thread encode rates, and notes that parallel inserts move the
threshold — but a reader could land on both and be confused. I deliberately did not rewrite the
existing section
, since it was revised recently on purpose. Flagging it for you to reconcile.

Drive-by fix

The ReadBufferSize row in the settings table still documented an 8 KiB default and a
large-object-heap warning. Since #451 the default is 64 KiB and the buffer is pooled, so the
warning no longer applies and the number was wrong.

Provenance

Every figure comes from the August 2026 macro-benchmark package (ClickBench hits, 1M rows,
105 columns, ClickHouse 26.x) across four environments spanning a 270x range of network latency. Each
number is captioned with the environment it holds in, because several of them invert.

Checks

  • All eleven cross-reference anchors resolve.
  • <Note> / <Warning> tags balanced; code fences balanced.
  • API names verified against main: GZipCompressor(CompressionLevel = Fastest, ...),
    InsertOptions.Compressor is nullable with a ZstdCompressor.Default default, RegisterPocoType<T>().
  • Claims checked against main specifically: Box-free POCO read fast path #449 has merged so QueryAsync<T> is box-free, but
    Box-free ADO read path: typed column slots (stacked on #449) #499 has not, so the ADO accessors still box. The section says so and does not overclaim.
  • dotnet run scripts/changelog.cs -- --check passes.

Draft because of the two open questions above.

🤖 Generated with Claude Code

Collects the tuning advice that was spread across the reference tables, and
adds the parts that were not documented anywhere, in order of how much each
one usually gives you.

New material, all of it from the macro-benchmark suite:

- Materialization path. QueryAsync<T> is the box-free path; MapTo<T> and the
  ADO accessors are not. 1,372 MB against 3,133 MB on a 1M-row, 105-column
  read. ORMs drive the ADO reader, so they get the boxed path.
- Batch size, with a warning. Large batches used to be a pessimisation and
  are now the fastest option, so tuning done against 1.3.0 or earlier is now
  inverted. Small batches cost far more on Cloud than on local disk.
- Compression by direction. Responses: compress. Requests: measure first, as
  it cost time at every distance measured up to 27 ms RTT.
- Server GC, which is worth up to 49% on insert-heavy work and nothing at all
  on reads, with the pause-length trade-off stated.
- Cold start decomposed into its three round trips, to show that no driver
  setting moves it and only connection reuse does.
- How to measure without misleading yourself, since several of these results
  reverse between loopback and a real network.

Also warns against GZipCompressor's default level. It constructs at
CompressionLevel.Fastest, and the server ingests that stream slowly enough
that a large insert can pass the 120 s HttpClient timeout. InsertOptions'
own documentation steers proxy-bound users toward gzip, so the default
constructor is easy to reach.

Corrects the ReadBufferSize row in the settings table, which still documented
the 8 KiB default and a large-object-heap warning. The default has been 64 KiB
and pooled since #451.

Co-Authored-By: Claude <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Assumes #499 (ADO typed column slots) lands before this. With it, the reader
keeps storage of the column's own type, so GetInt32/GetInt64/GetGuid and the
scalar GetFieldValue<T> read a field instead of unboxing. GetValue and
GetValues still box, because they return object.

That splits the ORM advice rather than flattening it: linq2db registers the
typed accessors per column and so reads without boxing, while a path that
goes through GetValue does not.

Co-Authored-By: Claude <noreply@anthropic.com>
@alex-clickhouse

Copy link
Copy Markdown
Collaborator Author

⚠ Now depends on #499 — do not merge before it

Updated the read-path guidance to assume #499 (ADO typed column slots) lands first.

This makes the PR wrong if merged on its own. The section now tells readers that GetInt32, GetInt64, GetGuid and the scalar GetFieldValue<T> do not box. On main today they still do — they are (int)GetValue(ordinal). Merge order matters here.

What changed in the section:

  • The single "ADO accessors box" row is now two rows: the typed accessors (box-free, reading typed per-column storage) and GetValue/GetValues (still boxing, since they return object).
  • Added that IsDBNull never materializes the value.
  • The ORM advice is no longer flat. linq2db registers GetInt64/GetDouble/GetDateTime per column, so it gets the box-free path; a path that reads through GetValue does not. That is a better answer than "ORMs get the slow path", and it is the one users can act on.

Also worth knowing for review

#550 (stacked on #499) removes the last per-value allocations on UUID, array elements and ClickHouseDecimal. It does not change anything this PR claims — the hits figures quoted here have no UUID, array or decimal columns — but it does move the numbers in the wider benchmark package, so those are being re-run once the stack lands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds task-oriented performance tuning guidance for reads, inserts, compression, GC, buffering, and measurement.

Changes:

  • Documents performance tradeoffs and benchmark results.
  • Corrects ReadBufferSize documentation.
  • Adds a changelog fragment.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
docs/overview.mdx Adds performance guidance and updates buffer settings.
changelog.d/491-performance-tuning.docs.md Records the documentation update.
Suppressed comments (2)

docs/overview.mdx:1441

  • The current typed accessors delegate to GetValue, so linq2db still consumes values that Read() already boxed in CurrentRow. Claiming that typed-accessor ORMs are box-free contradicts the implementation and the PR description's note that #499 has not merged.
- **ORMs get the fast path when they use typed accessors.** linq2db registers `GetInt64`,
  `GetDouble` and `GetDateTime` for each column, so it reads without boxing. Code that reads through
  `GetValue` — including a `dynamic` result from Dapper — boxes each value. If an ORM query is hot
  and reads through `GetValue`, use `QueryAsync<T>` for that one query.

docs/overview.mdx:1516

  • The existing rough guide at docs/overview.mdx:1015-1020 recommends zstd:1 for same-region inserts, but this table says compression costs 18% in that same deployment category and the at-a-glance table suggests disabling it. These opposing recommendations need to be reconciled or explicitly scoped to different bandwidth, codec, or parallelism conditions.
| Client to server | Effect of compression |
|---|---|
| Same host (loopback) | Costs 30% |
| Same cloud region | Costs 18% |
| One region away | Costs 4% |

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/overview.mdx Outdated
Comment thread docs/overview.mdx Outdated
Comment thread docs/overview.mdx Outdated
Comment thread docs/overview.mdx Outdated
@alex-clickhouse
alex-clickhouse changed the base branch from main to poco-read-boxfree-slots August 16, 2026 08:48
@alex-clickhouse
alex-clickhouse marked this pull request as ready for review August 16, 2026 08:48
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.

Add performance tuning section to docs

2 participants