@@ -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