Skip to content

Parallelize PromQL selector scans by budgeting read streams for decode-heavy columns - #116932

Open
nikitamikhaylov wants to merge 1 commit into
masterfrom
promql-selector-read-parallelism
Open

Parallelize PromQL selector scans by budgeting read streams for decode-heavy columns#116932
nikitamikhaylov wants to merge 1 commit into
masterfrom
promql-selector-read-parallelism

Conversation

@nikitamikhaylov

@nikitamikhaylov nikitamikhaylov commented Aug 28, 2026

Copy link
Copy Markdown
Member

Changelog category (leave one):

  • Performance Improvement

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Scans of the TimeSeries samples table performed by PromQL selectors are now parallelized: the per-stream read budget on the selector's query context is lowered from the generic merge_tree_min_bytes_for_concurrent_read (240 MiB) to 4 MiB, so the row budget becomes the binding constraint for these narrow, decode-heavy columns.

Documentation entry for user-facing changes

A PromQL selector scan reads a few narrow, decode-heavy columns (DoubleDelta timestamps, Gorilla values) from a handful of primary-key-selected granule ranges of the samples table. The generic per-stream read budget merge_tree_min_bytes_for_concurrent_read (240 MiB) assumes bytes are a proxy for work, which heavy codecs break: with the samples-table geometry (32768-row granules, index_granularity_bytes = 10 MiB) the budget demands 24 granules per stream, so a dashboard-sized scan of ~20 granules (hundreds of thousands of rows) is planned as a single-stream in-order read (ReadPoolInOrder × 1), serializing all the decompression on one core.

This PR lowers the byte budget on the selector's query context to 4 MiB — the byte equivalent of the default row budget (merge_tree_min_rows_for_concurrent_read = 163840 rows at ~30 bytes per row of id + timestamp + value ≈ 5 MiB) — so the row budget becomes the binding constraint: each stream still gets 1–2 ms of decode work, and the scan parallelizes. The override applies only when the setting is not explicitly changed by the user; the ..._for_remote_filesystem counterpart is budgeted the same way.

Measured on a 9-billion-row samples table (ClockBench d02: 10 series over 7 days, ~720K rows scanned, 32 cores):

  • the selector SQL alone: 35.8 ms → 13 ms (ReadPoolInOrder × 1ReadPool × 3);
  • end-to-end PromQL query_range over the Prometheus HTTP API: 25 ms → 20 ms warm, 60 ms → 32 ms on the first query after a server restart with a dropped page cache.

A stateless test asserts the pipeline shape both ways (parallel by default, single-stream when the user pins the generic budget) and that both plans return identical results.


Workflow [PR]
Sync PR [sync-upstream/pr/116932]

@clickhouse-gh

clickhouse-gh Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [5a027e1]

Summary:

job_name test_name status info comment
Stress test (arm_asan_ubsan) FAIL
Segmentation fault (STID: 3773-32b0) FAIL cidb

AI Review

Summary

This PR lowers the timeSeriesSelector read-stream byte threshold to 4 MiB so PromQL selector scans stop being planned as a single in-order stream on narrow, decode-heavy samples reads. The local-path test covers that behavior, but the remote-filesystem path still ignores an explicit generic byte-budget override, so the advertised opt-out is still broken for object-storage deployments.

Findings

❌ Blockers

  • [src/Storages/StorageTimeSeriesSelector.cpp:880] The new selector override promises to leave user-chosen read budgets alone, but for all-remote parts ReadFromMergeTree reads only merge_tree_min_*_for_remote_filesystem (src/Processors/QueryPlan/ReadFromMergeTree.cpp:1128-1131). As written, a query that explicitly sets merge_tree_min_bytes_for_concurrent_read still gets forced to 4 MiB on object storage whenever the deprecated remote setting itself was not changed. That removes the documented opt-out for Cloud/object-storage deployments and changes their behavior even when the user pins the generic threshold.
    Suggested fix: if merge_tree_min_bytes_for_concurrent_read was changed, treat that as an opt-out for the remote override too, or copy the explicit generic value into merge_tree_min_bytes_for_concurrent_read_for_remote_filesystem when the remote setting is unset.
Tests
  • ⚠️ [tests/queries/0_stateless/05047_promql_selector_read_parallelism.sql:1] The new regression test is tagged no-object-storage, so it never exercises the remote selector path where the explicit-budget contract is still broken. Add the smallest focused object-storage coverage that sets only merge_tree_min_bytes_for_concurrent_read and verifies the selector stays single-stream on remote parts.
Final Verdict

Changes requested. The local parallelization improvement is reasonable, but the remote-filesystem path still violates the PR's explicit-budget contract.

LLVM Coverage Report

Measured on commit 5a027e1.

Metric Baseline Current Δ
Lines 88.40% 88.50% +0.10%
Functions 92.00% 92.00% +0.00%
Branches 80.80% 80.80% +0.00%

Changed lines: Changed C/C++ lines covered: 12/12 (100.00%) · Uncovered code

Full report · Diff report

@clickhouse-gh clickhouse-gh Bot added the pr-performance Pull request with some performance improvements label Aug 28, 2026

/// Reads over remote filesystems (e.g. object storage) take their per-stream budget from the
/// `..._for_remote_filesystem` counterpart - budget them the same way.
if (!context->getSettingsRef().isChanged("merge_tree_min_bytes_for_concurrent_read_for_remote_filesystem"))

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.

ReadFromMergeTree does not fall back from the remote thresholds to the generic ones (src/Processors/QueryPlan/ReadFromMergeTree.cpp:1128-1131), so this branch still overrides object-storage selector scans even when the user explicitly sets merge_tree_min_bytes_for_concurrent_read in the query. For example, the opt-out that the new test exercises on local storage:

SELECT count()
FROM timeSeriesSelector(...)
SETTINGS merge_tree_min_bytes_for_concurrent_read = 251658240

still gets merge_tree_min_bytes_for_concurrent_read_for_remote_filesystem = 4 MiB here when the samples table lives on remote storage, because the deprecated remote setting usually remains "unchanged" and is the only one the remote path reads. That breaks the "respect an explicit byte budget" contract for Cloud/object-storage deployments. The remote override needs to treat a changed generic setting as an opt-out too (or copy the generic value into the remote counterpart when the remote setting is unset).

@clickhouse-gh

clickhouse-gh Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Build profile diff (arm_release)

Comparing 5a027e1bd with master 6c5b5aa32 (stripped binary size, per-symbol sizes and ThinLTO time; compile times per translation unit against the most recent warmup build that recompiled it).

✅ No significant changes.

Binary sizes
Binary Master PR Δ
programs/clickhouse-stripped 710.10 MiB 707.04 MiB -3.06 MiB (-0.43%)

Only the stripped binary is compared: the official master build keeps debug symbols while PR builds strip them, so the other binaries differ by construction.

Compile time of recompiled translation units

7 translation units recompiled, 11 s compile time in total, 7 of them have a recent master baseline.

Job report

…or decode-heavy columns

A PromQL selector scan reads a few narrow, decode-heavy columns (DoubleDelta
timestamps, Gorilla values) from a handful of primary-key-selected granule
ranges of the samples table. The generic per-stream read budget
merge_tree_min_bytes_for_concurrent_read (240 MiB) assumes bytes are a proxy
for work, which heavy codecs break: with the samples-table geometry
(32768-row granules, index_granularity_bytes = 10 MiB) the budget demands 24
granules per stream, so a dashboard-sized scan of ~20 granules (hundreds of
thousands of rows) is planned as a single-stream in-order read and all the
decompression is serialized on one core.

Lower the byte budget on the selector's query context to 4 MiB - the byte
equivalent of the default row budget (merge_tree_min_rows_for_concurrent_read
= 163840 rows at ~30 bytes per row) - so the row budget becomes the binding
constraint: each stream still gets 1-2 ms of decode work and the scan
parallelizes. Applied only when the user has not set the setting; same for
the remote-filesystem counterpart.

Measured on a 9-billion-row samples table (10 series over 7 days, 720K rows
scanned): the selector SQL drops from 35.8 ms to 13 ms (ReadPoolInOrder x1
-> ReadPool x3); the end-to-end PromQL query_range latency over the
Prometheus HTTP API drops from 25 ms to 20 ms warm and from 60 ms to 32 ms
after a server restart with a cold page cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nikitamikhaylov
nikitamikhaylov force-pushed the promql-selector-read-parallelism branch from 74853fd to 5a027e1 Compare August 28, 2026 22:05
@clickhouse-gh

clickhouse-gh Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

📊 Cloud Performance Report

✅ AI verdict: no_change — no significant changes across 40 queries analysed

no significant changes detected. K_source=6 K_base=30 flagged=0/65

clickbench

🟢 No significant changes

tpch_adapted_1_official

🟢 No significant changes

Debug info
  • StressHouse run: a393f22e-2b03-497f-9c7e-dbf3175e426f
  • MIRAI run: 2f6f4bc8-32f4-4ea5-b155-281f451b77f1
  • PR check IDs:
    • clickbench_259180_1787963783
    • clickbench_259186_1787963783
    • clickbench_259192_1787963783
    • tpch_adapted_1_official_259203_1787963783
    • tpch_adapted_1_official_259215_1787963783
    • tpch_adapted_1_official_259250_1787963783

@nikitamikhaylov nikitamikhaylov added the comp-promql PromQL / time-series subsystem: TimeSeries storage engine, PromQL parser, PromQL-to-SQL converter... label Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp-promql PromQL / time-series subsystem: TimeSeries storage engine, PromQL parser, PromQL-to-SQL converter... pr-performance Pull request with some performance improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant