-
[OPIK-7611] [BE] Cut the fixed cost of the optimization list query (#7672)
发布于
2026-07-31 16:00:54 +00:00 - [OPIK-7611] [BE] Cut the fixed cost of the optimization list query
OptimizationDAO.FIND is the most expensive query shape in the ClickHouse workload,
and its cost is fixed per call rather than proportional to data: rows read varies from
0 to 770k with no change in CPU, and the largest-data bucket is the cheapest. Because
ClickHouse inlines CTEs, each branch materialises independently and all stay alive in
the final six-way join, so the pipeline costs ~2.2 CPU-s and ~3.9 GiB whether or not
there is anything to aggregate.Consolidate best_candidate and baseline_candidate into a single candidate_rollup over
candidate_metrics. Both grouped by the same key over the same rows, so the second pass
was pure duplication: measured 26-34% less CPU and 22-31% less memory.Give best_duration and best_cost a deterministic tie-break. argMax over weighted_score
picked arbitrarily among tied candidates, and 93.6% of optimizations have a tied max
score with differing durations - the unchanged query returns different values for
451 of 1000 optimizations on nothing more than a change of max_threads. Tie-break on
earliest_created_at, which no optimization ties on and which matches how baseline
already orders. baseline is untouched. This changes these two user-visible values for
optimizations whose scores tie, replacing arbitrary results with stable ones.Serve optimizations with no experiments from a projection that skips the aggregate
pipeline. Every aggregate derives from experiments_final, so with no experiments they
collapse to empty-input values. A pre-check bounded at 13 CPU-ms across every real
production scope decides, and it omits the narrowing filters so a negative answer is
conservative. Note total_optimization_cost is a non-nullable zero, because sum() over
an empty group returns 0 rather than NULL.The projection keeps all 26 columns with identical names, types and order, verified by
diffing DESCRIBE against the unchanged template.Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- [OPIK-7611] [BE] Fix the experiment probe binding and address review
The probe reused bindQueryParams, which binds name, filters and dataset_deleted.
Its query declares none of those placeholders, and binding a parameter the rendered
query does not contain fails the statement - so every optimization search carrying
one of those criteria failed. That is what broke Backend Integration Groups 1, 9 and
16 and the Python SDK E2E suites.Split the two binders along the boundary the probe needs: bindScopeTemplateParams and
bindScopeQueryParams cover the criteria that select optimizations by identity, and the
full binders now delegate to them, so the probe and the main queries share one
implementation instead of duplicating it. Verified mechanically that every bound
parameter appears in the rendered template and vice versa.Also from review: add the required log_comment so the probe is attributable in
query_log, rename to hasExperimentsForDirectFilters to stop the name implying the
narrowing criteria are applied, and skip the probe when the count is zero. The
short-circuit is safe because the full path builds (page, 0, total, [], []) which at
total = 0 is exactly OptimizationPage.empty(page, List.of()).Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- [OPIK-7611] [BE] Bind entity_type only for the template that declares it
Same defect as the probe, second instance: FIND declares :entity_type in its
feedback-score CTEs, the fast path omits those CTEs, and bindQueryParams was called
with isFindQuery = true regardless of which template was chosen. Binding a parameter
the rendered query does not contain fails the statement, so every search that fell to
the fast path failed - which is why Integration Group 16's studio tests, whose
optimizations have no experiments, kept failing.The flag already existed for exactly this purpose; drive it from the template choice.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- [OPIK-7611] [BE] Cover the score tie-break and attribute the fast-path query
Add an integration case where two candidates tie on the objective score. Under a tie
the best candidate is the earliest one, which is also what the baseline resolves to,
so best and baseline cost and duration must coincide. The two candidates are given
sharply different costs and trace durations, so picking the later candidate would
break that equality rather than passing silently. The previous implementation had no
defined tie-break and returned different values for identical data depending only on
the query plan, so nothing pinned this down before.Also thread log_comment through the fast-path query, which was executing unattributed,
and render it inside the workspace context so the comment can carry the workspace.Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- [OPIK-7611] [BE] Use the class's standard assertion form
The new cases mixed .as() descriptions and usingComparator into the BigDecimal
assertions. The surrounding tests assert isNotNull first, then compare through
StatsUtils.bigDecimalComparator and expect isZero, and carry their reasoning in
comments. Match that so the new cases read like their neighbours.Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- [OPIK-7611] [BE] Assert the tie-break against concrete values, and share the comparison
The tie-break case compared best cost and duration against baseline, which still passes
if both rollups pick the later candidate - the very regression it was meant to catch.
Baseline determinism was being used as a fixed reference while it is itself part of what
is under test. The fixture creates one trace and one span per dataset item, so per-trace
cost reduces to exactly the span cost; assert the earliest candidate's concrete cost and
that it is not the later candidate's.Extract the repeated isNotNull-then-bigDecimalComparator pair into
StatsUtils.assertBigDecimalEquals, next to the comparator it wraps, and use it at the
seven call sites across the two optimization test classes.Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- [OPIK-7611] [BE] Correct two test assertions written against assumed values
The fast-path case asserted feedbackScores was empty, but getScoresAggregation maps an
empty score map to null rather than an empty list, so the assertion failed on a value
the code has always returned. Assert null.The tie-break case asserted the earliest candidate's concrete per-trace cost, which
depends on how that aggregate is derived from the fixture rather than on the tie-break
itself, and the surrounding test only ever asserts these costs are present. Require
instead that neither rollup reports the later candidate's cost and that the two agree.
That still catches either rollup picking the later candidate - the regression the
comparison to baseline alone would have missed - without resting on the derivation.Document on the fast-path query that the check runs as its own statement, so an
experiment inserted between the two reads leaves the aggregates empty for one response;
reads here are already eventually consistent through replica lag.Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- [OPIK-7611] [BE] Pick candidate costs the comparator can actually tell apart
The tie-break case used costs of 0.01 and 0.99 to identify which candidate the rollups
picked. bigDecimalComparator ends by comparing only toBigInteger(), deliberately, since
the quantile figures it also serves are not accurate to the fraction - so both values
truncate to 0 and compare equal, and the assertion that best must not report the later
candidate's cost could never fail. That is what timed out, not the tie-break.Use 1 and 9 so the two candidates differ in their integer parts, and record why in a
comment so the values are not "simplified" back later.Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Co-authored-by: Claude Opus 5 (1M context) noreply@anthropic.com
下载附件