Groups & windows — many rows into one
The question for this lesson: every node so far has taken one row in and emitted one row out. But often you want a summary: total sales, a count per status, clicks per hour. That means turning many rows into one. How do you do that?
You use an aggregate node, and you tell it how to group the rows.
What you’ll be able to do
Section titled “What you’ll be able to do”- Use an
aggregatenode withgroup_byto collapse many rows into a summary. - Pick the right grouping: one summary overall, or one row per group.
- Bucket time-stamped rows into a tumbling time window.
An aggregate collapses rows into a summary
Section titled “An aggregate collapses rows into a summary”A transform reshapes each row on its own. An aggregate
is different: it gathers many rows into a group and emits one row per group, using
aggregate functions like count(*), sum(...), avg(...), min(...), and max(...).
The group_by setting decides what a “group” is:
group_by: []: an empty list means one group: everything. You get a single summary row.group_by: [status]: one group per distinctstatus. You get one row for each.
- type: aggregate name: summary input: customers config: group_by: [] # one summary over all rows cxl: | emit customers = count(*) # how many rows came in emit total_value = sum(lifetime_value.to_int())🌱 New here? — count(*) and sum(...)
count(*) counts the rows in the group. sum(lifetime_value.to_int()) adds up a number
across the group, with .to_int() first because lifetime_value is a declared string (the same
text-to-number step from lesson 02). avg, min, and max work the same way.
Use: one summary over everything
Section titled “Use: one summary over everything”This aggregate has group_by: [], so it folds all the customers into a single row. Predict
the two numbers, then run.
> output appears here — predict, then run
One row out, no matter how many rows came in: that’s what group_by: [] means.
customers is count(*) = 3; total_value is the sum, 80000 + 1200 + 4000 = 85200.
Modify: one row per status
Section titled “Modify: one row per status”Now group instead of folding everything together. In the runner above, change the aggregate
to group by status and carry that column through:
group_by: [status] # one group per distinct statuscxl: | emit status = status # the grouping key, kept in the output emit customers = count(*) emit total_value = sum(lifetime_value.to_int())Create: a richer per-group summary
Section titled “Create: a richer per-group summary”Now author one from scratch. The question: per status, how many customers, and what is their
average lifetime value? Start from the per-status aggregate above and swap the total for an
average (avg is built in, like sum).
- type: aggregate name: by_status input: customers config: group_by: [status] cxl: | emit status = status # keep the grouping key emit customers = count(*) emit avg_value = avg(lifetime_value.to_int()) # avg, not sumOver the same three customers, (active, 80000), (inactive, 1200), (active, 4000), that gives:
status,customers,avg_valueactive,2,42000inactive,1,1200active averages (80000 + 4000) / 2 = 42000; inactive has just the 1200. Check your own
with clinker run your_pipeline.yaml --dry-run (config + types) and --explain (the plan)
before you run it for the numbers.
Grouping by time: the tumbling window
Section titled “Grouping by time: the tumbling window”Sometimes the group you want is a slice of time: clicks per hour, sales per day. When
your rows carry a timestamp, add a time_window to the aggregate. A tumbling window
chops time into back-to-back, non-overlapping buckets (every hour, on the hour) and makes
each bucket a group.
Here is Clinker’s real tumbling_clicks example, per-user clicks counted per hour:
- type: source name: clicks config: name: clicks type: csv path: ./data/clicks.csv options: { has_header: true } watermark: column: event_ts # which column carries event time schema: - { name: user_id, type: string } - { name: event_ts, type: date_time } - { name: kind, type: string }
- type: aggregate name: hourly_clicks input: clicks config: group_by: [user_id] # group by user AND by the time bucket below time_window: tumbling: { size: 1h } # one bucket per hour cxl: | emit user_id = user_id emit n = count(*)A user who clicks five times between 09:00 and 10:00 and twice between 10:00 and 11:00
produces two output rows: n = 5 for the 09:00 bucket and n = 2 for the 10:00 bucket:
user_id,nu1,5u1,2🌱 New here? — watermark
A time window needs to know which column is the event time, so the source declares a
watermark: { column: event_ts }. Without a watermark on the source, Clinker has no clock to
bucket by, and a time_window won’t work. The column must be a date_time (or date).
Other window shapes (same idea, different slicing, and you’ll reach for these later): hopping windows overlap (e.g. a 1-hour window that advances every 15 minutes, so each row lands in several windows), and session windows group bursts of activity separated by a gap of idleness. Tumbling is the one to start with.