Skip to content

fix: one finest-grain stored summary serves several groupings (quantiles at the estimate level, sums at the state level) - #804

Merged
zzylol merged 3 commits into
mainfrom
fix/per-series-summary-shares-identity
Oct 21, 2026
Merged

zzylol merged 3 commits into
mainfrom
fix/per-series-summary-shares-identity

Conversation

@zzylol

@zzylol zzylol commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Why

ASAPQuery keeps one finest-grain summary per series and answers every grouping of a metric from it: one sketch per series for the quantiles, one per-series sum for a fine and a coarse sum. The joint plan built one copy per query instead. Each query's PromQL scan projects only the labels its own by needs, so per-series summaries holding the same states got different definitions:

  • avg by (region) (quantile_over_time(0.99, data[100s])) and avg by (region, service, job) (quantile_over_time(0.9, data[100s])) stored two per-series DDSketch definitions, about 1.6 GB each at ASAPQuery's scale.
  • sum by (region, service, job) (sum_over_time(data[W])) and sum by (region) (sum_over_time(data[W])) stored two per-series Sums.

What

In both cases, one finest-grain stored summary now serves several groupings.

  • Quantiles: one summary at the estimate level. One per-series DDSketch is stored. Each query estimates its quantile per series from it, keeps the labels it needs, then applies its own avg by G. Sketches are not merged.
  • Sums: one summary at the state level. One per-series Sum is stored. Each grouping's sum is computed from the per-series sums (the per-series Sum states combined by group), then finalized.
  • Rollups over panes. A rollup Pass 2 candidate is now also built over tumbling panes. Before, Pass 2 never combined the rollup rule with window composition, so a rolled-up plan read its finer build's whole window from raw samples, and every rollup was rejected on a deployment that keeps no raw samples.

