This page requires JavaScript to display.
H
Unpacking...
← Data Engineering
Data Engineering

Apache Hudi Reference

Copy-on-Write vs Merge-on-Read, upserts and incremental queries, the timeline, indexing, table services, HoodieStreamer, and a Delta vs Iceberg vs Hudi comparison.

Contents
1. What Apache Hudi Is 2. Copy-on-Write vs Merge-on-Read 3. Spark Setup & Upserts 4. Reading: Snapshot, Incremental & Time Travel 5. The Timeline 6. Indexing 7. Table Services 8. HoodieStreamer & Hudi on AWS 9. Delta vs Iceberg vs Hudi 10. Common Interview Questions

1. What Apache Hudi Is

Apache Hudi (Hadoop Upserts Deletes and Incrementals) is an open table format and data-lake platform created at Uber. It was built specifically for record-level upserts and deletes on lake storage and for incremental processing: pulling only the records that changed since a given commit. That makes it a strong fit for CDC ingestion and near-real-time lakes.

Record keys + indexes
Every record has a key; an index maps keys to file groups so upserts touch only the right files.
Two table types
Copy-on-Write for read-heavy tables; Merge-on-Read for write-heavy, low-latency ingestion.
Incremental queries
Read only the records changed after a commit time, for efficient downstream pipelines.
Timeline
An ordered log of every action on the table, enabling ACID, rollback and time travel.
Built-in table services
Compaction, clustering, cleaning and indexing can run inline or asynchronously.
Ingestion tooling
HoodieStreamer ingests from Kafka, DFS or databases with checkpointing out of the box.

2. Copy-on-Write vs Merge-on-Read

Copy-on-Write (CoW)Merge-on-Read (MoR)
StorageColumnar base files (Parquet) onlyBase files + row-based log files (Avro) of changes
On updateRewrite the affected base filesAppend changes to log files; compaction merges them later
Write latency / costHigherLower
Read costLowestHigher for snapshot reads until compaction
Best forBatch ETL, read-heavy analyticsCDC, streaming ingestion, frequent updates
Query typeReturnsTable types
SnapshotLatest data (MoR merges base + logs at read time)CoW, MoR
Read-optimizedOnly compacted base files: faster, possibly slightly staleMoR
IncrementalRecords changed after a given commit timeCoW, MoR
Time travelThe table as of a past instantCoW, MoR

3. Spark Setup & Upserts

spark = (SparkSession.builder.appName("hudi")
  .config("spark.jars.packages", "org.apache.hudi:hudi-spark3.5-bundle_2.12:1.0.0")
  .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
  .config("spark.sql.extensions", "org.apache.spark.sql.hudi.HoodieSparkSessionExtension")
  .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.hudi.catalog.HoodieCatalog")
  .getOrCreate())
hudi_options = {
    "hoodie.table.name": "transactions",
    "hoodie.datasource.write.table.type": "MERGE_ON_READ",       # or COPY_ON_WRITE
    "hoodie.datasource.write.recordkey.field": "txn_id",          # unique key (composite: "a,b")
    "hoodie.datasource.write.partitionpath.field": "event_date",
    "hoodie.datasource.write.precombine.field": "updated_at",     # latest wins when keys collide
    "hoodie.datasource.write.operation": "upsert",
    "hoodie.datasource.write.hive_style_partitioning": "true",    # event_date=2026-10-25/
}

(updates_df.write.format("hudi")
    .options(**hudi_options)
    .mode("append")                     # append = upsert into existing table
    .save("s3://acme-lake/hudi/transactions"))
OperationUse
upsertInsert new keys, update existing ones (default)
insertFaster when you know keys are new; skips the index lookup
bulk_insertInitial loads of large datasets; sorts and writes efficiently
deleteDelete records by key (or send rows with _hoodie_is_deleted = true in an upsert)
insert_overwriteReplace the partitions present in the input

The precombine field decides which record wins when the same key appears more than once, within a batch or against existing data. Usually it is an update timestamp or a CDC sequence number.

4. Reading: Snapshot, Incremental & Time Travel

path = "s3://acme-lake/hudi/transactions"

snapshot = spark.read.format("hudi").load(path)

read_optimized = (spark.read.format("hudi")
    .option("hoodie.datasource.query.type", "read_optimized")
    .load(path))

incremental = (spark.read.format("hudi")
    .option("hoodie.datasource.query.type", "incremental")
    .option("hoodie.datasource.read.begin.instanttime", "20261025000000")   # exclusive
    .load(path))

as_of = (spark.read.format("hudi")
    .option("as.of.instant", "20261025120000")
    .load(path))

Every record carries metadata columns: _hoodie_commit_time, _hoodie_commit_seqno, _hoodie_record_key, _hoodie_partition_path and _hoodie_file_name. A downstream job stores the last _hoodie_commit_time it processed and uses it as the next incremental begin time.

5. The Timeline

The .hoodie/ folder holds the timeline: every action on the table as an instant with a timestamp, an action type and a state.

ActionMeaning
commitA write to a CoW table (or a completed compaction)
deltacommitA write to a MoR table (adds log files)
compactionMerging MoR log files into new base files
cleanRemoving old file versions no longer needed
replacecommitClustering or insert_overwrite replacing file groups
rollback / restoreUndoing a failed or unwanted write

