ASAPPlanner is a system to map a query workload to an ASAP plan. A query workload is a batch of queries or a set of repeating queries, in any query language like PromQL or SQL. An ASAP plan is a query plan (like in databases) that uses ASAP primitives like sketches, exact summaries, wavelets, etc.
ASAPPlanner does this mapping in 2 steps: (1) normalizing query workloads from different query languages into a common intermediate representation (IR) (2) mapping the IR to an ASAP plan
Separating these 2 steps is helpful to decouple concerns and for extensibility. Step 1 interprets the query workload semantics and normalizes them into our own IR. Adding a new query language or dialect can be done by extending step 1 and not touching step 2. Step 2 maps query workload semantics to ASAP primitives. Adding a new ASAP primitive can be done by extending step 2 and not touching step 1.
- Interpretation — understand a language-specific query and construct semantic intent.
- Intent canonicalization — normalize equivalent queries from different languages into a common IR
- Mapping intents to ASAP primitives — decide whether and how each intent can be answered by a summary, and select/size the corresponding summary family.
cargo build
cargo test --workspaceNo external setup required. See docs/user-guide/user-guide.md for how to run a
query through the pipeline.
Let us define a few terms.
E.g. PromQL, Clickhouse dialect of SQL, Datafusion dialect of SQL, etc.
The semantics of what the query wants to do. Multiple different query strings (in the same language or different) can have the same query intent.
See examples below. All queries in the same example share the same query intent.
In each example, the orders table has columns time, price, city, and category.
SELECT SUM(price)
FROM orders
WHERE time BETWEEN NOW() and NOW() - 1m
GROUP BY cityand
WITH intermediate_table AS (
SELECT SUM(price)
FROM orders
WHERE time BETWEEN NOW() and NOW() - 1m
GROUP BY city, category
)
SELECT SUM(price)
FROM intermediate_table
GROUP BY citySELECT SUM(price)
FROM orders
WHERE time BETWEEN NOW() and NOW() - 1m
GROUP BY cityand
sum by (cpu) (sum_over_time(orders[1m]))
A set of queries that are to be executed. For now, this is either a batch of queries executed on data at rest, or a set of repeating queries executed on recently ingested data.
A common IR that different query workloads in different languages are normalized to. has no notion of ASAP primitives or summaries. The purpose of this IR is to simply have a common representation for diverse query workloads and languages.
An IR that includes of ASAP primitives, apart from the usual relational and time-series operators.
A DAG constructed in either the pre-ASAP IR or post-ASAP IR. Pre-ASAP plan represents the original exact intent of the input query workload. Post-ASAP plan represents the same intent using ASAP primitives.
As of Aug 13, 2026, ASAPPlanner will be scoped to:
- generating a set of candidate ASAP plans, not choosing the optimal one between them
- not caring about CTSA stages i.e. whether a part of a plan is executed at the collector or at the analytics stage
- not caring about assignment of physical resources, like CPU threads and memory, to nodes in the ASAP plan i.e. ASAPPlanner is NOT doing any phyiscal query planning (ref: database term)
- // TODO: add scope on what "IR to ASAP plan logic" we support right now
query workload
│
│ parse
▼
pre-ASAP plan
│
│ canonicalize
▼
canonical pre-ASAP plan
│
│ ASAP-aware mapping
▼
post-ASAP plan
SELECT service, COUNT(*)
FROM metrics
WHERE region = 'us-east'
GROUP BY service
ORDER BY COUNT(*) DESC
LIMIT 10;topk(
10,
count by (service) (
{region="us-east"}
)
)
Aggregate(
reduction = Reduce(by = [service]),
measures = [TopK { k = 10 }],
child = Filter(
pred = region = "us-east",
child = Scan("metrics")
)
)
The two languages may use very different syntax and data models, but the summary-relevant semantic intent is the same.
Consider:
SELECT service, COUNT(*)
FROM metrics
WHERE region = 'us-east'
GROUP BY service
ORDER BY COUNT(*) DESC
LIMIT 5;Aggregate(
reduction = Reduce(by = [service]),
measures = [TopK { k = 5 }],
child = Filter(
pred = region = "us-east",
child = Scan("metrics")
)
)
Equivalent PromQL converges to the same structure.
Aggregate(reduction = Reduce(by = [service]), measures = [TopK { k = 5 }], ...)
↓
SpaceSaving(k=5)
The canonical algebra should represent query intent rather than reproduce the source language's syntax tree. Canonicalization should make semantically equivalent computations structurally identical so that reusable sub-computations can be recognized and shared.
Operations such as Quantile, DistinctCount, and TopK deserve semantic representation
because they have distinct summary mappings.
Do not add a node merely because SQL has an operator with that name. Add a node when it carries semantic information that matters downstream.
- Change input from a single query string to
QueryWorkload(tracked in #194 - Remove legacy data structures and types (tracked in #179, #205
- Implement the ASAP-aware mapping logic and interfaces
- Connect output of ASAPPlanner to asap-fusion
- Connect output of ASAPPlanner to ASAPCollector and ASAPQuery (see open question #1 below)
- Description of pre-ASAP IR
- Description of post-ASAP IR
- Converting a QueryWorkload to a pre-ASAP plan
- Converting a pre-ASAP plan to a post-ASAP plan
- Guide on how to use ASAPPlanner
- Integration with downstream artifacts: How to connect the output of ASAPPlanner to ASAPQuery? ASAPPlanner produces a post-ASAP plan that has semantics of batch query execution over data at rest. Somehow this needs to be converted into two plans (1) streaming dataflow graph that computes summaries on raw data, and (2) batch query execution plan that uses summaries to answer queries.
- Grouping semantics: Should grouping remain embedded in
Aggregate, or should grouping become a reusable relational dimension node? - Expression semantics: Which arithmetic or derived expressions need dedicated semantic nodes because they materially affect summary selection?
- Approximation contracts: Should accuracy/error requirements be fields on the intent, the workload, or the measure itself?
- Summary composability: How should nested intents describe summaries that can be merged, transformed, or reused across queries?