Approach: use a window function to find events for the same user and event_type that occur within 1 second of the previous (or next) event; mark both as likely duplicates. Then show aggregation-time dedupe (choose earliest per 1s window) and propose ingestion-time strategies and performance considerations.SQL (marks pairs where gap <= 1 second):sql
WITH ordered AS (
SELECT
*,
LAG(occurred_at) OVER (PARTITION BY user_id, event_type ORDER BY occurred_at, ingestion_time) AS prev_occurred_at,
LAG(event_id) OVER (PARTITION BY user_id, event_type ORDER BY occurred_at, ingestion_time) AS prev_event_id
FROM raw_events
)
SELECT
event_id,
user_id,
event_type,
occurred_at,
ingestion_time,
CASE
WHEN prev_occurred_at IS NOT NULL
AND EXTRACT(EPOCH FROM (occurred_at - prev_occurred_at)) <= 1
THEN TRUE ELSE FALSE
END AS likely_duplicate_of_prev,
prev_event_id
FROM ordered
WHERE
-- optional: restrict to recent timeframe for performance
occurred_at >= CURRENT_DATE - INTERVAL '7 days';
Aggregation-time dedupe (keep earliest event per user/event_type in 1-second buckets):sql
WITH ranked AS (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY user_id, event_type, FLOOR(EXTRACT(EPOCH FROM occurred_at)/1)
ORDER BY occurred_at, ingestion_time, event_id
) AS rn
FROM raw_events
)
SELECT * EXCEPT(rn)
FROM ranked
WHERE rn = 1;
Why this works:- Partitioning by user_id and event_type groups comparable events.- LAG finds near neighbours; the FLOOR(EXTRACT(EPOCH)/1) trick buckets timestamps into 1-second intervals for deterministic aggregation.Edge cases & correctness:- Out-of-order ingestion: use ingestion_time tie-breaker; consider event_time watermarking.- Clock skew: apply allowable skew or use ingestion_time if event timestamps unreliable.- Boundary handling: decide inclusive/exclusive for exactly 1.0s.Ingestion-time deduplication strategies:- Upstream de-dup service (stream processor like Flink/Beam/Kafka Streams) with state keyed by (user_id,event_type) and TTL ~ few seconds: drop events whose timestamps fall within window of a retained event.- Use an idempotent write store: compute dedupe key (user_id||event_type||floor(ts/1s)) and perform upsert into a dedupe table with conditional insert (INSERT...ON CONFLICT DO NOTHING).- Bloom filter + cache for fast approximate filtering when high cardinality (accept some false positives).- Use event_id if globally unique — maintain recent event_id cache (Redis) with TTL.Aggregation-time dedupe strategies:- Run the SQL above at query-time or materialize a cleaned events table daily.- Use approximate dedupe during streaming ETL then finalize in batch with stronger logic.- Implement retention of canonical event (earliest ingestion or highest-confidence).Performance considerations for large volumes:- Partition by date (occurred_at date) and cluster/index by user_id + event_type to limit scan scope.- Limit windowing to per-partition workloads; avoid full-table window functions where possible.- For streaming dedupe, state size grows with active keys — memory and TTL must be tuned; use RocksDB state backend or external key-value store.- Use micro-batching to amortize overhead; backpressure controls.- For very high cardinality users, sharding and key-salt to distribute load, at cost of complicating grouping.- Consider approximate methods (Bloom filters, sampling) if exactness is not required.- Monitor latency, state size, and false-positive/negative rates; add metrics and alerts.Recommended pragmatic flow:1) Do lightweight ingestion dedupe (keyed, TTLed) to remove obvious duplicates.2) Keep raw events immutable but tag potential duplicates.3) Run stronger batch dedupe during aggregation when writing canonical dataset used for analytics.