Skip to content

[Design] Why the SummaryExpr is designed like this? #463

Open

Description

@Selvomega

See the following code

fn summary_children(expr: &SummaryExpr) -> Vec<&Rc<SummaryNode>> {
    match expr {
        SummaryExpr::KeepPreAsap(_) => vec![],
        SummaryExpr::BinaryOp { lhs, rhs, .. } => vec![lhs, rhs],
        SummaryExpr::CandidateTopK { candidates, values, .. } => vec![candidates, values],
        SummaryExpr::ValueOperation { child, .. } => vec![child],
        SummaryExpr::RelationalJoin { left, right, .. } => vec![left, right],
        SummaryExpr::SummaryAgg { child, .. } => vec![child],
        SummaryExpr::SummaryJoin { outer, inner, .. } => vec![outer, inner],
        SummaryExpr::SummarySubtract { left, right } => vec![left, right],
        SummaryExpr::SummaryDelete { summary_input, .. } => vec![summary_input],
        SummaryExpr::SummaryEstimate { summary_input, .. } => vec![summary_input],
        SummaryExpr::SummaryMerge { children } => children.iter().collect(),
    }
}

I don't know what is the design principle of these branches. For example, I cannot understand

  1. Why ValueOperation is a parallel concept with BinaryOp: BinaryOp looks like a special case of projection, which is a sub-concept of ValueOperation.
  2. Why KeepPreASAP is a parallel concept with ValueOperation: They both contain lots of repeated concepts, like projection, filter, so on and so forth
  3. What is the relation between ExactAggregate in SummaryAgg and the original aggregation in KeepPreAsap?

I feel there are a lot of semantic overlaps in the post-ASAP-DAG language and am really lost.

Activity

  1. zzylol commented on Sep 24, 2026

    @zzylol
    Contributor

    The confusion is understandable: SummaryExpr mixes several classification axes, rather than representing a strict hierarchy of operator semantics. The three distinctions in the current implementation are:

    1. BinaryOp vs. ValueOperation

    ValueOperation wraps one post-ASAP child, while BinaryOp connects two independently planned children and carries PromQL vector-matching semantics.

    Arithmetic between columns of a single input row can be represented as a projection. Arithmetic between independently computed vectors also needs to match elements across the two inputs. The separate variant preserves both input plans and those matching semantics.

    So the distinction is primarily about input structure and composition. The name ValueOperation is broader than its current single-child representation, which makes the overlap confusing.

    2. KeepPreAsap vs. ValueOperation

    These represent different planning boundaries:

    • KeepPreAsap embeds an entire original QueryExpr subtree. Its internal operators are not exposed as post-ASAP nodes.
    • ValueOperation exposes an operation over a post-ASAP child, allowing that child to contain independently selected summaries and readouts.

    For example:

    KeepPreAsap(Project(Aggregate(Scan)))
    

    retains the original computation as a whole, whereas:

    ValueOperation(Project)
      → SummaryEstimate
        → SummaryAgg
          → KeepPreAsap(Scan)
    

    allows the aggregate to use a summary while preserving the projection above its readout.

    The projection/filter semantics can therefore be the same in both representations. What differs is how much of the computation has been exposed to post-ASAP planning. This also explains why summary_children(KeepPreAsap(...)) returns no children: it has no SummaryNode children, even though it contains a QueryExpr subtree.

    3. ExactAggregate in SummaryAgg vs. an original aggregate in KeepPreAsap

    Both can produce exact results, but they expose different execution contracts:

    Representation Meaning
    KeepPreAsap(Aggregate(...)) Retains the original aggregate computation.
    SummaryAgg with an ExactAggregate family Selects a concrete exact accumulator and explicitly represents its summary state.
    ValueOperation(Exact(Aggregate(...))) Executes ordinary aggregation over an independently planned post-ASAP child.

    Here, “summary” does not necessarily mean “approximate.” An exact accumulator is also summary state, with explicit maintenance and applicable merge behavior.

    The current implementation uses FinalizeExactAccumulator to mark the transition from maintained exact state to a read-time value, even when the accumulator state already represents the scalar result. Approximate summaries instead use SummaryEstimate for their readout.

    The organizing distinctions are therefore retained logical subtrees, computation over ordinary values, and computation over summary state, with input structure and execution timing providing additional dimensions. The semantic overlap is real, but these variants expose different planning and execution boundaries.

    References: expression definitions, execution-state rules.

  2. Selvomega commented on Sep 24, 2026

    @Selvomega
    CollaboratorAuthor

    Further questions:

    Against your response 1:

    Is it true that BinaryOp is only used for PromQL, and has nothing to do with the SQL world?

    Against your response 2:

    1. Is the sub-tree covered by KeepPreASAP not re-shaped by the optimizer at all? I see some generated plans half-ValueOperation and hald-KeePreASAP,
    2. I still do not understand why you want to do such separation for, say, a projection operator. Operators like Projection only semantically mean that I am going to do some per-row transformation from my input data. Why you think pre-ASAP Projection and post-ASAP Projection need to be distinguished? (since to me they have the same semantic)

    Against your response 3:

    The way I understand your explanation is: Our "Summary" actually means "State". And in our current design, we are considering the query execution in the following perspectives:

    • What kind of data are we operating on: Summary defined by us v.s. others
    • What kind of operations are we performing: Like project, filter, agg, binaryop, etc.
    • Whether the execution is legacy execution or ASAP execution

    Based on the above dimensions, we have

    • KeepPreAsap(Aggregate(...)) fall into others + agg + legacy
    • SummaryAgg fall into summary + agg + asap
    • ValueOperation(Exact(Aggregate(...))) falls into others + agg + asap
    • so on and so forth

    Is this understanding correct?

    In general

    As you yourself said: "SummaryExpr mixes several classification axes", can you articulate what are the classification axes?

    I tried to mentally reverse-engineer some axes in the section above, but I feel it would be better to have you articulate the design intention directly.

    Also, according to what I have reversed out, I feel some axes may be unnecessary, like "legacy execution vs ASAP execution". However, now I am not sure either if this an actual axis, or if my feeling of un-necessity is correct.
    Therefore, I feel it will be extremely helpful for you to articulate out the design considerations of the post-ASAP dag language. :)

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions