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.
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.
2. Copy-on-Write vs Merge-on-Read
| Copy-on-Write (CoW) | Merge-on-Read (MoR) | |
|---|---|---|
| Storage | Columnar base files (Parquet) only | Base files + row-based log files (Avro) of changes |
| On update | Rewrite the affected base files | Append changes to log files; compaction merges them later |
| Write latency / cost | Higher | Lower |
| Read cost | Lowest | Higher for snapshot reads until compaction |
| Best for | Batch ETL, read-heavy analytics | CDC, streaming ingestion, frequent updates |
| Query type | Returns | Table types |
|---|---|---|
| Snapshot | Latest data (MoR merges base + logs at read time) | CoW, MoR |
| Read-optimized | Only compacted base files: faster, possibly slightly stale | MoR |
| Incremental | Records changed after a given commit time | CoW, MoR |
| Time travel | The table as of a past instant | CoW, 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"))
| Operation | Use |
|---|---|
upsert | Insert new keys, update existing ones (default) |
insert | Faster when you know keys are new; skips the index lookup |
bulk_insert | Initial loads of large datasets; sorts and writes efficiently |
delete | Delete records by key (or send rows with _hoodie_is_deleted = true in an upsert) |
insert_overwrite | Replace 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.
| Action | Meaning |
|---|---|
commit | A write to a CoW table (or a completed compaction) |
deltacommit | A write to a MoR table (adds log files) |
compaction | Merging MoR log files into new base files |
clean | Removing old file versions no longer needed |
replacecommit | Clustering or insert_overwrite replacing file groups |
rollback / restore | Undoing 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.
| Index | How it works | Notes |
|---|---|---|
| Bloom | Bloom filters and key ranges stored in base file footers or the metadata table | Good when keys are roughly ordered (e.g. time-based IDs) |
| Simple | Joins incoming keys against keys read from the affected partitions | Good for random keys with updates concentrated in recent partitions |
| Bucket | Hash of the key decides the file group directly | No lookup cost; bucket count must be planned |
| Record-level index | Key → location map stored in the metadata table | Fast lookups for large tables with random updates |
| Global variants | Enforce key uniqueness across all partitions | Needed 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
| Service | Purpose | Key settings |
|---|---|---|
| Compaction (MoR) | Merge log files into new base files for faster reads | hoodie.compact.inline, hoodie.compact.inline.max.delta.commits |
| Clustering | Rewrite small files into larger, sorted ones to improve query performance | hoodie.clustering.inline, hoodie.clustering.plan.strategy.sort.columns |
| Cleaning | Delete old file versions beyond what time travel and readers need | hoodie.cleaner.policy, hoodie.cleaner.commits.retained |
| Archival | Move old timeline instants out of the active timeline | hoodie.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
| Service | Hudi support |
|---|---|
| EMR | Hudi bundled with Spark, Hive, Presto/Trino |
| Glue ETL | Job parameter --datalake-formats hudi |
| Athena / Redshift Spectrum | Query Hudi tables registered in the Glue Catalog (check which query types your engine version supports) |
| DMS | Write CDC to S3, then upsert into Hudi with Glue/EMR or HoodieStreamer |
9. Delta vs Iceberg vs Hudi
| Delta Lake | Apache Iceberg | Apache Hudi | |
|---|---|---|---|
| Origin | Databricks (2019) | Netflix (2018) | Uber (2017) |
| Metadata | JSON transaction log + Parquet checkpoints | metadata.json → manifest list → manifests | Timeline in .hoodie + metadata table |
| Core strength | Tight Spark/Databricks integration, simple operations | Engine-neutral spec, hidden partitioning, partition evolution | Record-level upserts, indexes, incremental pulls |
| Partitioning | Hive-style, or liquid clustering | Hidden partition transforms, evolvable | Hive-style partition paths |
| Row-level changes | MERGE; deletion vectors | MERGE with copy-on-write or merge-on-read | Upserts with CoW or MoR table types |
| Incremental reads | Change Data Feed, streaming source | Incremental scans between snapshots, changelog views | Native incremental queries by commit time |
| Catalogs | Unity Catalog, Hive/Glue | REST (Polaris, Unity, Nessie), Glue, Hive, JDBC | Hive Metastore, Glue |
| Best fit | Databricks-centric lakehouses | Multi-engine platforms, AWS (Athena, S3 Tables), Snowflake | CDC-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
| Question | Short 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 |