Skip to content

Query engine: merge sketches across subpopulations (roll-up to a coarser grouping) #839

Closed

Description

@milindsrivastava1997

Goal

Let the query engine answer a query grouped by label set Q from a precomputed aggregation grouped by G ⊋ Q by merging the sketches of every subpopulation that shares the same Q-projection (a roll-up). Today the engine only merges across time.

Example: KLL deployed by (service, endpoint). Query quantile by (service) (0.5, ...). Answer it by merging, per output step, all (service, *) KLLs into one KLL per service, then querying the quantile. Mergeable summaries (KLL, DDSketch, HLL, CMS, Sum, MinMax, Increase) keep their error guarantee under this merge, which re-aggregating final scalars (quantile-of-quantiles) does not.

Current state

  • Matcher: labels_compatible (asap_types/src/capability_matching.rs:153) is exact-equality only (TODO in place, tracked in labels_compatible() is exact-match only, not superset matching #649). The test label_strict_superset_rejected pins this behaviour.
  • Engine: estimate_range_query (simple_engine/mod.rs:2690) merges buckets per store group key across time (window_merger.rs, merge_utils.rs). Different group keys are never merged together.
  • Output labels: query_output_labels = requirements.grouping_labels. Store-key rows are zipped by position with those labels (utils/http.rs:128), which is only correct because G == Q today.
  • Planner: milp_output.rs:96-110 refuses roll-ups explicitly (RollupUnsupported, test rollup_is_refused), because the engine can't serve them.
  • Existing scalar regrouping (engines/query_time_aggregation.rs, nested_aggregation_plan) regroups final f64 values. It is correct for sum-of-sums but wrong for quantiles, so it does not cover this case.

Scope sketch

  1. Matcher: accept config.grouping_labels ⊇ req.grouping_labels when the value type is mergeable (labels_compatible() is exact-match only, not superset matching #649). Prefer an exact match over a superset match.
  2. Engine: after the per-key time merge, project each store key onto Q (index map from G to Q), bucket the keys by projection, and fold each bucket with merge_with (or the batch merge in merge_utils.rs). Then query the statistic and emit rows labelled with Q.
  3. Planner: lift RollupUnsupported once the engine supports roll-ups, so the MILP can share one deployment across AQEs (No cross-AQE sharing — greedy deploys one sketch per AQE even when they could share #650).

Open questions

  • Keyed types (MultipleSum, CMS, CMS+heap, HydraKLL) put labels in aggregated_labels with an empty grouping (Capability-matching fallback never matches keyed aggregations (grouping_labels = []) #779). Do roll-ups over aggregated labels belong in scope? For CMS, a subset query would need every matching sub-key enumerated from the DeltaSet key aggregator, then the point estimates summed, and the error grows with the fan-in.
  • Merge order: merge across subpops first then across time, or the other way round? The result is the same for commutative merges, but cost differs. DeltaSet is order-sensitive.
  • Cost: the fan-in can be large (endpoint cardinality). Is a batch merge needed from day one?
  • Instant vs range: both go through estimate_range_query, so one change should cover both.
  • SQL: the same thing for GROUP BY service over a (service, endpoint) config.

Related: #649, #650, #779, #405.

Activity

  1. milindsrivastava1997 commented on Oct 13, 2026

    @milindsrivastava1997
    ContributorAuthor

    Resolved design (v1)

    Terms. G is the value config's grouping_labels. Q is the query's output labels (query_output_labels).

    1. No new config field. The engine infers a roll-up from G ⊋ Q, and QueryConfig doesn't change. Capability matching is not involved: it only runs when no query_config matches the query string.
    2. Scope: single-subpopulation configs only. These have empty aggregated_labels and no separate key aggregation, i.e. KLL, DDSketch, HLL, Sum and MinMax. Increase is excluded. Keyed configs (Multiple*, CMS, CMS+heap, HydraKLL, topk-by) keep today's path; their roll-ups are Query engine: roll-ups over keyed aggregations (aggregated_labels) #846.
    3. Where. Re-key the read output. Right after the store read, map each G-key in ComposedRangeRead.groups to its Q-projection and append its buckets into the same BucketMap slot (start_ts → Vec). estimate_range_query stays unchanged: one batch get_merged() per (Q-key, step) merges across time and subpopulations together, fallback_key becomes the Q-key so output labels are right, and instant queries share the path. Rejected alternatives: inside the store (leaks query semantics into the store trait), a second merge after the per-key time merge (restructures the step loop and costs N+1 merges), and query_time_aggregations (a quantile of quantiles is wrong).
    4. Sliding completeness. The range per-step check counts distinct start timestamps, not buckets, for every query. A window that hasn't been computed yet is missing for all subpopulations at once, so it's still caught. A subpopulation with no data in a window just contributes nothing. This also fixes a latent bug where duplicate buckets from same-key fan-in could mask a missing window. The instant all-or-nothing check (per raw G-key) stays as is.
    5. Errors. A single rollup_plan(G, Q, type) -> Result<Option<Projection>, _> in the shared context path covers PromQL, SQL and Elastic, gated on single-subpopulation configs. G == Q returns None (today's path). G ⊋ Q with a v1 type returns a projection. Q ⊄ G and an unsupported type both return Err(Native), not a fall-through to Prometheus, because both mean the planner or config is wrong.
    6. Producers. v1 covers the engine and the MILP planner. The capability-matcher superset change stays in labels_compatible() is exact-match only, not superset matching #649.
      • KLL / DD / HLL: a query_config pointing at the G-grouped aggregation. The engine infers the roll-up.
      • exact-sum / min / max: these are emitted as keyed Multiple* types, but final-value regrouping is exact for them, so the MILP writes planned_subquery (the by clause rewritten to G on the AST) plus query_time_aggregations = [{Sum|Min|Max, by Q}]. Count uses Sum. No engine change is needed.
      • exact-increase / cms-heap: keep RollupUnsupported. sketch-bench's mergeable_across_groups never proposes them anyway.
      • Labels: aggregation_config takes its labels from deployment.grouping_labels, not the first served item.

    Tests

    1. Unit: rollup_plan and the re-key, covering label reordering, Q = ∅, same-projection appends at the same start_ts, and the error cases.
    2. Equivalence through SimpleEngine (instant and range): compare a (service, endpoint) config against a (service) config fed the same samples. Sum and MinMax must be exactly equal. For KLL, DD and HLL, the result must equal a manual merge_with of the stored sketches and be within the sketch's error bound of the truth.
    3. Regression: the distinct-timestamp Sliding check (duplicate bucket plus a missing window gets skipped), topk and keyed queries unchanged, G == Q output identical to before.
    4. PromQL compliance: a new optional Suite.planner_output (fixed streaming and inference YAML, skipping the planner), plus a rollup.yaml suite compared against Prometheus. Its dataset includes an endpoint that appears mid-range.

    PR plan

    1. Sliding check counts distinct start timestamps, plus its regression test.
    2. Engine roll-up: rollup_plan, the re-key, and test layers 1–3.
    3. Compliance harness: planner_output override plus the rollup.yaml suite (layer 4). Depends on 2.
    4. MILP lift for sketches: labels from the deployment, allow KLL/DD/HLL, replace the rollup_is_refused test.
    5. MILP lift for exact types: planned_subquery plus query_time_aggregations.

    PRs 4 and 5 are independent of 1–3. Follow-ups: #649 (matcher superset) and #846 (keyed roll-ups).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions