Shape / table · All operations
Group streaming rows into time windows
A variant of Calculate across rows
Assign rows of a stream to fixed, sliding or session windows of event time, so later steps emit one result per window.
Status
- In arcform
Runs as a SQL step
arcform carries it as a
sql:step today.
The DuckDB step
Assign events to fixed, sliding and session windows of event time, closing them on a watermark carried across two runs
-- events in arrival order; the one that arrives fifth, with event time 00:05, is late
CREATE TEMP TABLE events AS SELECT * FROM (VALUES
(1, TIMESTAMP '2026-03-01 00:01:00'), (2, TIMESTAMP '2026-03-01 00:04:00'), (3, TIMESTAMP '2026-03-01 00:08:00'),
(4, TIMESTAMP '2026-03-01 00:12:00'), (5, TIMESTAMP '2026-03-01 00:05:00'), (6, TIMESTAMP '2026-03-01 00:11:00'),
(7, TIMESTAMP '2026-03-01 00:30:00'), (8, TIMESTAMP '2026-03-01 00:34:00')
) t(arrival, ts);
-- the watermark before an event is the latest event time already seen, less 5 minutes of allowed lateness;
-- an event behind it is late and is left out of every window
CREATE TEMP TABLE on_time AS
SELECT arrival, ts FROM (
SELECT *, max(ts) OVER (ORDER BY arrival ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) - INTERVAL 5 MINUTE AS watermark FROM events)
WHERE watermark IS NULL OR ts >= watermark;
-- fixed (tumbling) windows of 10 minutes
CREATE TEMP TABLE fixed AS
SELECT time_bucket(INTERVAL 10 MINUTE, ts) AS window_start, count(*) AS events FROM on_time GROUP BY ALL;
-- sliding windows of 10 minutes every 5: an event belongs to the two windows that cover it
CREATE TEMP TABLE sliding AS
SELECT time_bucket(INTERVAL 5 MINUTE, ts) - k * INTERVAL 5 MINUTE AS window_start, count(*) AS events
FROM on_time, range(2) r(k) GROUP BY ALL;
-- session windows: a gap of more than 5 minutes between events closes the session
CREATE TEMP TABLE sessions AS
SELECT min(ts) AS window_start, max(ts) AS window_end, count(*) AS events
FROM (SELECT *, sum(starts_session) OVER (ORDER BY ts) AS session_id
FROM (SELECT *, (lag(ts) OVER (ORDER BY ts) IS NULL OR ts - lag(ts) OVER (ORDER BY ts) > INTERVAL 5 MINUTE)::INT AS starts_session FROM on_time))
GROUP BY session_id;
-- two runs over one stream. State kept between them: the events in windows still open, and the watermark.
-- A window is emitted by the first run whose watermark has reached its end; an arrival behind the stored watermark is dropped.
CREATE TEMP TABLE state_events (ts TIMESTAMP);
CREATE TEMP TABLE state_watermark (watermark TIMESTAMP);
CREATE TEMP TABLE emitted (run INTEGER, window_start TIMESTAMP, events BIGINT);
CREATE TEMP TABLE run1_arrivals AS SELECT * FROM (VALUES
(TIMESTAMP '2026-03-01 00:01:00'), (TIMESTAMP '2026-03-01 00:04:00'), (TIMESTAMP '2026-03-01 00:08:00'),
(TIMESTAMP '2026-03-01 00:12:00'), (TIMESTAMP '2026-03-01 00:17:00')) t(ts);
CREATE TEMP TABLE run2_arrivals AS SELECT * FROM (VALUES
(TIMESTAMP '2026-03-01 00:05:00'), (TIMESTAMP '2026-03-01 00:11:00'), (TIMESTAMP '2026-03-01 00:13:00'),
(TIMESTAMP '2026-03-01 00:30:00'), (TIMESTAMP '2026-03-01 00:34:00')) t(ts);
-- run 1: nothing is stored yet, so nothing is late
INSERT INTO state_events SELECT ts FROM run1_arrivals;
INSERT INTO state_watermark SELECT max(ts) - INTERVAL 5 MINUTE FROM state_events;
INSERT INTO emitted
SELECT 1, time_bucket(INTERVAL 10 MINUTE, ts), count(*) FROM state_events
WHERE time_bucket(INTERVAL 10 MINUTE, ts) + INTERVAL 10 MINUTE <= (SELECT watermark FROM state_watermark) GROUP BY ALL;
DELETE FROM state_events WHERE time_bucket(INTERVAL 10 MINUTE, ts) + INTERVAL 10 MINUTE <= (SELECT watermark FROM state_watermark);
CREATE TEMP TABLE open_after_run1 AS SELECT ts FROM state_events;
-- run 2: read the stored watermark, drop the arrivals behind it, then move the watermark on and emit what has closed
CREATE TEMP TABLE run2_late AS SELECT ts FROM run2_arrivals WHERE ts < (SELECT watermark FROM state_watermark);
INSERT INTO state_events SELECT ts FROM run2_arrivals WHERE ts >= (SELECT watermark FROM state_watermark);
UPDATE state_watermark SET watermark = greatest(watermark, (SELECT max(ts) FROM state_events) - INTERVAL 5 MINUTE);
INSERT INTO emitted
SELECT 2, time_bucket(INTERVAL 10 MINUTE, ts), count(*) FROM state_events
WHERE time_bucket(INTERVAL 10 MINUTE, ts) + INTERVAL 10 MINUTE <= (SELECT watermark FROM state_watermark) GROUP BY ALL;
DELETE FROM state_events WHERE time_bucket(INTERVAL 10 MINUTE, ts) + INTERVAL 10 MINUTE <= (SELECT watermark FROM state_watermark);
SELECT * FROM fixed ORDER BY window_start;
SELECT * FROM sliding ORDER BY window_start;
SELECT * FROM sessions ORDER BY window_start;
SELECT * FROM emitted ORDER BY run, window_start;
SELECT CASE WHEN ((SELECT count(*) FROM on_time) = 7 AND (SELECT list(arrival ORDER BY arrival) FROM events WHERE arrival NOT IN (SELECT arrival FROM on_time)) = [5] AND (SELECT list((strftime(window_start, '%H:%M'), events) ORDER BY window_start) FROM fixed) = [('00:00', 3), ('00:10', 2), ('00:30', 2)] AND (SELECT list((strftime(window_start, '%H:%M'), events) ORDER BY window_start) FROM sliding) = [('23:55', 2), ('00:00', 3), ('00:05', 3), ('00:10', 2), ('00:25', 2), ('00:30', 2)] AND (SELECT list((strftime(window_start, '%H:%M'), strftime(window_end, '%H:%M'), events) ORDER BY window_start) FROM sessions) = [('00:01', '00:12', 5), ('00:30', '00:34', 2)] AND (SELECT list((run, strftime(window_start, '%H:%M'), events) ORDER BY run, window_start) FROM emitted) = [(1, '00:00', 3), (2, '00:10', 3)] AND (SELECT list(strftime(ts, '%H:%M') ORDER BY ts) FROM open_after_run1) = ['00:12', '00:17'] AND (SELECT list(strftime(ts, '%H:%M') ORDER BY ts) FROM run2_late) = ['00:05', '00:11'] AND (SELECT strftime(watermark, '%H:%M') FROM state_watermark) = '00:29' AND (SELECT list(strftime(ts, '%H:%M') ORDER BY ts) FROM state_events) = ['00:30', '00:34']) THEN 'as expected' ELSE error('the fixed, sliding and session windows of the on-time events were not the expected counts, or the late event was not left out, or the two runs did not emit window 00:00 then window 00:10 with the watermark carried over') END AS verdict;- The statement builds its own data.
- It needs nothing installed beyond DuckDB itself.
- The statement checks its own result and raises an error when it is wrong: “the fixed, sliding and session windows of the on-time events were not the expected counts, or the late event was not left out, or the two runs did not emit window 00:00 then window 00:10 with the watermark carried over”.
- It passed on DuckDB
v2.0.0 (Cyanoptera) d54cbbe3df.
What other tools call it
Meridian's classification; an independent blind rating agreed on 95.7% of 324 rows (variant, version 3).
Apache Hop (2)
Beam timestampread 2026-09-27Assign an event-time timestamp to each row of a streaming Beam pipeline
Beam windowread 2026-09-27Group streaming rows into fixed, sliding, session or global event-time windows on Beam
Duckle (1)
Tumbling Windowread 2026-09-25event-time tumbling windows held across runs until a watermark closes them
Talend (1)
tWindowread 2026-09-27Apply a Spark window to the incoming data and pass the windowed data on
Background
Windowing slices up a dataset into finite chunks for processing as a group, which unbounded data needs before most forms of grouping such as aggregation.[1] In Apache Flink, windows split an infinite stream into buckets of finite size, over which computations can be applied.[2] In Apache Beam, a watermark is the system's notion of when all data in a certain window can be expected to have arrived.[3]
When to use it
With unbounded data, windowing is required for aggregation, outer joins and time-bounded operations, and unnecessary for filtering, mapping and inner joins.[1] For bounded data, windowing is essentially optional, though still a semantically useful concept in many situations.[1] Unbounded datasets have been processed using repeated runs of batch systems since their conception.[1]
Event time is the time at which an event actually occurred, and processing time is the time at which it is observed at a given point during processing.[1] In Apache Beam, there are no guarantees that data events appear in the pipeline in the same order that they were generated.[3] In Apache Beam, once the watermark passes the end of a window, any further element with a timestamp in that window is late data.[3] In Apache Flink, late elements are dropped by default when the watermark is past the end of the window, as the allowed lateness defaults to 0.[2]
Apache Beam's default windowing assigns all elements of a collection to a single global window and discards late data, even for unbounded collections.[3]
The method
Fixed windows, sometimes called tumbling windows, are defined by a static window size, such as hourly or daily windows.[1] Sliding windows are defined by a window size and a slide period, and the windows may overlap when the period is less than the size.[1] Fixed windows are a special case of sliding windows where the size equals the period.[1] Sessions are windows that capture a period of activity over a subset of the data, typically defined by a timeout gap.[1]
In Apache Flink, a sliding window assigner places an element in several windows when the slide is smaller than the window size.[2] In Apache Flink, time-based windows have an inclusive start timestamp and an exclusive end timestamp.[2] In Apache Flink, a window is created when its first element arrives and removed once time passes its end timestamp plus the allowed lateness.[2]
The term Dataflow Model describes the processing model of Google Cloud Dataflow, which is based on technology from FlumeJava and MillWheel.[1]
Compared with the alternatives
In Apache Beam, GroupByKey groups the elements of a collection by key and window.[3] In PostgreSQL, unlike non-window aggregates, window functions do not group rows into a single output row, and each row retains its separate identity.[4] Use stream time windows when the result wanted is one figure per stretch of time as events arrive, and a SQL window function when every row should keep an answer of its own. (Meridian's view)
Sources
- [1]The Dataflow Model: A Practical Approach to Balancing Correctness, Latency, and Cost in Massive-Scale, Unbounded, Out-of-Order Data Processingsecondary source
- [2]Windows | Apache Flinkdocumentation
- [3]Beam Programming Guidedocumentation
- [4]PostgreSQL: Documentation: 18: 3.5. Window Functionsdocumentation