Each instant moves through requested → inflight → completed. Readers only see completed instants, which is how Hudi provides atomic commits; a failed write is rolled back automatically on the next write.

6. Indexing

An upsert must find which file group already holds each key. The index makes that lookup fast instead of scanning the whole table.

IndexHow it worksNotes
BloomBloom filters and key ranges stored in base file footers or the metadata tableGood when keys are roughly ordered (e.g. time-based IDs)
SimpleJoins incoming keys against keys read from the affected partitionsGood for random keys with updates concentrated in recent partitions
BucketHash of the key decides the file group directlyNo lookup cost; bucket count must be planned
Record-level indexKey → location map stored in the metadata tableFast lookups for large tables with random updates
Global variantsEnforce key uniqueness across all partitionsNeeded if a record's partition value can change; more expensive

Trap: with a non-global index, uniqueness is only per partition. If a record's partition field changes (e.g. its date is corrected), you get a duplicate in the new partition unless you use a global index with partition-path updates enabled.

7. Table Services

ServicePurposeKey settings
Compaction (MoR)Merge log files into new base files for faster readshoodie.compact.inline, hoodie.compact.inline.max.delta.commits
ClusteringRewrite small files into larger, sorted ones to improve query performancehoodie.clustering.inline, hoodie.clustering.plan.strategy.sort.columns
CleaningDelete old file versions beyond what time travel and readers needhoodie.cleaner.policy, hoodie.cleaner.commits.retained
ArchivalMove old timeline instants out of the active timelinehoodie.keep.min.commits / hoodie.keep.max.commits

Services can run inline (after a write, simple but adds latency), asynchronously alongside a streaming writer, or as separate scheduled jobs.

8. HoodieStreamer & Hudi on AWS

HoodieStreamer (formerly DeltaStreamer) is a ready-made Spark ingestion job: it reads from Kafka, files or JDBC, applies an optional transformation, upserts into Hudi, and checkpoints its position.

spark-submit \
  --class org.apache.hudi.utilities.streamer.HoodieStreamer \
  hudi-utilities-slim-bundle_2.12-1.0.0.jar \
  --table-type MERGE_ON_READ \
  --source-class org.apache.hudi.utilities.sources.JsonKafkaSource \
  --source-ordering-field updated_at \
  --target-base-path s3://acme-lake/hudi/transactions \
  --target-table transactions \
  --props file:///opt/config/kafka-source.properties \
  --continuous                       # keep running; omit for a single batch
ServiceHudi support
EMRHudi bundled with Spark, Hive, Presto/Trino
Glue ETLJob parameter --datalake-formats hudi
Athena / Redshift SpectrumQuery Hudi tables registered in the Glue Catalog (check which query types your engine version supports)
DMSWrite CDC to S3, then upsert into Hudi with Glue/EMR or HoodieStreamer

9. Delta vs Iceberg vs Hudi

Delta LakeApache IcebergApache Hudi
OriginDatabricks (2019)Netflix (2018)Uber (2017)
MetadataJSON transaction log + Parquet checkpointsmetadata.json → manifest list → manifestsTimeline in .hoodie + metadata table
Core strengthTight Spark/Databricks integration, simple operationsEngine-neutral spec, hidden partitioning, partition evolutionRecord-level upserts, indexes, incremental pulls
PartitioningHive-style, or liquid clusteringHidden partition transforms, evolvableHive-style partition paths
Row-level changesMERGE; deletion vectorsMERGE with copy-on-write or merge-on-readUpserts with CoW or MoR table types
Incremental readsChange Data Feed, streaming sourceIncremental scans between snapshots, changelog viewsNative incremental queries by commit time
CatalogsUnity Catalog, Hive/GlueREST (Polaris, Unity, Nessie), Glue, Hive, JDBCHive Metastore, Glue
Best fitDatabricks-centric lakehousesMulti-engine platforms, AWS (Athena, S3 Tables), SnowflakeCDC-heavy and near-real-time ingestion

The formats are converging: Delta UniForm exposes Iceberg metadata, Apache XTable translates metadata between all three, and REST catalogs serve tables to many engines. Choose by your primary engines, catalog strategy and write pattern (batch vs CDC/upserts).

10. Common Interview Questions

QuestionShort answer
CoW vs MoR in Hudi?CoW rewrites base files on every update (fast reads, slower writes); MoR appends changes to log files and compacts later (fast writes, reads merge until compacted)
What is the precombine field?The column that decides which version of a record wins when duplicate keys arrive, usually an update timestamp or CDC sequence number
How do incremental queries work?Each record stores its commit time; you ask for records with commits after a given instant and keep that instant as a checkpoint
Global vs non-global index?Non-global guarantees key uniqueness per partition (cheaper); global guarantees it across the table, needed when partition values can change
Why do MoR snapshot queries get slower over time?Log files accumulate and must be merged at read time; schedule compaction
When would you choose Hudi over Delta or Iceberg?Heavy CDC or upsert workloads needing record-level indexes and native incremental pulls, especially on EMR/Glue-based lakes