Skip to main content

Temporal aggregation grouping

Group a temporal aggregation by calendar month, by hour of the day, by unit, or by the value another attribute had at the time. aggrGroupBy=unitCode,observedAt;month gives the average speed per calendar month, split by the unit each reading carried. Off by default.

At a glance​

Turning it on​

[extensions.temporal_aggregation_grouping]
mode = "enabled"
max_groups = 10000

extensions.temporal_aggregation_grouping.max_groups (default 10,000) is the most distinct groups one grouped aggregation may produce. A request that would produce more gets 403 TooManyResults, and nothing is returned.

Using it​

GET /ngsi-ld/v1/temporal/entities?type=Vehicle&timerel=after&timeAt=2026-01-01T00:00:00Z&format=groupedValues&aggrMethods=avg&aggrGroupBy=unitCode,observedAt;month
{
"id": "urn:ngsi-ld:Vehicle:1",
"type": "Vehicle",
"speed": {
"type": "Property",
"avg": {
"KMH": {
"2026-01-01T00:00:00Z": [
[
54.2,
"2026-01-01T00:00:00Z",
"2026-02-01T00:00:00Z"
]
]
},
"HM": {
"2026-01-01T00:00:00Z": [
[
31.0,
"2026-01-01T00:00:00Z",
"2026-02-01T00:00:00Z"
]
]
}
}
}
}

The extension adds two things to GET /temporal/entities, GET /temporal/entities/{id} and POST /temporal/entityOperations/query (as aggrParams.aggrGroupBy there):

ParameterValue
format or optionsgroupedValues, the grouped version of aggregatedValues
aggrGroupByone or more grouping terms, described below

aggrGroupBy needs groupedValues and at least one aggrMethods value. The shape of the response depends on the format alone.

Writing aggrGroupBy​

A term is a key, optionally followed by ; and a bucket. Terms are separated by ,, and each one nests inside the groups of the one before it.

The key can be:

  • an attribute path, written as in q or orderBy, such as zone, address[city] or refDevice.category
  • datasetId or unitCode, which belong to each temporal instance
  • id, type or scope, which are allowed but give one group per entity, since they don't change within one entity's history

A bucket goes on a temporal key such as observedAt:

  • year, month, week, day or hour cut the time into calendar periods.
  • dayOfWeek, hourOfDay or monthOfYear fold it into cycles, so observedAt;hourOfDay puts every reading between 08:00 and 09:00 into one group, whatever the day.

The full grammar:

AggregationGrouping = GroupingTerm *("," GroupingTerm)
GroupingTerm = GroupKey *1(";" BucketTerm)
GroupKey = ValuePath / EntityMember
EntityMember = "id" / "type" / "scope" / "datasetId" / "unitCode"
BucketTerm = "year" / "month" / "week" / "day" / "hour" ; calendar (contiguous)
/ "dayOfWeek" / "hourOfDay" / "monthOfYear" ; cyclic (non-contiguous)

How grouping works​

aggrPeriodDuration still comes first. The history is cut into periods, and each period is then grouped by aggrGroupBy. Without a period, the whole range is one period. The order is: attribute and datasetId, then each aggrGroupBy term, then the period.

An attribute path groups each instant by the value that attribute had in effect at that instant: its latest instance not after the one being aggregated, on the same time axis. That is how you get "the average speed by the zone the vehicle was in".

Grouping by unitCode makes a mixed-unit period answerable. A plain aggregation refuses avg over kilograms and pounds (see Temporal aggregation), but with one group per unit, each group holds one unit and the period is split instead.

The response​

groupedValues keeps the array of entities with each entity's id and type. Between each method member and its [value, start, end] triples, it adds one map per grouping term, keyed by the group's value. In the example above, unitCode is the outer map and observedAt;month the inner one. A calendar bucket is keyed by its first instant, such as 2026-01-01T00:00:00Z for January. An absent key is @none, and an explicit NGSI-LD Null is urn:ngsi-ld:null. Groups are ordered as orderBy orders values, under the query's collation.

Paging​

A grouped answer is paged like an aggregated one: by whole periods, at most temporal.query.max_last_n periods and temporal.query.max_aggregation_instances instances per attribute and page, with every group of a period on the same page. Without a period, the whole range is one period and isn't paged. The group limit counts the groups on one page.

What gets refused​

  • 400 InvalidRequest for aggrGroupBy or groupedValues with the extension off

    Turn it on, or use aggregatedValues.

  • 400 InvalidRequest for aggrGroupBy without format=groupedValues

    Ask for groupedValues, or leave aggrGroupBy out.

  • 400 InvalidRequest for aggrGroupBy together with lastN

    lastN limits the instances of each attribute, and grouping then cuts them along another axis, so the two can't be combined. Narrow the time window instead.

  • 400 InvalidRequest for a grouping key that names an attribute listed in attrs

    Grouping an attribute by its own value is ambiguous. Group by another attribute, or by a bucket of observedAt.

  • 403 TooManyResults when a request would produce more than extensions.temporal_aggregation_grouping.max_groups groups

    Narrow the window, the entities or the keys, or raise the limit.

Context sources​

Only the broker that receives the request groups. The temporal query goes to the registered sources as a plain aggregated query (groupedValues becomes aggregatedValues), their answers are merged, and Andromeda groups and aggregates the result. aggrGroupBy is never forwarded.

Why it is an extension​

The specification fixes the group of a temporal aggregation at entity, attribute, datasetId and a fixed-width period, and lets you change none of it. Three things follow:

  • aggrPeriodDuration tiles the window from timeAt. P1M gives month-long periods starting on the day of timeAt, so they line up with calendar months only when timeAt is the first of a month. And there's no way to ask for "every reading between 08:00 and 09:00, across all days".
  • unitCode sits on the instance, so one Property can hold values in different units, and a period that mixes them can't be averaged.
  • Grouping by the value another attribute had at each instant can't be expressed at all.

The grouping grammar is modelled on the entity ordering language, and attribute paths are the same paths q uses.