Repository navigation
Retained raw data as concrete state groups per lookback: offsets, @ and a retention limit #955
Description
Problem, by example
The cases below come from reading Stage 3 on main (plan-selection/src/lib.rs), not from a planner run.
1. An offset is not retained. For rate(m[5m] offset 1h) evaluated at query time, the selector reads raw samples from (t − 65m, t − 60m], so 65 minutes of m must be kept. Stage 3 keeps and prices 5 minutes. windows_read follows a TimeShift but "moves a window without widening it", so query_time_lookback_ms returns 5 minutes. The plan is priced as if it needed one thirteenth of the raw data it reads. @ T has the same gap: the window it reads lies now − T back.
2. Retention is all or nothing. The deployment declares DeploymentCapabilities::raw_data_retained: bool.
- With
true, a query-time scan of any window is valid, even when the deployment actually keeps, say, 15 minutes of raw data, as a short-retention Prometheus does. - With
false, every query-time scan is rejected (raw_data_violation).
So no deployment can say "15 minutes kept", and no plan is rejected or repriced because its horizon exceeds what is kept.
3. The horizon is a plan-level afterthought. Stage 3 already charges raw data once per source, at the longest lookback of any query-time scan of that source (the raw_data map in price_at_scale, priced at cost_per_disk_byte_second). That is the right quantity, but it is computed only after a whole plan is fixed. The memo has no raw-horizon site, although the design doc of #509 (memo section, Pass 2 row) lists "Raw horizon: a source's raw-data horizon site on every candidate that reads a raw window of it". In fact the code has no coupling-site type at all. So a reader choosing between "scan 1 hour of raw m at query time" and "maintain a pane chain at ingestion" is not told that another reader already pays for that hour, or that it alone would extend the horizon.
Proposal: retained raw data as a concrete state group per lookback
Requirement of a candidate
Every candidate that reads raw rows of source S at query time declares a lookback r(S), the oldest raw data it reads, measured back from its evaluation time:
| Reader | Lookback |
|---|---|
SQL ts > now() - INTERVAL 'w' |
w |
PromQL m[w] |
w |
PromQL m[w] offset o |
o + w (fixes example 1) |
PromQL m[w] @ T |
w + (now − T) at the latest evaluation; unbounded for an @ that falls ever further back |
instant selector m |
the source's look-back (sample_interval) |
| a scan with no time bound | unbounded |
Ingestion-time readers declare nothing: they read rows as they arrive.
Retained raw data as concrete state groups (decided 2026-10-27)
There is no new site type. The memo already represents a shared choice by concrete state groups, one per threshold (memo/state_groups.rs: readers needing KLL k ≥ 345 and k ≥ 200 get concrete KLL(345) and KLL(200) groups). Retained raw data uses the same interface:
- For a source
S, one concrete groupraw window(S, H)per distinct lookbackHamong the candidates that readSat query time. It stands for the raw rows ofSkept forHback from the evaluation time. - A reader with lookback
rgets one candidate perraw window(S, H)withH ≥ r. The candidate reads that group and keeps only its own window,(t − o − w, t − o]or(T − w, T]. - The group's own cost is its retention, charged once whatever the number of readers:
retention(S, H) = bytes_per_second(S) × H × cost_per_disk_byte_second
Choosing the horizon becomes choosing which raw window group the readers read, exactly as choosing a shared sketch size is choosing a KLL(k) group. The search then sees "this reader extends the horizon from 5m to 1h" as the price difference between reading raw window(S, 5m) and raw window(S, 1h), instead of a maximum Stage 3 recomputes after the plan is fixed. Today's per-source maximum (raw_data in price_at_scale) is what the search obtains when every reader of S reads one group.
This is also the shared-producer shape of #954: the covering need is the longest lookback, and the derivation is the reader's own window filter.
Deployment capability
raw_data_retained: bool becomes a horizon limit per source, defaulting to the current behavior:
/// How much raw data the deployment keeps, per source (default: all; `None` = unbounded).
raw_data_retention: RawRetention, // was raw_data_retained: bool
enum RawRetention {
/// Kept as long as any plan needs: today's `true`.
Unbounded,
/// Nothing kept after arrival: today's `false`.
None,
/// At most this much per source, e.g. a short-retention Prometheus.
Limit(HashMap<SourceName, Duration>),
}A candidate whose lookback exceeds the limit is invalid, with the lookback and the limit as the rejection reason. raw_data_violation generalizes from "any query-time scan" to "a lookback over the limit". When raw data is already kept by an external store at no marginal cost, the deployment sets a zero disk price for raw data, and the limit still bounds which raw window groups exist.
Relation to #951
The shared scan of #951 reads the union of its readers' windows. That union's oldest point is exactly the lookback the shared scan declares. Both rules use one function for a reader's window, (t − o − w, t − o] or (T − w, T], so they cannot disagree.
Scope and acceptance
In scope:
- the lookback function (with offsets,
@, instant selectors, SQLtsbounds); raw window(S, H)groups through the state-group interface, their reader candidates, and their pricing in Stage 3 (replacing the after-the-factraw_datamaximum);- the
RawRetentioncapability; raw_data_violationgeneralized to a limit;- the docs: propose workload-wide planning, summary sharing, and materialization #509 design doc's description of raw horizon as
raw windowgroups.
Out of scope:
- tiered storage (memory vs disk for raw data).
Acceptance:
- Example 1 fails first:
rate(m[5m] offset 1h)at query time keeps and prices 65 minutes ofm, not 5. - With
Limit(15m), a 1-hour query-time scan is rejected with a reason naming both durations, and a plan that maintains the same query at ingestion is selected instead. - Two readers of one source with lookbacks 5m and 1h both read
raw window(S, 1h)when that is cheapest, and the 1h retention is charged once, not 65 minutes. - Default
UnboundedandNonereproduce today's selected plans and costs on the existing tests (stored_data_without_raw_data_retained_is_bad_input, theraw_data_retained: falsetests inplan-selection,materialization_e2e).
Related
The design doc of #509 (memo section, Pass 2 row: "Raw horizon"); #951 (shared scan: the union of readers' windows); query_time_lookback_ms, windows_read, raw_data_violation and price_at_scale in plan-selection/src/lib.rs.
Activity
[-]Raw-data horizon as a per-source coupling site: offsets, @ and a retention limit[/-][+]Retained raw data as concrete state groups per lookback: offsets, @ and a retention limit[/+]zzylol commentedon Oct 27, 2026
Two parts of this issue land with the physical → deployment refactor (#968):
PhysicalASAPDAGinasap-types:scan_window(scan). It returns the union of the readers' windows,(T - end_offset - width, T - end_offset], orUnbounded. The executor's table-slot window, theTimeRangelowering, and Stage 3'squery_time_lookback_ms/windows_readall use it. Example 1 (rate(m[5m] offset 1h)priced as 5 minutes) is fixed there, with a test that fails before the fix.InstallablePlan::raw_data_retention(), which gives, per source, the longest query-time lookback, computed with the same function. A deployment learns from the plan file how long to keep raw data. Once this issue addsraw window(S, H)groups,raw_data_retention()returns the selectedH.Still in this issue:
@lookbacks, theRawRetentioncapability and its limit, and theraw window(S, H)memo groups with their pricing.fix(plan-selection): keep a shifted query-time window's whole raw loo…
zzylol commentedon Oct 28, 2026
Two more raw-retention gaps in Stage 3 pricing, found while reviewing #991 (
InstallablePlan::raw_data_retention()):max_over_time(m[5m])evaluated over the last 1 h, with query-time results kept: the panes keepretention_ms = 3_900_000, because a range request may refresh a pane for its oldest step. That refresh reads rawm65 minutes back. Stage 3's raw-data disk price uses only the scan's window lookback (5 minutes), notlookback + the readers' range span. The same gap applies to any range-evaluated query that scans raw data at query time. refactor(ir, planner): InstallablePlan holds a versioned DAG and query texts; StoredOutput becomes MaterializedView #991 fixes the number handed to the runtime by recording it on theScannodes at installation. Stage 3 still under-prices it.Both are lookbacks of the
raw window(S, H)groups this issue proposes.Hshould be the maximum, over readers, ofscan_windowlookback plus range span, with kept chains counted as all their panes.