nexyron.temporal_bucket_aggregate
Generic Description
Aggregate one declared temporal property across all subjects, or across a selected subject list, into calendar or fixed-width time buckets without first materializing every temporal point as a Cypher row.
Simple example:
CALL nexyron.temporal_bucket_aggregate(
'User',
'payments',
'month',
'sum',
'amount',
null,
null,
null
)
YIELD bucket_start, bucket_end, value
RETURN bucket_start, bucket_end, value
ORDER BY bucket_start
Arguments:
label: owning node label for the declared temporal property.property: declared temporal property name.bucket:year,month,week,day, or a positive fixed bucket width in milliseconds.aggregate:count,sum,min,max,avg,first, orlast.value_path: optional dot path inside object values, such asamountorpayment.amount; usenullfor scalar temporal values.from_ts: optional inclusive lower bound in epoch milliseconds.to_ts: optional inclusive upper bound in epoch milliseconds.owners: optional list of internal owner node IDs; usenullto aggregate every subject that has the declared label/property stream.
Returned columns:
bucket_start: epoch-millisecond bucket start.bucket_end: exclusive epoch-millisecond bucket end.value: aggregate result for the bucket.sample_count: number of samples included. For numeric aggregates this means numeric samples; forcountthis means records wherevalue_pathexists, or non-null scalar records whenvalue_pathisnull.subject_count: number of distinct owner nodes that contributed to the bucket.
Consumer-Level Explanation
Use this procedure when the business question is naturally phrased as "sum/count/average this temporal metric per year, month, week, or day" and the data is stored as a declared temporal property.
The important difference from nexyron.temporal_history or nexyron.temporal_rollup is scope. temporal_history(node_id, ...) and temporal_rollup(node_id, ...) read one owner at a time. That is useful for one account, one device, or one user. It is not the clean shape for "all users by month" because the caller would have to discover owners, loop over them, and then aggregate outside the temporal store.
nexyron.temporal_bucket_aggregate keeps that work inside the temporal store. The caller names the label, property, bucket, aggregate, and optional value path. If owners is null, Nexyron aggregates all subjects that have that declared temporal property. If owners is supplied, Nexyron aggregates only those subjects.
For business users, this means:
- "monthly payment amount across all users" does not require passing every user ID.
- "weekly payment amount for this segment" can pass only the selected owner IDs.
- missing
from_tsandto_tsmeans "use the stored data range", not "guess from wall-clock now". - the result is already one row per bucket, so normal Cypher math can adjust it.
Conceptual Explanation
This procedure fills the gap between one-subject temporal rollups and general Cypher aggregation.
Normal Cypher aggregation is excellent after a query has already produced rows. One-subject temporal rollups are excellent when a known owner already has materialized buckets. Cross-owner business reporting needs a different shape: the engine should read the temporal store directly, group by the requested time bucket, and only return the small bucket result.
The procedure keeps the row grain explicit:
one returned row = one populated time bucket for one declared temporal property
That grain is useful for reports, charts, feature generation, assistant-authored analytics, and downstream calculations such as dividing every bucket by a denominator from another query.
Calendar buckets are UTC calendar buckets. month means real calendar months such as January and February, not a fixed 30-day duration. week uses ISO-style Monday-start weeks.
More Detailed Explanation
The procedure scans the temporal-property store directly and maintains one compact aggregate state per output bucket. It does not build a list of all matching history rows, and it does not keep per-subject row buffers.
The memory shape is:
bucket_start -> aggregate_state
For a ten-year monthly report this means roughly 120 states, plus a distinct-subject set per populated bucket for subject_count. It does not mean one in-memory object per payment row in the final aggregation layer.
The execution path uses the existing declared temporal-property storage:
- Resolve
label.propertyin the temporal property store. - Iterate only retained subjects for that property, or only subjects in the optional owner filter.
- Use sorted per-subject history vectors to skip records outside
from_tsandto_ts. - Extract the scalar value directly from the temporal value or from
value_path. - Put the timestamp into a calendar bucket (
year,month,week,day) or fixed millisecond bucket. - Update the aggregate state in place.
- Return finalized bucket rows ordered by
bucket_start.
Advanced Example
This example calculates three monthly payment totals and then divides each monthly total by four. The division happens after the bucket aggregate, so the procedure still returns a compact bucket series first.
WITH datetime('2026-01-01T00:00:00Z') AS from_ts,
datetime('2026-04-01T00:00:00Z') AS to_ts
CALL nexyron.temporal_bucket_aggregate(
'User',
'payments',
'month',
'sum',
'amount',
from_ts,
to_ts,
null
)
YIELD bucket_start, bucket_end, value, sample_count, subject_count
RETURN bucket_start,
bucket_end,
value AS monthly_payment_sum,
value / 4.0 AS monthly_payment_sum_divided_by_4,
sample_count,
subject_count
ORDER BY bucket_start
This example applies a denominator from another graph query. The denominator is computed once, then applied to every returned bucket.
MATCH (:Company {id: $company_id})<-[:BELONGS_TO]-(u:User)
WITH collect(id(u)) AS owners
CALL {
MATCH (p:Plan {id: $plan_id})
RETURN coalesce(p.normalization_divisor, 1.0) AS divisor
}
WITH owners, divisor
CALL nexyron.temporal_bucket_aggregate(
'User',
'payments',
'month',
'sum',
'amount',
null,
null,
owners
)
YIELD bucket_start, value, sample_count, subject_count
RETURN bucket_start,
value AS selected_users_monthly_sum,
value / divisor AS normalized_monthly_sum,
sample_count,
subject_count
ORDER BY bucket_start
This example uses scalar temporal values instead of object values. Because the temporal value itself is numeric, value_path is null.
CALL nexyron.temporal_bucket_aggregate(
'Account',
'balance_delta',
'week',
'avg',
null,
datetime('2026-01-01T00:00:00Z'),
datetime('2026-03-01T00:00:00Z'),
null
)
YIELD bucket_start, value, sample_count, subject_count
RETURN bucket_start,
value AS avg_weekly_balance_delta,
sample_count,
subject_count
ORDER BY bucket_start
Real Use Cases
- Revenue and payment reporting: aggregate
User.payments.amountby month across all users without first returning every payment row to Cypher. - Subscription cohorts: match a selected customer segment, pass
collect(id(u))asowners, and return weekly or monthly totals for only that segment. - Account balance monitoring: aggregate scalar balance deltas by day or week to detect cash-flow changes without modeling every delta as a visible graph node.
- Operational event volumes: count numeric event flags or quantities by day across all devices, stores, or accounts that own a declared temporal property.
- Normalized business metrics: compute bucket totals first, then divide each bucket by a fixed value, a plan property, or a scalar produced by another query.
Real Limitations And Tradeoffs
- The procedure currently targets declared temporal properties. It does not aggregate arbitrary relationship timestamp properties, hidden event streams, or graph-visible event nodes unless those facts are also stored as temporal properties.
- For
sum,min,max,avg,first, andlast,value_pathmust point to a numeric scalar. Non-numeric values are skipped for numeric aggregates, so an incorrect path can return empty or undercounted buckets. Forcount, the path only needs to exist. ownersuses internal owner node IDs, usually produced byid(u)in the same query. Passingnullis the intended shape for all-subject aggregation.- Calendar buckets use UTC. If a business needs region-specific wall-clock calendars, that should be added explicitly rather than silently applying local machine time.
subject_countkeeps distinct owner IDs per bucket. That is useful for business coverage reporting, but it is the one part of the result state that grows with the number of contributing subjects per bucket.- Existing fixed-width temporal rollups still serve one-owner rollup reads. This procedure is for cross-owner bucket aggregation and does not replace
nexyron.temporal_rollupwhere the query is about one subject's stored rollup buckets.