Skip to content

Design proposal: separate "what kind of computation" from "which dimensions to group by" in the intent algebra (dbt-style measure/dimension split) #165

Description

@zzylol

Motivation

In a multi-language query system, an aggregation node actually answers two independent questions at once: "is this operation performing a reduction at all," and, if so, "which dimensions is it reducing by." When the answer to the first question is inferred implicitly from whether the second happens to be empty, the two questions collapse into one — and there are real cases where a language-level construct has neither "I'm reducing" syntax nor "I'm grouping by these dimensions" syntax, yet looks identical, on the surface, to "explicitly declared a reduction over zero dimensions." That implicit coupling is itself a design smell — it shouldn't be inferred from convention-following structure.

Design principle: two orthogonal declarations, not one inferred field

Borrowed directly from how a semantic layer like dbt's models this: dbt never lets "which dimensions did the query request" answer "is this measure actually being reduced or not." Every measure independently declares what kind of computation it is (its agg type), and, where relevant, which time dimension it's anchored to (agg_time_dimension) — entirely independent of any query's group_by. Dimensions/group_by are a purely query-time concept, layered on top of the measure, and never feed back into what the measure itself is.

Applied to our IR: whether an aggregation node is performing a reduction at all should be a first-class, explicitly declared property, decided once by whoever constructs the node (which knows unambiguously what semantics it's expressing) — never reconstructed by a downstream consumer from whether a grouping-key list happens to be empty, or from what a neighboring node happens to look like.

Proposed model

Every aggregation-like node should independently declare two things:

  1. Computation kindReduction (collapses its input into fewer output rows, via an explicit grouping) or PerEntity (preserves one output row per input entity/series; there is no grouping question to ask at all).
  2. Grouping — only meaningful under Reduction: which columns to group by, including "group by every dimension" and "group by none, collapse to one row" as distinct, explicit choices. This axis is unaffected by, and must never be used to infer, the computation kind above.

Any downstream consumer (schema derivation, physical-execution decisions, any serialization/merge logic) reads the computation kind directly — it never has to reconstruct it from structural adjacency or from whether some unrelated list happens to be empty.

Why this generalizes across query languages

A language whose surface syntax has no notion of "per-entity, no grouping" (e.g. a purely relational language where every row is already a complete, self-describing record) simply never constructs the PerEntity variant — the same way it already doesn't reach for other languages' vocabulary it has no use for; no special-casing needed. A language that does have this notion (some entity-scoped windowed computation) declares it explicitly at the one point where it's unambiguous which one is meant, instead of leaving it to be reconstructed later from a coincidence of emptiness.

Alternatives considered

  • Reuse the existing "group by every dimension" mechanism to express "per-entity" semantics, since both avoid merging distinct entities together. Rejected: "group by every dimension" is still fundamentally a reduction — it collapses each entity's own rows into one summary row, losing internal row structure (e.g. a time axis) in the process; a true per-entity pass-through must preserve that structure. The two only agree on "don't merge across entities" — they disagree on what happens within one entity's own rows. Treating them as the same mechanism would produce a semantically wrong result.
  • Leave the decision as an inferred property (status quo): works today only because every current construction path happens to follow certain structural conventions. Nothing in the type system protects those conventions going forward — a future rewrite/optimization pass could silently invalidate the inference and produce a wrong result with no error.

Non-goals

  • Not proposing a second aggregation node type — one node, carrying two explicit, orthogonal properties instead of one inferred one.
  • Not proposing to unify this with other entity-scoped computation mechanisms (e.g. analytic/windowed functions) — a related but separable, larger question worth its own proposal if pursued.

Open questions

  • The exact shape/naming of the "computation kind" property, and how it should be validated at construction/lowering time.
  • Whether "computation kind" belongs on the aggregation node itself, or one level up, as a property of how a query fragment enters the aggregation — this may affect how cleanly it composes under nested aggregation.

References

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions