Skip to content

Commit 391e1c2

Browse files
committed
docs: exhaustively document the multi-stage grain directive
Add a concise `grain` reference section (keep_only / exclude / include) to the measures reference, covering the parameter spec, the legacy-alias mapping, and the either/or rule against the legacy parameters. Document grain as the canonical form of group_by / reduce_by / add_group_by (which map to keep_only / exclude / include), and that a measure uses either a `grain` block or the legacy parameters, never both — per the Tesseract planner's build_grain_from_legacy / from_measure_ definition, a `grain` block causes the legacy directives to be ignored. Keep the deep dive out of the reference: the per-key mechanics and worked result tables (verified against the multi-stage-grain integration test) live in the conceptual "Controlling grain" section of the measures guide. Add a new "Semi-additive (end-of-period) measures" recipe demonstrating grain (include + keep_only) and rank for end-of-period balances, and register it in the docs navigation.
1 parent 0e89fb0 commit 391e1c2

4 files changed

Lines changed: 506 additions & 44 deletions

File tree

docs-mintlify/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@
620620
"recipes/data-modeling/nested-aggregates",
621621
"recipes/data-modeling/filtered-aggregates",
622622
"recipes/data-modeling/share-of-total",
623+
"recipes/data-modeling/semi-additive-measures",
623624
"recipes/data-modeling/period-over-period",
624625
"recipes/data-modeling/passing-dynamic-parameters-in-a-query",
625626
"recipes/data-modeling/using-dynamic-measures",

docs-mintlify/docs/data-modeling/measures.mdx

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -412,13 +412,129 @@ measures:
412412
type: rank
413413
```
414414

415-
<Note>
415+
### Controlling grain
416416

417-
`grain` replaces the standalone `group_by`, `reduce_by`, and `add_group_by`
418-
parameters, which remain supported. See the [`grain`][ref-grain] reference for
419-
the migration mapping.
417+
[`group_by`][ref-group-by], [`reduce_by`][ref-reduce-by], and
418+
[`add_group_by`][ref-add-group-by] each adjust the grain of a multi-stage
419+
measure's inner aggregation in one direction. The [`grain`][ref-grain] parameter
420+
is a unified alternative that expresses all of them — and their combinations —
421+
through three composable keys: `keep_only`, `exclude`, and `include`.
420422

421-
</Note>
423+
A multi-stage measure is computed in two stages: an **inner** aggregation at some
424+
grain, then an **outer** aggregation that rolls the result up to the query's grain.
425+
`grain` sets that inner grain, so the measure can take a value at a different grain
426+
than the row it is returned on.
427+
428+
The examples below use an `orders` cube with `status`, `category`, and `id`
429+
dimensions and a `total_amount` (`sum`) base measure.
430+
431+
#### `keep_only`
432+
433+
Restricts the measure's grain to only the listed dimensions that are present in the
434+
query; the value repeats across every other query dimension. This is the building
435+
block for "share within a group" — divide the base measure by a `keep_only` measure
436+
to get each row's share of its group total.
437+
438+
```yaml
439+
measures:
440+
- name: amount_by_status
441+
multi_stage: true
442+
sql: "{total_amount}"
443+
type: sum
444+
grain:
445+
keep_only:
446+
- orders.status
447+
```
448+
449+
Querying both `status` and `category`, the measure returns the per-status total on
450+
every row, regardless of `category`:
451+
452+
| `status` | `category` | `total_amount` | `amount_by_status` |
453+
|---|---|---:|---:|
454+
| completed | books | 170 | **370** |
455+
| completed | electronics | 200 | **370** |
456+
| pending | books | 50 | **125** |
457+
| pending | electronics | 75 | **125** |
458+
| cancelled | books | 30 | **30** |
459+
460+
If none of the listed dimensions are present in the query — for example, grouping
461+
only by `category` so `status` is absent — the grain becomes empty and the measure
462+
collapses to the grand total:
463+
464+
| `category` | `total_amount` | `amount_by_status` |
465+
|---|---:|---:|
466+
| books | 250 | **525** |
467+
| electronics | 275 | **525** |
468+
469+
#### `exclude`
470+
471+
Removes the listed dimensions from the grain. The measure is computed as if those
472+
dimensions were not in the query, so the same value repeats across them. `exclude`
473+
is the inverse framing of `keep_only` — convenient when you want to drop one
474+
dimension while keeping the rest.
475+
476+
```yaml
477+
measures:
478+
- name: amount_excl_status
479+
multi_stage: true
480+
sql: "{total_amount}"
481+
type: sum
482+
grain:
483+
exclude:
484+
- orders.status
485+
```
486+
487+
Querying both `status` and `category`, excluding `status` from the grain leaves
488+
`category`, so each row gets the per-category total:
489+
490+
| `status` | `category` | `total_amount` | `amount_excl_status` |
491+
|---|---|---:|---:|
492+
| completed | books | 170 | **250** |
493+
| pending | books | 50 | **250** |
494+
| cancelled | books | 30 | **250** |
495+
| completed | electronics | 200 | **275** |
496+
| pending | electronics | 75 | **275** |
497+
498+
`exclude` accepts multiple members — for example `exclude: [status, id]` drops both
499+
from the grain, leaving only `category`.
500+
501+
#### `include`
502+
503+
Adds dimensions to the inner (leaf) grain that are not in the query; the outer stage
504+
then re-aggregates back to the query's grain. Use this to force a finer intermediate
505+
breakdown before the outer aggregation.
506+
507+
```yaml
508+
measures:
509+
- name: amount_incl_id
510+
multi_stage: true
511+
sql: "{total_amount}"
512+
type: sum
513+
grain:
514+
include:
515+
- orders.id
516+
```
517+
518+
When you query by `category`, the inner stage groups by `(category, id)` and the
519+
outer stage sums back over `id` to the per-category total.
520+
521+
`keep_only` and `include` can be combined: `keep_only` narrows the parent grain,
522+
then `include` extends the leaf. For example, `keep_only: [status]` with
523+
`include: [id]`, queried by `status` and `category`, narrows the parent grain to
524+
`status` and adds `id` to the leaf — the outer stage re-aggregates by
525+
`(status, category)`, broadcasting each per-status total across its categories:
526+
527+
| `status` | `category` | `total_amount` | `amount_by_status_incl_id` |
528+
|---|---|---:|---:|
529+
| completed | books | 170 | **370** |
530+
| completed | electronics | 200 | **370** |
531+
| pending | books | 50 | **125** |
532+
| pending | electronics | 75 | **125** |
533+
| cancelled | books | 30 | **30** |
534+
535+
See the [`grain` reference][ref-grain] for the parameter spec, and the
536+
[semi-additive measures recipe][ref-semi-additive-recipe] for a worked
537+
end-of-period example.
422538

423539
### Conditional measures
424540

@@ -475,7 +591,11 @@ measures:
475591
[ref-format]: /reference/data-modeling/measures#format
476592
[ref-rolling-window]: /reference/data-modeling/measures#rolling_window
477593
[ref-time-shift]: /reference/data-modeling/measures#time_shift
594+
[ref-group-by]: /reference/data-modeling/measures#group_by-reduce_by-and-add_group_by-legacy
595+
[ref-reduce-by]: /reference/data-modeling/measures#group_by-reduce_by-and-add_group_by-legacy
596+
[ref-add-group-by]: /reference/data-modeling/measures#group_by-reduce_by-and-add_group_by-legacy
478597
[ref-grain]: /reference/data-modeling/measures#grain
598+
[ref-semi-additive-recipe]: /recipes/data-modeling/semi-additive-measures
479599
[ref-filter]: /reference/data-modeling/measures#filter
480600
[ref-case]: /reference/data-modeling/measures#case
481601
[ref-switch-dim]: /reference/data-modeling/dimensions#type

0 commit comments

Comments
 (0)