Skip to content

feat(asap-tools): exact materialized-view arm for sketch-bench e2e queries - #845

Open
zzylol wants to merge 8 commits into
feat/e2e-sql-from-tablefrom
feat/e2e-sql-mv
Open

zzylol wants to merge 8 commits into
feat/e2e-sql-from-tablefrom
feat/e2e-sql-mv

Conversation

@zzylol

@zzylol zzylol commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Stacked on the sketch-bench SQL PR (base branch feat/e2e-sql-from-table).

Summary

Adds the baseline_mv arm: ClickHouse with materialized views and no sketches, only exact, mergeable states. Each view's bucket width is the query's repeat interval T.

  • One view per (table, value column, statistic, T), an AggregatingMergeTree keyed by bucket and the union of the groupings that read it, plus their child_of parents. Per-series and instant queries keep every label. Coarser groupings GROUP BY a subset and merge (roll-up). For example, template 16's whole CUBE reads one (region, service, endpoint) view.

  • States:

    Statistic State
    sum, count sumState, countState
    quantile quantilesExactInclusiveState over the view's levels
    distinct count uniqExactState
    instant vector argMaxState(value, ts)
    rate/increase per series: first/last value, min/max ts and count, fed into the same extrapolatedRate
    top-k exact per-key sums, then ORDER BY … LIMIT k
  • Windows: bucket b holds (b−T, b], and an edge flag keeps samples at exactly b apart. Classic's closed [E−W, E] merges the buckets in (E−W, E] plus bucket E−W's edge rows. Multigroup's left-open (E−W, E] merges just the buckets. A window that isn't a multiple of T raises. Instant queries need T = 1 s.

  • Per-arm init SQL, so the baseline arm never pays for the views: init.sql (tables only) and init_mv.sql (tables, then views; the views must exist before data arrives). Both drop old views first.

    • experiment_run_clickhouse.py now uses the run's first mode for ingest monitoring when baseline isn't in the run, so a baseline_mv-only run works.

Testing

  • On the same datasets as the SQL PR, every MV query matches its raw SQL in clickhouse-local over 392 eval points, classic and multigroup. Distinct counts, quantiles, per-series stats and top-k keys match bit-exactly; sums match within 1e-9 (the summation order differs).
  • Mutation checks: ignoring the edge rows, or using left-closed toStartOfInterval buckets, makes 84–132 subtests fail.
  • Real tables: every MV statement and init_mv.sql execute for classic (4 views), multigroup (3 views: http_user_id_cardinality_60s, http_latency_quantile_60s, flows_src_ip_cardinality_60s) and all-metrics8 (56 views).
  • New runner test in tests/test_experiment_run_clickhouse.py. black/flake8 clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

zzylol and others added 7 commits October 13, 2026 01:44
The sketch-bench ClickHouse configs now run baseline_mv beside baseline:
exact mergeable states in materialized views, no sketches. There is one
AggregatingMergeTree view per (metric, value, statistic, interval T). Its
key is the T-second bucket plus the union of the queries' groupings and their
child_of parents (every label for per-series statistics). The states are
sumState, countState, quantilesExactInclusiveState over the queries' levels,
argMaxState per series for instant vectors, and per-series first/last
sample, min/max ts and count for rate/increase. Those feed the same
extrapolatedRate SQL.

Bucket b holds (b - T, b], and an `edge` flag keeps samples at exactly b
apart. A closed window [E - W, E] therefore merges the buckets in
(E - W, E] plus the edge rows of bucket E - W. A query whose window is not a
multiple of T raises, so instant vectors are only served at T = 1 s. The
init SQL creates the views after their tables and before any data arrives.

The test runs every MV query next to its raw SQL in clickhouse-local. Results
match exactly, except sums, which agree within 1e-9 relative.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
Each sketch-bench table now gets one config per arm. The baseline config
(init.sql) creates only the tables. The baseline_mv config (init_mv.sql)
creates the tables and their views. Both files drop the views first, so
none left by an earlier baseline_mv run keeps feeding on the recreated
tables. Both configs list both arms' SQL files, so saved results label every
query by its baseline text.

experiment_run_clickhouse.py monitored ingest under the baseline mode's
client config and output dir. A baseline_mv-only run has neither, so ingest
now uses baseline when the run has it, and otherwise the run's first mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
A multigroup query's window is now ASAPQuery's (E - W, E], so its
baseline_mv SQL merges just the buckets in (E - W, E] and skips the edge
rows that hold a closed window's left boundary. Classic PromQL queries keep
the closed window and its edge rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
…a query

convert() planned views for every query, and plan_views raises when a
window is not a multiple of the interval (e.g. an instant vector, which
reads [T - 1s, T], at T > 1 s). So such a table failed to convert even for
the baseline arm, which needs no views.

Now a table with such a query gets only the baseline arm: init.sql, the
raw SQL and the baseline config, plus a note on stderr naming the queries.
Dropping just those queries from the MV arm would shift its query indices
against the baseline SQL that labels both arms' results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
…MV SQL

The views' `bucket` column is a DateTime (whole seconds), so integer bounds
already filter it exactly. They are now toDateTime(<bound>) anyway, to match
the column type, as the raw SQL's ts bounds now are.

The key-pruning regression test, merged from feat/e2e-sql-from-table, now
passes the views to load_script (this branch's signature). It also runs each
query's MV SQL over views with 1-row granules and checks raw and MV against
the expected counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
Brings in origin/main (#837, #828) and the switch to the table's ASAPQuery
SQL. Conflicts resolved by keeping both sides: the module docstring keeps
this branch's per-arm outputs with the new asapquery.sql line; the tests
keep PlanViewsTest and FlooredEndDatetimeTest; experiment_run_clickhouse
and its test import both _ingest_mode and #828's prebuilt-output helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
@milindsrivastava1997

Copy link
Copy Markdown
Contributor

Code review

Stacks on #844. Core MV logic checks out (closed and left-open windows, -State/-Merge pairs, child_of rollups), and test_mv_sql_matches_raw_sql is good coverage.

  1. Medium — sketch_bench_workloads.py ~L535 (init_sql): only views from the current table file are dropped. If workload A's baseline_mv run created data_value_quantile_60s, workload B on the same data table (different stats/intervals) drops and recreates data but never drops A's view, which likely keeps firing on inserts (untested) and skews B's insert cost. Drop all views sourcing the metric tables (via system.tables dependencies) or use a per-stem prefix.
  2. Medium-low — ~L584 (convert): when the mv arm is skipped (unservable query), stale init_mv.sql, interval_<T>s_baseline_mv.sql, <stem>_clickhouse_mv.yaml from an earlier run remain; a sweep over *_clickhouse*.yaml runs them with misaligned query indices. Delete them on skip; test_unservable_query_skips_mv_arm uses a fresh dir so can't catch this.
  3. Low — ~L425 (plan_views): name = parent[name] raises a bare KeyError if child_of names a label not in the metric — inputs that converted fine before now crash with no context. Validate and raise a ValueError naming table and label.
  4. Low — ~L577 vs ~L413: "window must be a multiple of interval" is checked in both convert and plan_views; a new unservable shape added to only one makes convert crash instead of skip. Share one mv_servable(q) predicate.
  5. Low — ~L443-510: view_states, mv_agg, raw_agg each branch on the same stat names; a per-stat table (state cols, -State, -Merge) would keep them in sync.
  6. Nit — docstring ~L29-31: uses T where it means eval time E (E - W, [E - 1s, E]).

Tests: missing a regression test that baseline init.sql drops existing views, and one for re-converting into a dir that already has mv files.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants