Approach (brief)- Ingest hourly event batches into a staging table, dedupe by a unique event_id, then compute incremental aggregates (e.g., DAU, revenue) grouped by day. Use MERGE/UPSERT into a daily_aggregates table keyed by date + metric. Ensure idempotency by dedup keys and writing aggregates computed only from staging after dedupe. Minimize recomputation by aggregating only the new/changed events (using batch_id or event_timestamp watermark).Schema (suggested)- raw_events(stage): - event_id PK (string) - user_id - event_type - revenue (numeric) - event_ts (timestamp) - batch_id (string) -- hourly batch identifier - received_at- daily_aggregates: - event_date (date) -- PK part 1 - metric (varchar) -- 'dau'|'revenue' PK part 2 - value (numeric) - last_updated_at (timestamp)- processed_batches: - batch_id PK - processed_atPattern (steps)1) Load staging (raw_events) append-only for each hour.2) Deduplicate staging for that batch:sql
CREATE TABLE deduped_events AS
SELECT DISTINCT ON (event_id) *
FROM raw_events
WHERE batch_id = :batch_id
ORDER BY event_id, received_at DESC;
3) Compute per-day incremental aggregates from deduped_events:sql
WITH per_day AS (
SELECT
CAST(event_ts AS date) AS event_date,
COUNT(DISTINCT user_id) FILTER (WHERE event_type IS NOT NULL) AS dau,
SUM(revenue) AS revenue
FROM deduped_events
GROUP BY CAST(event_ts AS date)
)
SELECT * FROM per_day;
4) Upsert into daily_aggregates using MERGE (idempotent):sql
MERGE INTO daily_aggregates t
USING (
SELECT event_date, 'dau' AS metric, dau::numeric AS value FROM per_day
UNION ALL
SELECT event_date, 'revenue' AS metric, revenue::numeric FROM per_day
) s
ON t.event_date = s.event_date AND t.metric = s.metric
WHEN MATCHED THEN
UPDATE SET value = t.value + s.value, last_updated_at = CURRENT_TIMESTAMP
WHEN NOT MATCHED THEN
INSERT (event_date, metric, value, last_updated_at)
VALUES (s.event_date, s.metric, s.value, CURRENT_TIMESTAMP);
5) Record batch processed:sql
INSERT INTO processed_batches(batch_id, processed_at) VALUES (:batch_id, now());
Idempotency & Minimal recomputation- Dedupe by event_id so reprocessing same batch doesn't double-count.- Use idempotent batch_id tracking: skip a batch if processed_batches contains batch_id (or allow re-run by deleting its previous effect first).- To support re-run that corrects prior bad data: compute per_day for the batch and either (a) subtract previous batch's contribution before adding new (store batch-level aggregates), or (b) maintain a batch_aggregates table and MERGE by replacing batch contribution per (event_date, metric) then recompute daily sum as SUM over batch_aggregates.Example pattern (batch_aggregates + recompute):sql
-- replace batch contribution
MERGE INTO batch_aggregates b
USING (SELECT event_date, 'dau' metric, dau value FROM per_day) s
ON b.batch_id = :batch_id AND b.event_date = s.event_date AND b.metric = s.metric
WHEN MATCHED THEN UPDATE SET value = s.value
WHEN NOT MATCHED THEN INSERT (batch_id, event_date, metric, value) VALUES (:batch_id, s.event_date, s.metric, s.value);
-- recompute daily from batch_aggregates and upsert to daily_aggregates (replace value)
MERGE INTO daily_aggregates t
USING (
SELECT event_date, metric, SUM(value) AS value FROM batch_aggregates GROUP BY event_date, metric
) s
ON t.event_date = s.event_date AND t.metric = s.metric
WHEN MATCHED THEN UPDATE SET value = s.value, last_updated_at = now()
WHEN NOT MATCHED THEN INSERT (...) VALUES (...);
Trade-offs / notes- Simple additive MERGE is cheaper but harder to correct retroactively. Maintaining batch_aggregates gives full idempotent re-run and easier corrections at cost of storage and recompute per affected date ranges only.- Partition daily_aggregates by event_date for faster replaces.- Keep retention / housekeeping for batch_aggregates to control storage.- Monitor late-arriving events and decide policy (accept and reprocess affected dates).Edge cases- Late-arriving events crossing date boundaries (use event_ts timezone normalization).- Duplicates across batches — ensure global dedupe by event_id.- Large backfills — run partition replacement or full recompute for the affected date range.