How

  • Summary-capability rule. It now groups per-series builds whose scans of one metric project different labels. The scans must have no label matchers and the same windows and offsets (the rollup rule's existing same_rows), the builds no filter, and both inputs a $promql_series_identity column. A per-series state is keyed by the series identity, which determines every label, so these builds hold the same states. Besides sketches, the rule now covers per-series exact accumulators read only through FinalizeExactAccumulator.
  • The shared build. It is the member whose scan has every member's labels; a group without one is not shared. Every reader of each other member (an estimate for a quantile, a finalization for a sum) is rebuilt over the shared build, followed by a Project that keeps that reader's own columns by name, so the operators above it are unchanged.
  • Sign through the Project. A projection of columns now keeps the sample sign of the values below it, so DDSketch's relative bound still carries through avg over a declared non-negative gauge. Only bare column references pass, so no computed value inherits the sign.
  • Rollup with panes. After each rollup Pass 2 candidate, its window-composition Pass 2 candidates are listed too.

Key code interfaces added / modified

crates/logical-asap-optimization/src/asap_aware_cse.rs

/// Summary builds the summary-capability rule can make one build. The type stays.
pub struct CapabilitySharingCandidate {
    /// The first member's family: every member builds this sketch algorithm at some
    /// size, or this per-series exact accumulator (read only through finalization).
    pub family: FieldDataType, // was SketchAlgorithm
    /// The summary builds, at least two, not all equal (in size or in projected labels).
    pub members: Vec<Rc<OperatorNode>>, // unchanged
}

Usage (the tests quantile_queries_with_different_groupings_share_one_per_series_sketch and fine_and_coarse_sums_store_one_sum):

// Three quantile queries over one metric and window, every 10 s, relative 0.05,
// over six series of a non-negative gauge, on a deployment that keeps no raw samples.
let plan_json = jointly_planned(&[
    "avg by (region) (quantile_over_time(0.99, data[100s]))",
    "avg by (region, service, job) (quantile_over_time(0.9, data[100s]))",
    "avg by (service) (quantile_over_time(0.5, data[100s]))",
], RELATIVE_0_05).await;
// Stage 1's capability rule shares the per-series DDSketch over the scan with
// job, region and service; Stage 3 selects it, panes built at ingestion.
assert_eq!(stored_definitions(&plan, "DDSketch").len(), 1);   // was 3
// Fine and coarse sums: one per-series Sum serves both group sums.
assert_eq!(stored_definitions(&sums_plan, "\"Sum\"").len(), 1); // was 2
// Executed from the stored panes, every answer equals query_time_reference.

Before this PR

  • The three quantile queries stored 3 DDSketch definitions.
  • The two sums stored 2 Sum definitions.
  • No rollup candidate was built over panes.

After this PR

  • The three quantile queries store 1 DDSketch definition.
  • The two sums store 1 Sum definition.
  • Rolled-up plans over panes are offered and priced.

Verification

Tests written first.

  • planner/tests/runtime_install.rs:
    • quantile_queries_with_different_groupings_share_one_per_series_sketch failed on main with 3 DDSketch definitions.
    • fine_and_coarse_sums_store_one_sum failed on main with 2 Sum definitions.
    • Both now pass. They execute the plan from stored panes, and every answer equals query_time_reference (2, 6 and 3 answers for the quantile queries; 6 and 2 for the sums).
  • logical-asap-optimization a_rollup_is_also_built_over_tumbling_panes: some Stage 1 candidate of the sum pair both rolls up and builds panes. It failed without the rollup change.

Added in review.

  • per_series_answers_beside_a_grouping_share_one_state_and_answer_as_the_reference: topk(2, sum_over_time(...)) beside sum by (region), and a per-series quantile_over_time beside avg by (region). Each stores one per-series state, and every answer, including the top-k series' labels, equals the query-time reference.
  • The reuse_rules execution test now expands pane chains and feeds each raw TimeRange the samples in its own window, so the paned rollup candidate is executed and compared, not skipped.

Existing tests updated to the shared plans.

Commands. cargo test --workspace --locked, after rebasing onto main: all test binaries passed. Under the current speed-first policy, clippy was not run.

Limitations and follow-up.

  • Rollup plan not selected in the test. On the sum pair, Stage 3 selects the shared per-series Sum, not the paned rollup. With no group-count declaration, the cost model assumes 100 groups for every by (...) reduction at ingestion (DEFAULT_GROUP_COUNT), even over 6 series, so the rollup's finer build prices higher. Follow-up: cap the assumed group count at the declared series count.
  • Sharing needs a label superset. One member's scan must have every member's labels (true for the ASAPQuery workloads). Queries whose labels are not nested, for example by (region) and by (job) alone, still get separate builds; sharing them would need a scan with the union of the labels. Scans with label matchers are not grouped.
  • Pass 2 candidate count. Each rollup Pass 2 candidate is now followed by its window-composition candidates, so the two rules' counts multiply; each rule keeps its own cap, but the product is not capped.

@zzylol zzylol added bug Something isn't working optimizer Workload optimization and plan selection promql PromQL front-end lowering (L1→L2) labels Oct 21, 2026
A per-series summary is keyed by the series identity, but each query's
PromQL scan projects only the labels its own `by` needs, so
`avg by (region) (quantile_over_time(0.99, data[100s]))` and
`avg by (region, service, job) (quantile_over_time(0.9, data[100s]))`
got different per-series DDSketch definitions, and so did the per-series
Sum panes of a fine and a coarse `sum by (...) (sum_over_time(...))`.
- The summary-capability rule now groups per-series builds whose scans
  of one metric (no matchers, same windows and shifts) project different
  labels, and also per-series exact accumulators read only through
  finalization. The shared build is the member over the scan with every
  member's labels; each other member's reader (estimate or finalize) reads
  it and a Project keeps that reader's columns by name.
- A projection of columns keeps a quantile estimate's sample sign, so
  the relative bound still carries through avg over the shared sketch.
- CapabilitySharingCandidate.family is the first member's FieldDataType.
Tests: three quantile queries with different groupings store one
DDSketch definition, and a fine and a coarse sum one Sum definition, each
answering as its query-time reference; the stored-output tests of #781
and #795 follow the shared plans.
…anes

The rollup rule's Pass 2 candidates were never composed with the
window-composition rule, so a rolled-up plan read its finer build's whole
window from raw samples, and a deployment that keeps no raw samples
rejected every rollup. After each rollup Pass 2 candidate, its
window-composition Pass 2 candidates are listed too: the finer build as
panes, the coarse computation rolled up from their merged state.
Test: for a fine and a coarse sum over one window, some Stage 1 candidate
both rolls up and builds panes (it failed before). The reuse-rule
execution test leaves out paned candidates, whose panes it does not feed.
… beside a grouping

The reuse execution test expands pane chains and feeds each raw
TimeRange the samples in its own window, so rolled-up candidates built
over tumbling panes are executed instead of skipped. A new runtime test
plans a top-k of per-series sums and per-series DDSketch quantiles beside
a grouping of the same values and checks one stored state and answers
equal to the query-time reference. Docs of the capability-sharing rule
name the label-superset case; an unused import is moved to the tests.
@zzylol zzylol changed the title fix: one finest-grain stored summary serves several groupings (per-series sketches and sums, rollups over panes) fix: one finest-grain stored summary serves several groupings (quantiles at the estimate level, sums at the state level) Oct 21, 2026
@zzylol
zzylol force-pushed the fix/per-series-summary-shares-identity branch from 45a0049 to 7782883 Compare October 21, 2026 09:16
@zzylol
zzylol merged commit ef52e17 into main Oct 21, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working optimizer Workload optimization and plan selection promql PromQL front-end lowering (L1→L2)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant