A 'summarize ... by' clause looks simple in HelixQL syntax, but resolving it against dynamic labels, computed expressions, and bucketed time takes real planning work.

The `by` clause in a HelixQL `summarize` looks like straightforward SQL `GROUP BY`, but HelixQL's label model — where every series can carry a different set of labels — means group-key resolution has to handle missing labels, computed grouping expressions, and time bucketing all in the same pass.

Static label grouping

The simple case, grouping by a label that's present on every matched series, resolves directly against the token-vocabulary label sets described elsewhere — the group key is just the token ID (or IDs, for multi-label grouping) for that label's value, no string comparison needed at execution time.

The missing-label case

Not every series carries every label — grouping by `pod` when 2% of matched series come from a job that doesn't set a pod label at all needs a defined behavior, not a crash. HelixQL treats a missing grouping label as an explicit `null` group, aggregated separately and clearly labeled in results rather than silently dropped or merged into an arbitrary bucket.

from metrics
| where name == 'queue.depth'
| summarize sum(value) by service, bin(timestamp, 5m)
# series without a 'service' label group into service = null, shown separately

Computed grouping expressions

Grouping by an expression — `by bin(timestamp, 5m)`, or `by strcat(service, '/', env)` — can't use the pre-built token dictionary directly, since the group key doesn't exist until the expression is evaluated. These paths fall back to computing the expression per row (or per batch, under vectorized execution) and hashing the result into a group key on the fly, which costs measurably more than static label grouping — about 3-5x, from our benchmarks — but is still cheap relative to the aggregation itself for typical cardinalities.

Group key cardinality limits

A `summarize ... by` clause grouping on a high-cardinality field (accidentally grouping by `trace_id` instead of `route`, for instance) can produce an enormous result set. The planner estimates result cardinality using the same histogram statistics that drive filter selectivity, and rejects queries whose estimated group count exceeds a configurable ceiling (default 500,000 groups) before execution even starts, rather than letting a query silently consume unbounded memory building group state.

  • Static label grouping resolves directly against pre-built token IDs; missing labels group into an explicit null bucket.
  • Computed grouping expressions (bin(), string concatenation) cost 3-5x more than static label grouping.
  • The planner rejects queries whose estimated group cardinality exceeds 500,000 before execution starts.