Use a continuous primary-key range for whole-metric PromQL selectors of TimeSeries tables - #114131
Use a continuous primary-key range for whole-metric PromQL selectors of TimeSeries tables#114131nikitamikhaylov wants to merge 8 commits into
Conversation
…of TimeSeries tables When a selector matches all series of one metric and the id layout is a two-component tuple with the canonical id generator, add a superset PK range on id to the generated WHERE and set use_index_for_in_with_subqueries_max_values=1 so index analysis uses the range instead of the large IN set. The IN condition stays for exact row filtering; any failed gate emits the previous SQL unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Workflow [PR], commit [06852ed] AI ReviewSummaryThis PR adds a whole-metric fast path for Findings❌ Blockers
Final VerdictStatus: ❌ Block
|
| log); | ||
|
|
||
| ContextPtr interpreter_context = context; | ||
| if (!whole_metric_id_range_conditions.empty()) |
There was a problem hiding this comment.
This branch only checks that the selector's id set is a row-level subset of one continuous id range. It does not check that the samples target can actually use that range for primary-key pruning.
TimeSeries still supports custom and external samples tables with arbitrary engines / key layouts; normalizeTimeSeriesDefinition only validates the column types of external targets, and inline SAMPLES ENGINE = MergeTree ORDER BY ... is user-configurable. On a supported layout like ORDER BY (timestamp, id) (or any non-id primary-key prefix), this branch still sets use_index_for_in_with_subqueries_max_values = 1, so the existing id IN (...) pruning is disabled even though the new id range cannot constrain the key. Whole-metric selectors would then regress to timestamp-only or full-part scans.
This needs one more guard before enabling the optimization: require the samples target to have id as the leading primary-key / sorting-key column (or keep the IN eligible for index analysis otherwise). A regression test with a custom or external samples target would pin this down.
|
📊 Cloud Performance Report ✅ AI verdict: 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
|
|
@groeneai I'm not sure how to read the performance test results. Where are the results of |
|
There are no results for it: Where to look: the test landed on Both show the same thing:
That is also why nothing surfaced anywhere else. The CI summary table lists the two shards as FAIL but names only unrelated unstable queries, and the cloud-benchmark comment reports The fix is to double the braces, which is what the 12 existing perf tests with literal braces do ( With no substitutions declared, Until that lands, the only measurements for this change are the tsbench numbers in the description, taken off-CI. |
|
@groeneai suggest a diff for the fix. |
perf.py runs every query through string.Formatter to expand
substitutions, so the PromQL label matchers {dc=~".*"} and {dc="0"}
were read as substitution placeholders and the whole test aborted
with KeyError at parse time on both shards it was assigned to.
Double the braces as other perf tests with literal braces do; with
no substitutions declared, str.format turns {{ back into {, so the
emitted SQL is unchanged.
#114131 (comment)
https://s3.amazonaws.com/clickhouse-test-reports/PRs/114131/e6eb16ae9aa3cf8f158e73b266ccbdd40c62a601/performance_comparison_amd_release_master_head_1_6/report.html
https://s3.amazonaws.com/clickhouse-test-reports/PRs/114131/e6eb16ae9aa3cf8f158e73b266ccbdd40c62a601/performance_comparison_arm_release_master_head_3_6/report.html
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
You already pushed exactly it in 96e5612 — the two
|
…-selector-pk-range
Build profile diff (arm_release)No arm_release build profile data for commit 097574b - the build was skipped, reused from cache, or predates profile upload. |
…-selector-pk-range
| /// One such series means the id set is not the whole metric's primary-key range: fall back. | ||
| /// No such series means every series the tags subquery can select lies in the range. The | ||
| /// probe result cannot be raced into incorrectness: series inserted after the probe get | ||
| /// their ids from the current (canonical) generator, so they stay inside the range, and |
There was a problem hiding this comment.
whole_metric_id_range_conditions stops being a pure index hint once it is added to the WHERE: it becomes part of the row-level filter, so the proof that "future inserts stay inside the range" has to remain true until the main read snapshot is taken.
TimeSeries still allows ALTER TABLE ... MODIFY SETTING id_generator after CREATE, and external tags targets can also change the id DEFAULT. If that metadata changes after this probe but before the main tags/samples reads are planned, newly inserted series for the same metric can get ids outside the precomputed range. The id IN (...) subquery will still return those ids, but the extra id >= ... AND id <= ... predicates added below will silently drop their samples.
This path needs a snapshot/lock that keeps the generator immutable for the lifetime of the query, or it needs to stay disabled whenever the generator source can change concurrently.
|
@groeneai investigate the test failures. |
|
None of them are caused by this PR. Current head
The crashing query, from This is issue #109181, which is still open. Reproduced on pristine master ( CREATE TABLE t1 (c0 Int32, c1 DateTime64, c2 Int32) ENGINE = MergeTree() PRIMARY KEY (toMonday(c1))
SETTINGS min_bytes_for_wide_part = 1;
SET enable_extended_results_for_datetime_functions = 1;
ALTER TABLE t1 COMMENT COLUMN c1 'a';
INSERT INTO t1 (c0, c1, c2) VALUES (1, '2020-01-01 00:00:00', 1);Aborts with the same cast pair and the same frames as CI ( My fix for it is #109196 (pin the setting off when analyzing key and index expressions so the key type is deterministic). It is open and unreviewed, which is why this keeps firing. Failures on the previous head The PR's own tests are clean: |
…range # Conflicts: # src/Storages/StorageTimeSeriesSelector.cpp
|
@groeneai investigate the test failures and link (implement if needed) the PRs with fixes. |
|
Both reds at
First one: Second one: a Neither needs new code from me. |
|
It is not failing on public CI: 1452 runs in 30 days, 0 failures, 0 above the cap. What you saw is the flaky check's wall-clock cap, Durations for
The slowest public lane is already at 115.9s of 180s, so any slower configuration crosses it. The cost is DDL count, not one slow query: 5 I would not shrink the matrix, since each fixture covers a distinct id layout your change branches on. They are independent though (each
That separates the two cost drivers. Both files keep The one-liner alternative is Worth picking the number when you apply it, another PR may take 04872 first. |
Master commit 1a2d352 ("TimeSeries: store all tags in the tags column") changed TimeSeriesIDGenerator::getDefault to take 2 arguments and removed the all_tags column from newly created tables, which broke the Fast test build of this PR. Update the call site and rewrite the custom-generator test cases in terms of the tags column and the new canonical generator tuple(sipHash64(metric_name), reinterpretAsUUID(sipHash128(tags))). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

A PromQL selector over a
TimeSeriestable filters the samples table withid IN (SELECT id FROM tags WHERE <matchers>). For a metric with tens of thousands of series,KeyConditionruns its single-threaded generic exclusion search with the whole set: 284 ms per selector on a 62-billion-row part with 1.9M marks (503 ms at 8.1M marks), and rule-style queries evaluate up to 5 selectors.With the two-component id layout
Tuple(UInt64, UUID)the canonical id generator derives the first component from the metric name alone, so all series of one metric form one continuous primary-key range. When a selector matches a whole metric — verified by metadata checks plus oneLIMIT 1probe on the tags table, which also detects out-of-range ids left by an earlierALTER ... MODIFY SETTING id_generator— the generated WHERE additionally carriesid >= tuple(hash(name), min) AND id <= tuple(hash(name), max)and the inner query setsuse_index_for_in_with_subqueries_max_values = 1. Index analysis uses the range; theINstays for exact row filtering, so both emissions return identical rows on any data. Any failed check emits today's SQL unchanged. The range can select a few extra boundary granules (+5 of 135,005 marks on a 30-day scan).Measured effect
tsbench PromQL suite: 62.455B samples / 361,432 series, 1.9M-mark part; Ryzen 9950X (16C/32T); baseline = clean master 9b6a2d7. Cold medians of 3 interleaved rounds:
Selectors that do not match a whole metric fall back and are unaffected (r05, s05: ±0.3%). Probe cost on non-firing selectors: ~2–4 ms each (r07: 66 → 73 ms); single-component id layouts never reach the probe. The removed cost grows with mark count, so the effect is larger at
index_granularity_bytes = 262144.Tests
04836_time_series_selector_whole_metric_pk_range: fires for whole-metric selectors (plan carries the range,INretained), falls back byte-identically for label-filtered, regex, custom-generator, and ALTERed-id_generatorhistory cases; both tuple layouts; fullprometheusQuery/prometheusQueryRangeresults compared.tests/performance/promql_selector_pk_range.xml: 20,000-series metric, range path plus fallback control.Related: #113768 (open) — removes no-op casts in the same generated SELECT; complementary, each stands alone.
Changelog category (leave one):
Changelog entry:
PromQL selectors that match all series of one metric now filter the samples table of a
TimeSeriestable with a continuous primary-key range onidduring index analysis instead of a largeid IN <set>condition, when the id layout is a two-component tuple with the canonical id generator. Removes the dominant single-threaded index-analysis cost of selector-heavy PromQL queries: up to −45% cold latency on dashboard and rule query shapes, −11% cold geomean over the full suite on a 62-billion-sample table.🤖 Generated with Claude Code