MQE: add subquery support to range vector splitting - #16444
Conversation
…t step-invariant expressions
…to CSE's own pass
|
💻 Deploy preview available (Mimir): |
|
@cursor review this pr |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
|
||
| // Range vector splitting can materialize and execute a nested Subquery/StepInvariantExpression more than | ||
| // once, once per split block, deduplicate it here. | ||
| splitSubqueriesDeduplicated, err := e.deduplicateSplitSubqueriesAcrossBlocks(plan.Root) |
There was a problem hiding this comment.
Note for reviewers: subqueries are evaluated internally as their own range query, with a step grid aligned to Unix epoch time 0. When splitting also applies to a subquery, that alignment means two different split blocks (or two different levels of subquery nesting, see tests) can make a nested Subquery/StepInvariantExpression compute the exact same derived time range for its own child. Materializing and executing that child multiply times would be duplicate work and also breaks the invariant that a plan node is only materialized once (see singleUseOperatorFactory).
To handle this, the child of any Subquery/StepInvariantExpression nested inside a split subquery is wrapped in a Duplicate node, so the different blocks share the one computation instead of redoing it.
There was a problem hiding this comment.
I agree with this approach, though as a note though this could cause nodes to be "unnecessarily" wrapped with a duplicate node, even when the splits don't have colliding nested subqueries. I don't think that would add too much overhead though, and I guess the problem is we can't predict which nodes will collide until we look up cache entries to figure out cached and uncached splits.
| expect no_info | ||
| {} 10.5`, | ||
|
|
||
| // TODO: Precision is lost calculating sum_over_time/avg_over_time across a block boundary, in cases |
There was a problem hiding this comment.
This is a pre-existing precision-loss, not caused by subqueries. It's already reproducible on main today a plain selector (test), subquery splitting just exposes it here as well. Fixing it is out of scope for this PR, just to keep the changes to a reasonable size.
fionaliao
left a comment
There was a problem hiding this comment.
With the help of Claude, I found a couple of bugs in the implementation: nt/rvs-subquery-support...fl/rvs-subquery-splitting-repro-bugs
Bug 2/3 are basically the same issue - we sometimes cache data within the OOO time window, as we use the time range for the top-level subquery to determine if we overlap with that window, rather than the actual time range selected in ingesters/store-gateways (impl here). This wasn't a problem with just range vector selectors since the queried time range and the selector time range was the same.
There is also this edge case with dropnames:
mimir/pkg/streamingpromql/optimize/plan/rangevectorsplitting/operator.go
Lines 385 to 399 in bb04969
I think we should just decide to handle this in a way that doesn't error (maybe just take the value of drop name from the first split?), even if it differs from unsplit behaviour
|
|
||
| // Range vector splitting can materialize and execute a nested Subquery/StepInvariantExpression more than | ||
| // once, once per split block, deduplicate it here. | ||
| splitSubqueriesDeduplicated, err := e.deduplicateSplitSubqueriesAcrossBlocks(plan.Root) |
There was a problem hiding this comment.
I agree with this approach, though as a note though this could cause nodes to be "unnecessarily" wrapped with a duplicate node, even when the splits don't have colliding nested subqueries. I don't think that would add too much overhead though, and I guess the problem is we can't predict which nodes will collide until we look up cache entries to figure out cached and uncached splits.
There was a problem hiding this comment.
Could you add a test with subqueries where series from the inner node aren't returned in a sorted order? For range vector selector cases, series returned from ingesters and store-gateways are always expected to be sorted. However, subqueries can reorder or group series loaded from storage in a way where the resulting series seen by the split function aren't sorted anymore. mergeSplitsMetadata should handle unsorted input (it doesn't assume inner node results are sorted), but I'd like a test to exercise that.
As an example: sum_over_time((metric_a or metric_b)[5h:1h]) - series matching the RHS only (metric_b) are returned before the LHS ones.
There was a problem hiding this comment.
Added tests covering this (unsorted_merge_* cases). Walked through the merge logic, it keys by labels, not by position. It doesn't rely on the splits returning series in order.
# Conflicts: # pkg/streamingpromql/planning/plan.go
* renamed deduplicateSplitSubqueriesAcrossBlocks → insertSplitSubqueryDuplicates, deduplicateAcrossSplitBlocks → insertDuplicatesAcrossSplitBlocks * fixed isDeduplicateOrDeduplicateFilter → isDuplicateOrDuplicateFilter * use ChildrenIter instead of index loops * removed stale test-line-number reference in a comment, named the test case instead
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 90a0721. Configure here.
Good catch! Yeah, you're right and root cause is the same for both. A smoothed/anchored/negative-offset selector nested inside a split subquery can make a cached block's read land inside ooo window, since the block boundary math only looks at the subquery own range/offset, with no visibility into what's inside (sub-sub query). Fixed by refusing to split them, the same restriction |

What this PR does
Adds experimental support for range vector splitting to also split subqueries, so queries like
sum_over_time(max_over_time(metric[5m])[1h:1m])benefit from block-level caching that range vector selectors already get.core.Subquerymaterialization to support being split into blocks.-querier.mimir-query-engine.range-vector-splitting.enable-subquery-splittingflag (experimental, disabled by default) and bumps the query plan version to V20Checklist
CHANGELOG.mdupdated - the order of entries should be[CHANGE],[FEATURE],[ENHANCEMENT],[BUGFIX]. If changelog entry is not needed, please add thechangelog-not-neededlabel to the PR.about-versioning.mdupdated with experimental features.