Approach: window/aggregation per event_id to keep the row with the latest event_ts; break ties deterministically (e.g., latest ingest_ts, then smallest UUID hash). Partition data so each job touches only relevant partitions (event_date derived from event_ts or a hashed prefix of event_id). Use an idempotent sink (e.g., Delta Lake MERGE or Hive overwrite-by-partition with deterministic keys).python
# pyspark (DataFrame) outline
from pyspark.sql import SparkSession, functions as F, Window
spark = SparkSession.builder.getOrCreate()
raw = spark.read.parquet("s3://bucket/events/") # input partitioned by event_date if available
# canonical fields: event_id, event_ts (timestamp), ingest_ts (timestamp), payload, event_date
# add deterministic tiebreaker: hash of payload or event_id
raw = raw.withColumn("tie_hash", F.abs(F.hash(F.col("event_id"))) )
w = Window.partitionBy("event_id").orderBy(F.col("event_ts").desc(),
F.col("ingest_ts").desc(),
F.col("tie_hash").asc())
deduped = raw.withColumn("rn", F.row_number().over(w)).filter("rn = 1").drop("rn", "tie_hash")
# write idempotently using Delta MERGE to target table keyed by event_id
from delta.tables import DeltaTable
target_path = "s3://bucket/clean/events_delta/"
if not DeltaTable.isDeltaTable(spark, target_path):
deduped.write.format("delta").partitionBy("event_date").mode("overwrite").save(target_path)
else:
delta = DeltaTable.forPath(spark, target_path)
(delta.alias("t")
.merge(deduped.alias("s"), "t.event_id = s.event_id")
.whenMatchedUpdate(condition = "s.event_ts > t.event_ts OR (s.event_ts = t.event_ts AND (s.ingest_ts > t.ingest_ts OR (s.ingest_ts = t.ingest_ts AND s.event_id < t.event_id)))",
set = {c: f"s.{c}" for c in deduped.columns})
.whenNotMatchedInsertAll()
.execute())
Key points:- Use event_date partitioning for pruning; if high-cardinality, add a hash-prefix partition (e.g., first 2 hex chars of md5(event_id)) to bound file sizes.- Avoid full shuffle where possible: pre-partition/repartition by event_id or partition+bucket to localize work.- Use row_number() over partition by event_id to handle out-of-order arrivals deterministically.- MERGE (Delta/iceberg) ensures idempotency: reprocessing same input will converge.Performance considerations:- At petabyte scale, avoid tiny files: coalesce writes per partition, use large files (256MB+), tune shuffle partitions.- Use map-side aggregations (reduceByKey via RDD/keyed aggregation) if memory pressure high.- Use statistics, Z-ordering / clustering on event_ts for read performance.Edge cases:- Missing event_ts: set to epoch or place in a "late" partition and handle separately.- Extremely high-cardinality event_id: ensure sufficient partitions to avoid hotspotting.Complexity: single pass with shuffle by event_id: O(N) work distributed; network and disk IO dominate.