- hadoop 11
- bigdata 7
- spark 7
- big data 6
- hive 6
- java 5
- hdfs 4
- Apache Iceberg 4
- apache-spark 3
- lakehouse 3
- data engineering 3
- Java Interview 2
- Pig 2
- metastore 2
- hbase 2
- hcatalog 2
- kafka 2
- java8 2
- scala 2
- cloud 1
- high availability 1
- scalable 1
- storage 1
- apache 1
- cxf 1
- soap 1
- webservice 1
- Intern 1
- String 1
- Technology 1
- Pig Latin 1
- Centralised Logging 1
- ElasticSearch 1
- ELK 1
- Kibana 1
- LogStash 1
- Api Docs 1
- documentation 1
- REST API 1
- Spring 1
- Spring Boot 1
- Swagger 1
- Finalize 1
- Garbage collection 1
- JVM 1
- Poodle 1
- ssl 1
- tls 1
- vulnerability 1
- datawarehouse 1
- mysql 1
- hadoop interview 1
- hiveserver2 1
- jdbc 1
- cluster by 1
- distribute by 1
- hive interview 1
- Hiveql 1
- order by 1
- sort by 1
- spark sql 1
- coprocessor 1
- storm 1
- twitter 1
- camus 1
- kafka-hdfs 1
- linkedin 1
- bg 1
- fg 1
- jobs 1
- process management 1
- unix 1
- collection 1
- hashmap 1
- fast 1
- rdd 1
- ELK Stack 1
- Learning Elk Stack 1
- PacktPub 1
- Saurabh Chhajed 1
- eclipse 1
- lambda 1
- maven 1
- mapereduce 1
- MultithreadedMapper 1
- spark interview 1
- spark on windows 1
- winutils.exe 1
- git 1
- repository 1
- split 1
- catboost 1
- HDFS 1
- Object Storage 1
- Spark 1
- Big Data 1
- Cloud 1
- Data Engineering 1
- data lakes 1
- open table format 1
- data architecture 1
- Apache Kafka 1
- Spark Structured Streaming 1
- streaming 1
- Model Context Protocol 1
- MCP 1
- PyIceberg 1
- AI agents 1
- Claude 1
- Apache Spark 1
- small files 1
- compaction 1
hadoop
How to Use MultiThreadedMapper in MapReduce
A practical, developer-focused guide to using Hadoop’s MultithreadedMapper for parallelizing map tasks and improving performance in CPU-bound jobs.
What is RDD in Spark ? and Why do we need it ?
A developer-friendly guide to understanding Resilient Distributed Datasets (RDDs) in Apache Spark, their properties, and why they are fundamental for fast, fault-tolerant distributed computing.
What is Apache HCatalog ?
A developer-friendly introduction to Apache HCatalog, its architecture, features, and how it fits into the Hadoop ecosystem.
Unix Job Control Commands – bg, fg, Ctrl+Z,jobs
A practical guide for developers and data engineers to manage long-running jobs in Unix, especially useful when working with Hadoop or other big data tools.
How-to : Write a CoProcessor in HBase
Coprocessor is a mechanism which helps to move computations closer to the data in HBase. It is like a Mapreduce framework to distribute tasks across the cluster.
Hive : SORT BY vs ORDER BY vs DISTRIBUTE BY vs CLUSTER BY
In Apache Hive HQL, you can decide to order or sort your data differently based on ordering and distribution requirement. In this post we will look at how SORT BY, ORDER BY, DISTRIBUTE BY and CLUSTER BY behave differently in Hive. Let’s get started -
How-To : Use HCatalog with Pig
This post is a step by step guide on running HCatalog and using HCatalog with Apache Pig:
Hive Strict Mode
![Sort By vs Order By vs Group By vs Cluster By in Hive]/assets/uploads/2015/01/images.jpg)
What is Hive Strict Mode?
Hive Strict Mode (hive.mapred.mode=strict) enables Hive to restrict certain performance intensive operations. Such as:
How-To : Configure MySQL Metastore for Hive ?
Hive by default comes with Derby as its metastore storage, which is suited only for testing purposes and in most of the production scenarios it is recommended to use MySQL as a metastore. This is a step by step guide on How to Configure MySQL Metastore for Hive in place of Derby Metastore (Default).
Hadoop : Getting Started with Pig
What is Apache Pig?
Apache Pig is a high level scripting language that is used with Apache Hadoop. It enables data analysts to write complex data transformations without knowing Java. Its simple SQL-like scripting language is called Pig Latin, and appeals to developers already familiar with scripting languages and SQL. Pig Scripts are converted into MapReduce Jobs which run on data stored in HDFS (refer to the diagram below).
Top 10 Hadoop Shell Commands to manage HDFS
So you already know what Hadoop is? Why it is used for? What problems you can solve with it? And you want to know how you can deal with files on HDFS? Don’t worry, you are in the right place.
bigdata
What does Skipped Stage means in Spark WebUI ?
Skipped Stages in Spark UI
You must have come across various scenarios where you see a DAG like below, where you see a few stages shows greyed out with a text (skipped) after the stage name. What does this mean? Did Spark ignore one of your stage due to an error? or this is due to something else? Well, it’s actually a good thing. It means that particular stage in the lineage DAG doesn’t need to be re-evaluated as its already evaluated and cached. This will save computation time for that stage. If you want to see what data frame for that stage was stored in the cache, you can check in Storage tab in Spark UI.
Dataframe Operations in Spark using Scala
A comprehensive, developer-friendly guide to common DataFrame operations in Apache Spark using Scala, with code examples and explanations for each join type.
What is RDD in Spark ? and Why do we need it ?
A developer-friendly guide to understanding Resilient Distributed Datasets (RDDs) in Apache Spark, their properties, and why they are fundamental for fast, fault-tolerant distributed computing.
What is Apache HCatalog ?
A developer-friendly introduction to Apache HCatalog, its architecture, features, and how it fits into the Hadoop ecosystem.
Hive : SORT BY vs ORDER BY vs DISTRIBUTE BY vs CLUSTER BY
In Apache Hive HQL, you can decide to order or sort your data differently based on ordering and distribution requirement. In this post we will look at how SORT BY, ORDER BY, DISTRIBUTE BY and CLUSTER BY behave differently in Hive. Let’s get started -
Hive Strict Mode
![Sort By vs Order By vs Group By vs Cluster By in Hive]/assets/uploads/2015/01/images.jpg)
What is Hive Strict Mode?
Hive Strict Mode (hive.mapred.mode=strict) enables Hive to restrict certain performance intensive operations. Such as:
How-To : Configure MySQL Metastore for Hive ?
Hive by default comes with Derby as its metastore storage, which is suited only for testing purposes and in most of the production scenarios it is recommended to use MySQL as a metastore. This is a step by step guide on How to Configure MySQL Metastore for Hive in place of Derby Metastore (Default).
spark
How to Train and Score Catboost Model on Spark
A practical, developer-focused guide to distributed training and inference with CatBoost on Apache Spark, including code examples and best practices.
Spark – How to Run Spark Applications on Windows
A step-by-step, developer-friendly guide to running Apache Spark applications on Windows, including configuration, environment setup, and troubleshooting tips.
What does Skipped Stage means in Spark WebUI ?
Skipped Stages in Spark UI
You must have come across various scenarios where you see a DAG like below, where you see a few stages shows greyed out with a text (skipped) after the stage name. What does this mean? Did Spark ignore one of your stage due to an error? or this is due to something else? Well, it’s actually a good thing. It means that particular stage in the lineage DAG doesn’t need to be re-evaluated as its already evaluated and cached. This will save computation time for that stage. If you want to see what data frame for that stage was stored in the cache, you can check in Storage tab in Spark UI.
Dataframe Operations in Spark using Scala
A comprehensive, developer-friendly guide to common DataFrame operations in Apache Spark using Scala, with code examples and explanations for each join type.
How to Configure Spark Application ( Scala and Java 8 Version with Maven ) in Eclipse.
A step-by-step, developer-friendly guide to setting up Apache Spark applications in Eclipse/Scala IDE using Maven, with both Java and Scala examples.
What is RDD in Spark ? and Why do we need it ?
A developer-friendly guide to understanding Resilient Distributed Datasets (RDDs) in Apache Spark, their properties, and why they are fundamental for fast, fault-tolerant distributed computing.
Hive : SORT BY vs ORDER BY vs DISTRIBUTE BY vs CLUSTER BY
In Apache Hive HQL, you can decide to order or sort your data differently based on ordering and distribution requirement. In this post we will look at how SORT BY, ORDER BY, DISTRIBUTE BY and CLUSTER BY behave differently in Hive. Let’s get started -
big data
Unlocking Data Lakes with Apache Iceberg: The Open Table Format Revolutionizing Analytics
Why Apache Iceberg is the Game-Changer Your Data Lake Needs
Still relying on traditional data lakes with unreliable reads, lack of ACID guarantees, and painful schema evolution?
Welcome to Apache Iceberg—an open table format purpose-built for modern, petabyte-scale analytics. With native support for engines like Apache Spark, Trino, Presto, Hive, and Flink, Iceberg solves the biggest challenges in big data processing.
In this post, we’ll break down:
- What Apache Iceberg is and why it matters
- How its architecture works (with a visual!)
- Key features that make it future-ready
- Real-world use cases
- A call to action for your next data platform decision
Spark – How to Run Spark Applications on Windows
A step-by-step, developer-friendly guide to running Apache Spark applications on Windows, including configuration, environment setup, and troubleshooting tips.
How-To : Write a Kafka Producer using Twitter Stream ( Twitter HBC Client)
A step-by-step guide to building a Kafka producer that streams live tweets using Twitter’s Hosebird Client (HBC) and publishes them to a Kafka topic. This is a practical, developer-focused walkthrough with code, configuration, and troubleshooting tips.
How-To : Setup Realtime Alalytics over Logs with ELK Stack : Elasticsearch, Logstash, Kibana?
Once we know something, we find it hard to imagine what it was like not to know it.
— Chip & Dan Heath, Authors of Made to Stick, Switch
What is the ELK stack?
The ELK stack consists of open source tools ElasticSearch, Logstash, and Kibana. These three provide a fully working real-time data analytics tool for getting wonderful information sitting on your data.
Hadoop : Getting Started with Pig
What is Apache Pig?
Apache Pig is a high level scripting language that is used with Apache Hadoop. It enables data analysts to write complex data transformations without knowing Java. Its simple SQL-like scripting language is called Pig Latin, and appeals to developers already familiar with scripting languages and SQL. Pig Scripts are converted into MapReduce Jobs which run on data stored in HDFS (refer to the diagram below).
Top 10 Hadoop Shell Commands to manage HDFS
So you already know what Hadoop is? Why it is used for? What problems you can solve with it? And you want to know how you can deal with files on HDFS? Don’t worry, you are in the right place.
hive
What is Apache HCatalog ?
A developer-friendly introduction to Apache HCatalog, its architecture, features, and how it fits into the Hadoop ecosystem.
Hive : SORT BY vs ORDER BY vs DISTRIBUTE BY vs CLUSTER BY
In Apache Hive HQL, you can decide to order or sort your data differently based on ordering and distribution requirement. In this post we will look at how SORT BY, ORDER BY, DISTRIBUTE BY and CLUSTER BY behave differently in Hive. Let’s get started -
How-To : Use HCatalog with Pig
This post is a step by step guide on running HCatalog and using HCatalog with Apache Pig:
Hive Strict Mode
![Sort By vs Order By vs Group By vs Cluster By in Hive]/assets/uploads/2015/01/images.jpg)
What is Hive Strict Mode?
Hive Strict Mode (hive.mapred.mode=strict) enables Hive to restrict certain performance intensive operations. Such as:
How-To : Connect HiveServer2 service with JDBC Client ?
HiveServer2 (HS2) is a server interface that enables remote clients to execute queries against Hive and retrieve the results. The current implementation, based on Thrift RPC, is an improved version of HiveServer and supports multi-client concurrency and authentication. It is designed to provide better support for open API clients like JDBC and ODBC.
How-To : Configure MySQL Metastore for Hive ?
Hive by default comes with Derby as its metastore storage, which is suited only for testing purposes and in most of the production scenarios it is recommended to use MySQL as a metastore. This is a step by step guide on How to Configure MySQL Metastore for Hive in place of Derby Metastore (Default).
java
How to Configure Spark Application ( Scala and Java 8 Version with Maven ) in Eclipse.
A step-by-step, developer-friendly guide to setting up Apache Spark applications in Eclipse/Scala IDE using Maven, with both Java and Scala examples.
Java : What does finalize do and How?
finalize method in the Object class is often a point of discussion regarding whether it should be used or not. Below are some important pointers on the finalize method:
How-To : Generate Restful API Documentation with Swagger ?
Swagger is a specification and complete framework implementation for describing, producing, consuming, and visualizing RESTful web services. The goal of Swagger is to enable client and documentation systems to update at the same pace as the server. The documentation of methods, parameters, and models are tightly integrated into the server code, allowing APIs to always stay in sync.
String Interning – What ,Why and When ?
What is String Interning
String Interning is a method of storing only one copy of each distinct String Value, which must be immutable.
SOAP Webservices Using Apache CXF : Adding Custom Object as Header in Outgoing Requests
What is Apache CXF?
Apache CXF is an open source services framework. CXF helps you build and develop services using frontend programming APIs, like JAX-WS and JAX-RS. These services can speak a variety of protocols such as SOAP, XML/HTTP, RESTful HTTP, or CORBA and work over a variety of transports such as HTTP, JMS etc.
hdfs
How-To : Integrate Kafka with HDFS using Camus (Twitter Stream Example)
A step-by-step guide to building a Kafka-to-HDFS data pipeline using Camus and a Twitter stream. This guide is aimed at developers looking for a practical, detailed walkthrough.
How-to : Write a CoProcessor in HBase
Coprocessor is a mechanism which helps to move computations closer to the data in HBase. It is like a Mapreduce framework to distribute tasks across the cluster.
Hadoop : Getting Started with Pig
What is Apache Pig?
Apache Pig is a high level scripting language that is used with Apache Hadoop. It enables data analysts to write complex data transformations without knowing Java. Its simple SQL-like scripting language is called Pig Latin, and appeals to developers already familiar with scripting languages and SQL. Pig Scripts are converted into MapReduce Jobs which run on data stored in HDFS (refer to the diagram below).
Top 10 Hadoop Shell Commands to manage HDFS
So you already know what Hadoop is? Why it is used for? What problems you can solve with it? And you want to know how you can deal with files on HDFS? Don’t worry, you are in the right place.
Apache Iceberg
The Small-Files Problem: A Field Guide
The Maintenance Chapter Your Streaming Pipeline Was Missing
Your streaming job writes 2 MB Parquet files every two minutes. Six months later, your dashboard queries are ten times slower — and nobody changed the data. Sound familiar?
Yesterday, in Streaming from Kafka into an Iceberg Lakehouse, End to End, we built the whole pipeline — Kafka topic, Spark Structured Streaming, Iceberg table — and Step 6 ended with a warning: one-minute micro-batches create lots of small files — the classic streaming lakehouse problem. This post picks up exactly there. The pipeline works; the file layout is what’s rotting underneath it.
Same stack as yesterday: Kafka 4.3.1, Spark 4.2.0 (Scala 2.13), Iceberg 1.12.0, PySpark — and the same lakehouse.order_events table, partitioned by days(event_time).
In this post, we’ll:
-
See why small files hurt — with real numbers, not vibes
-
Diagnose the problem using Iceberg’s own metadata tables
-
Stop writing small files at write time
-
Fix the damage with compaction (
rewrite_data_files, done right) -
Keep the layout healthy with a checklist that fits on an index card
Step 1: Understand Why Small Files Hurt
It’s tempting to treat file count as cosmetic. It isn’t. Every file in your table is a tax paid four times: at plan time, at task-scheduling time, at request time, and at footer-read time.
Query planning. Iceberg query planning reads manifest files to decide which data files to scan — the manifest and snapshot layer I broke down in the Iceberg intro is exactly what gets stressed here. Dremio’s engineers measured this directly: the same dataset stored in 2,000 files of 256 MB each typically plans in under one second, while a table fragmented into 100,000 files pays a planning cost an order of magnitude higher. Manifests fragment too — every micro-batch commit adds manifests, and the metadata tree stops being an index and becomes a maze.
Task scheduling. Each file becomes at least one execution task. Scheduling costs roughly 1–10 milliseconds per task — trivial, until you multiply: a query against 100,000 files creates at least 100,000 tasks. Even if partition pruning eliminates 95% of them, the surviving 5,000 tasks carry 5–50 seconds of pure scheduling overhead before any data moves. On JVM engines like Spark, giant task graphs also pile on GC pressure, adding unpredictable latency.
Object-store requests. S3 charges $0.0004 per 1,000 GET requests. Reading a file takes at minimum one GET, but Parquet readers also fetch the footer and individual row groups — conservatively, two requests per file. So 100,000 files means 200,000 requests, or $0.08 per query, in storage API costs alone. A table queried 10,000 times a day burns $800 a day — $24,000 a month — on requests. Compact to 2,000 files and that drops to about $1,600 a month, a 93% reduction (source).
Footer reads. Every Parquet file carries a footer — schema, row-group metadata, column statistics. Reading it is mandatory and costs roughly the same whether the file is 1 MB or 512 MB: one to several milliseconds plus a network round trip. For a 256 MB file that’s noise; for a 1 MB file it dominates. If 95% of your read time goes to footers instead of column data, you have a severe small-files problem (source).
How fast it accumulates. Streaming engines checkpoint every 1–10 minutes, and each checkpoint commits new Parquet files — one per partition. A table with 100 partitions on 10-minute checkpoints produces 14,400 new files per day, most of them a few megabytes each (source). A table with 500,000 small files queried 50 times a day generates roughly 25 million GET requests a month. The write path that felt so elegant on day one is a liability by month six.
Step 2: Diagnose — Is This Actually Your Problem?
Don’t compact on vibes. Iceberg gives you metadata tables that answer the question directly:
-- File health at a glance: count, average size, smallest/largest
SELECT
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb,
ROUND(MIN(file_size_in_bytes) / 1048576, 2) AS min_mb,
ROUND(MAX(file_size_in_bytes) / 1048576, 2) AS max_mb,
SUM(CASE WHEN file_size_in_bytes < 134217728
THEN 1 ELSE 0 END) AS files_under_128mb
FROM lakehouse.order_events.files
WHERE content = 0; -- 0 = DATA files; 1/2 = delete files
If a large share of files sits under 128 MB — or you see thousands of files averaging a few MB — you have the problem. Next, find where it hurts most, so your first compaction pass buys the biggest win:
-- Worst partitions first: file count and avg size per partition
SELECT
partition,
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb
FROM lakehouse.order_events.files
WHERE content = 0
GROUP BY partition
ORDER BY file_count DESC
LIMIT 30;
Two more signals worth checking:
-- Are snapshots piling up from streaming appends?
SELECT snapshot_id, committed_at, operation, summary
FROM lakehouse.order_events.snapshots
ORDER BY committed_at DESC
LIMIT 20;
A wall of append snapshots with no replace (compaction) in sight confirms the diagnosis. And don’t forget the manifests metadata table — if manifest count is climbing with file count, planning overhead is compounding.
Outside SQL, the symptoms are just as telling: Spark UI jobs where a stage fans out into tens of thousands of tiny tasks, query plans that take longer than the scan itself, and — the one people notice last — your S3 GetObject/ListObjects request metrics climbing month over month while data volume stays flat.
Step 3: Fix It at Write Time
Compaction is corrective. The cheaper move is to stop writing small files in the first place. Three things worth knowing about the write path:
1. Target file size. The table property write.target-file-size-bytes defaults to 512 MB. Spark writers roll a new file when buffered data crosses this threshold — but only if a single task actually accumulates that much. A streaming micro-batch with 50 MB of data never reaches it, which is exactly how you get 2 MB files:
-- Set once at the table level (256 MB is a common streaming-table choice)
ALTER TABLE lakehouse.order_events
SET TBLPROPERTIES ('write.target-file-size-bytes' = '268435456');
You can also override it per write with the target-file-size-bytes DataFrame option.
2. Distribution mode. write.distribution-mode decides how rows are shuffled before writing: none (no shuffle — each task writes whatever it has; the small-files factory), hash (hash-distribute by partition key), or range (range-distribute by partition or sort key). For streaming writes into a partitioned table, hash is usually the right call:
(df.writeStream
.format("iceberg")
.option("checkpointLocation", checkpoint_path)
.option("distribution-mode", "hash") # shuffle by partition key before writing
.option("fanout-enabled", "true") # keep partition writers open across batches
.toTable("lakehouse.order_events"))
3. The fanout writer. Streaming data arrives out of order across partitions. Without fanout, every partition switch inside a task closes the current file and opens a new one — file rollover churn. write.spark.fanout.enabled (default false) keeps multiple partition writers open simultaneously, at the cost of more memory. For streaming jobs, enable it.
We used
fanout-enabled: truein the streaming job from yesterday’s post for exactly this reason — without it, out-of-order events across days would have multiplied our file count from the very first batch.
Two more write-path habits that pay off:
repartitionbefore batch writes. Line writers up with the partition spec so each task writes fewer, larger files:
from pyspark.sql.functions import to_date
def write_batch(batch_df, batch_id):
(batch_df
.repartition(to_date("event_time")) # align writers with days(event_time)
.writeTo("lakehouse.order_events")
.option("fanout-enabled", "true")
.append())
(query.writeStream
.foreachBatch(write_batch)
.option("checkpointLocation", checkpoint_path)
.trigger(availableNow=True)
.start())
- Trigger intervals. The one-minute trigger we used yesterday is great for freshness demos and terrible for file layout — on a low-volume topic it guarantees tiny files. Prefer
Trigger.AvailableNowor multi-minute processing-time triggers, and coalesce inside the batch function as above.
None of this eliminates compaction entirely — streaming tables will always drift — but it slows the drift from days to weeks.
Step 4: Compact What’s Already There
Yesterday’s post showed the one-line version — CALL lakehouse.system.rewrite_data_files(table => 'order_events'). Here’s the full picture. Iceberg’s compaction primitive rewrites selected data files into fewer, larger ones and commits the result as a new snapshot — fully online, no downtime; readers keep working against the old snapshot until the new one commits.
Binpack (default): fast and dumb, in a good way. It groups small files by partition and concatenates them up to the target size. No shuffle, no reordering — the cheapest possible rewrite:
-- Compact only the files that need it: small files in recent partitions
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'binpack',
where => 'event_time >= current_timestamp() - INTERVAL 7 DAYS',
options => map(
'target-file-size-bytes', '268435456', -- 256 MB
'min-input-files', '5' -- skip groups already healthy
)
);
Sort: slower, but fixes layout too. Binpack doesn’t touch data ordering. If your queries filter on event_time and customer_id but files were written in arrival order, column statistics are weak and pruning suffers. The sort strategy globally sorts before allocating files to tasks, tightening min/max ranges per file:
-- Merge + cluster by the columns your queries actually filter on
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'sort',
sort_order => 'event_time ASC NULLS LAST, customer_id ASC NULLS LAST',
options => map(
'target-file-size-bytes', '268435456',
'partial-progress.enabled', 'true',
'max-concurrent-file-group-rewrites', '5'
)
);
-- For multi-column filters with no single dominant column, Z-order instead:
-- sort_order => 'zorder(event_time, customer_id)'
(Iceberg 1.12 also adds Hilbert-curve clustering as a newer alternative to Z-ordering — worth evaluating once your engine supports it.)
The options that matter in production:
| Option | Default | What it does |
|---|---|---|
target-file-size-bytes |
table’s write.target-file-size-bytes (512 MB) |
Output file size target |
min-input-files |
5 | File groups with fewer files are skipped |
min-file-size-bytes |
75% of target | Files below this are always candidates |
max-file-size-bytes |
180% of target | Files above this are always candidates |
max-concurrent-file-group-rewrites |
5 | Parallelism of the rewrite; raise for large tables |
max-file-group-size-bytes |
100 GB | Splits the job into file groups |
partial-progress.enabled |
false | Commit file groups as they finish instead of one big commit |
partial-progress.max-commits |
10 | Cap on commits when partial progress is on |
rewrite-all |
false | Force-rewrite everything, ignoring the filters above |
rewrite-job-order |
none | Order groups: bytes-asc, bytes-desc, files-asc, files-desc |
Two of these deserve emphasis. partial-progress.enabled=true is the difference between losing hours of rewrite work on a mid-job failure and keeping what finished — always turn it on for large tables. And max-concurrent-file-group-rewrites is the knob real teams reach for at scale: Ancestry’s data team raised it from the default 5 to 100 to compact a 100-billion-row Iceberg table, because the default couldn’t keep up with their update frequency.
The follow-ups people forget. Compaction creates a new snapshot but leaves the old files on disk — old snapshots pin them. Run the cleanup chain, in this order:
-- 1. Expire old snapshots so their files can actually be deleted
-- (defaults: older_than = 5 days ago, retain_last = 1)
CALL lakehouse.system.expire_snapshots(
table => 'order_events',
older_than => TIMESTAMP '2026-10-01 00:00:00',
retain_last => 10
);
-- 2. Consolidate manifest files (metadata-only, fast)
CALL lakehouse.system.rewrite_manifests(table => 'order_events');
-- 3. Sweep up orphan files from failed/partial writes (default window: 3 days)
-- Use dry_run first, and never set older_than shorter than your longest write.
CALL lakehouse.system.remove_orphan_files(
table => 'order_events',
older_than => TIMESTAMP '2026-10-10 00:00:00',
dry_run => true
);
Compacting without ever expiring snapshots means storage grows forever. And
remove_orphan_fileswith too aggressive a window can delete files from in-flight writes — multi-day windows are normal; hours are not.
Step 5: Keep It Healthy — the Index-Card Checklist
The industry rule of thumb is 128–512 MB per file for analytical workloads, and Iceberg’s own defaults agree: write.target-file-size-bytes is 512 MB, and the read path combines splits toward 128 MB (read.split.target-size).
Five lines, taped to your monitor:
- Measure first. Query
tbl.filesmonthly (weekly for streaming tables). Alert when average file size drops below ~128 MB or file count crosses your threshold. - Fix the writer. Set
write.target-file-size-bytes, usedistribution-mode=hashandfanout-enabled=trueon streaming writes, and size your trigger intervals to the data volume. - Compact on a cadence that matches ingestion. Streaming tables often need compaction every few hours, not nightly; batch tables can go weekly.
- Always expire snapshots and rewrite manifests after compacting. Compaction without cleanup is half a job.
- Sweep orphans quarterly with a safe
older_thanwindow — on mature streaming lakes, orphans routinely account for a quarter or more of billable storage (source).
Variations Worth Knowing
Delete-heavy tables (merge-on-read)?
Watch delete-file counts in tbl.files too — compact position deletes with rewrite_position_delete_files, not just data files.
Sub-hour freshness needs?
Don’t chase 512 MB if it means holding micro-batches open. 128–256 MB is fine for streaming tables where freshness matters more than scan efficiency.
Heavy point lookups?
Smaller files with tight clustering (sort/Z-order) beat larger unsorted ones. File size is a means to an end — the end is pruning.
✅ Conclusion
Small files are not a moral failing — they’re the natural output of a healthy streaming pipeline. The failure is letting the write path run unsupervised. Measure the file layout like you’d measure query latency, and the problem stays a maintenance task instead of becoming an incident.
If your Kafka-to-Iceberg pipeline is past the demo stage, its next chapter isn’t a new feature — it’s maintenance. Measure
tbl.files, fix the writer, compact on cadence.
💬 Let’s Discuss
Are you compacting on a schedule, or only when queries get slow? Binpack or sort — which strategy do you reach for first? And what’s the worst small-files incident you’ve seen in production? Share your setup or war stories in the comments!
Related reading
- Unlocking Data Lakes with Apache Iceberg: The Open Table Format Revolutionizing Analytics
- MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
- HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
Additional Details
- Description: A field guide to diagnosing, preventing, and fixing the small-files problem in Spark + Apache Iceberg lakehouse tables — with real numbers, working SQL, and a maintenance checklist.
MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
Your AI Agent Can’t See Your Lakehouse - Let’s Fix That
How many times this year have you pasted a table schema into a chat window so the AI could help you write a query?
In my last post, we streamed Kafka events into an Apache Iceberg lakehouse with Spark Structured Streaming. The natural follow-up in 2026: instead of querying that table myself, can I just ask an AI agent what’s in it - and get a real answer from the live catalog, not a pasted screenshot?
That is exactly what the Model Context Protocol (MCP) is for. In this post, we’ll build a small MCP server that sits on top of an Iceberg catalog and exposes it to any MCP-capable agent (Claude, IDEs, your own agents):
- What MCP actually is, in data-engineering terms
- A working MCP server over an Iceberg catalog (Python, ~60 lines)
- Wiring it into an agent host like Claude
- The security lines you should not cross
Every code sample below was run end to end while writing this post - client, server, and a real Iceberg table.
Streaming from Kafka into an Iceberg Lakehouse, End to End
From Kafka Topics to a Lakehouse Table, the Right Way
Still landing Kafka data into HDFS or S3 as raw files and batching it later?
Back in 2015, I wrote about moving Twitter streams from Kafka to HDFS with Camus. A decade later, the goal is the same - get streaming data durably into storage - but the destination has changed completely. Today, we write straight into an Apache Iceberg table and get ACID commits, schema evolution, and time travel for free.
In this post, we’ll build the whole pipeline end to end:
- A Kafka topic producing JSON events
- A Spark Structured Streaming job that consumes the topic
- Streaming writes into an Iceberg table with exactly-once semantics
- Querying and maintaining the table (compaction, snapshots)
Unlocking Data Lakes with Apache Iceberg: The Open Table Format Revolutionizing Analytics
Why Apache Iceberg is the Game-Changer Your Data Lake Needs
Still relying on traditional data lakes with unreliable reads, lack of ACID guarantees, and painful schema evolution?
Welcome to Apache Iceberg—an open table format purpose-built for modern, petabyte-scale analytics. With native support for engines like Apache Spark, Trino, Presto, Hive, and Flink, Iceberg solves the biggest challenges in big data processing.
In this post, we’ll break down:
- What Apache Iceberg is and why it matters
- How its architecture works (with a visual!)
- Key features that make it future-ready
- Real-world use cases
- A call to action for your next data platform decision
apache-spark
Spark – How to Run Spark Applications on Windows
A step-by-step, developer-friendly guide to running Apache Spark applications on Windows, including configuration, environment setup, and troubleshooting tips.
What does Skipped Stage means in Spark WebUI ?
Skipped Stages in Spark UI
You must have come across various scenarios where you see a DAG like below, where you see a few stages shows greyed out with a text (skipped) after the stage name. What does this mean? Did Spark ignore one of your stage due to an error? or this is due to something else? Well, it’s actually a good thing. It means that particular stage in the lineage DAG doesn’t need to be re-evaluated as its already evaluated and cached. This will save computation time for that stage. If you want to see what data frame for that stage was stored in the cache, you can check in Storage tab in Spark UI.
Dataframe Operations in Spark using Scala
A comprehensive, developer-friendly guide to common DataFrame operations in Apache Spark using Scala, with code examples and explanations for each join type.
lakehouse
The Small-Files Problem: A Field Guide
The Maintenance Chapter Your Streaming Pipeline Was Missing
Your streaming job writes 2 MB Parquet files every two minutes. Six months later, your dashboard queries are ten times slower — and nobody changed the data. Sound familiar?
Yesterday, in Streaming from Kafka into an Iceberg Lakehouse, End to End, we built the whole pipeline — Kafka topic, Spark Structured Streaming, Iceberg table — and Step 6 ended with a warning: one-minute micro-batches create lots of small files — the classic streaming lakehouse problem. This post picks up exactly there. The pipeline works; the file layout is what’s rotting underneath it.
Same stack as yesterday: Kafka 4.3.1, Spark 4.2.0 (Scala 2.13), Iceberg 1.12.0, PySpark — and the same lakehouse.order_events table, partitioned by days(event_time).
In this post, we’ll:
-
See why small files hurt — with real numbers, not vibes
-
Diagnose the problem using Iceberg’s own metadata tables
-
Stop writing small files at write time
-
Fix the damage with compaction (
rewrite_data_files, done right) -
Keep the layout healthy with a checklist that fits on an index card
Step 1: Understand Why Small Files Hurt
It’s tempting to treat file count as cosmetic. It isn’t. Every file in your table is a tax paid four times: at plan time, at task-scheduling time, at request time, and at footer-read time.
Query planning. Iceberg query planning reads manifest files to decide which data files to scan — the manifest and snapshot layer I broke down in the Iceberg intro is exactly what gets stressed here. Dremio’s engineers measured this directly: the same dataset stored in 2,000 files of 256 MB each typically plans in under one second, while a table fragmented into 100,000 files pays a planning cost an order of magnitude higher. Manifests fragment too — every micro-batch commit adds manifests, and the metadata tree stops being an index and becomes a maze.
Task scheduling. Each file becomes at least one execution task. Scheduling costs roughly 1–10 milliseconds per task — trivial, until you multiply: a query against 100,000 files creates at least 100,000 tasks. Even if partition pruning eliminates 95% of them, the surviving 5,000 tasks carry 5–50 seconds of pure scheduling overhead before any data moves. On JVM engines like Spark, giant task graphs also pile on GC pressure, adding unpredictable latency.
Object-store requests. S3 charges $0.0004 per 1,000 GET requests. Reading a file takes at minimum one GET, but Parquet readers also fetch the footer and individual row groups — conservatively, two requests per file. So 100,000 files means 200,000 requests, or $0.08 per query, in storage API costs alone. A table queried 10,000 times a day burns $800 a day — $24,000 a month — on requests. Compact to 2,000 files and that drops to about $1,600 a month, a 93% reduction (source).
Footer reads. Every Parquet file carries a footer — schema, row-group metadata, column statistics. Reading it is mandatory and costs roughly the same whether the file is 1 MB or 512 MB: one to several milliseconds plus a network round trip. For a 256 MB file that’s noise; for a 1 MB file it dominates. If 95% of your read time goes to footers instead of column data, you have a severe small-files problem (source).
How fast it accumulates. Streaming engines checkpoint every 1–10 minutes, and each checkpoint commits new Parquet files — one per partition. A table with 100 partitions on 10-minute checkpoints produces 14,400 new files per day, most of them a few megabytes each (source). A table with 500,000 small files queried 50 times a day generates roughly 25 million GET requests a month. The write path that felt so elegant on day one is a liability by month six.
Step 2: Diagnose — Is This Actually Your Problem?
Don’t compact on vibes. Iceberg gives you metadata tables that answer the question directly:
-- File health at a glance: count, average size, smallest/largest
SELECT
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb,
ROUND(MIN(file_size_in_bytes) / 1048576, 2) AS min_mb,
ROUND(MAX(file_size_in_bytes) / 1048576, 2) AS max_mb,
SUM(CASE WHEN file_size_in_bytes < 134217728
THEN 1 ELSE 0 END) AS files_under_128mb
FROM lakehouse.order_events.files
WHERE content = 0; -- 0 = DATA files; 1/2 = delete files
If a large share of files sits under 128 MB — or you see thousands of files averaging a few MB — you have the problem. Next, find where it hurts most, so your first compaction pass buys the biggest win:
-- Worst partitions first: file count and avg size per partition
SELECT
partition,
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb
FROM lakehouse.order_events.files
WHERE content = 0
GROUP BY partition
ORDER BY file_count DESC
LIMIT 30;
Two more signals worth checking:
-- Are snapshots piling up from streaming appends?
SELECT snapshot_id, committed_at, operation, summary
FROM lakehouse.order_events.snapshots
ORDER BY committed_at DESC
LIMIT 20;
A wall of append snapshots with no replace (compaction) in sight confirms the diagnosis. And don’t forget the manifests metadata table — if manifest count is climbing with file count, planning overhead is compounding.
Outside SQL, the symptoms are just as telling: Spark UI jobs where a stage fans out into tens of thousands of tiny tasks, query plans that take longer than the scan itself, and — the one people notice last — your S3 GetObject/ListObjects request metrics climbing month over month while data volume stays flat.
Step 3: Fix It at Write Time
Compaction is corrective. The cheaper move is to stop writing small files in the first place. Three things worth knowing about the write path:
1. Target file size. The table property write.target-file-size-bytes defaults to 512 MB. Spark writers roll a new file when buffered data crosses this threshold — but only if a single task actually accumulates that much. A streaming micro-batch with 50 MB of data never reaches it, which is exactly how you get 2 MB files:
-- Set once at the table level (256 MB is a common streaming-table choice)
ALTER TABLE lakehouse.order_events
SET TBLPROPERTIES ('write.target-file-size-bytes' = '268435456');
You can also override it per write with the target-file-size-bytes DataFrame option.
2. Distribution mode. write.distribution-mode decides how rows are shuffled before writing: none (no shuffle — each task writes whatever it has; the small-files factory), hash (hash-distribute by partition key), or range (range-distribute by partition or sort key). For streaming writes into a partitioned table, hash is usually the right call:
(df.writeStream
.format("iceberg")
.option("checkpointLocation", checkpoint_path)
.option("distribution-mode", "hash") # shuffle by partition key before writing
.option("fanout-enabled", "true") # keep partition writers open across batches
.toTable("lakehouse.order_events"))
3. The fanout writer. Streaming data arrives out of order across partitions. Without fanout, every partition switch inside a task closes the current file and opens a new one — file rollover churn. write.spark.fanout.enabled (default false) keeps multiple partition writers open simultaneously, at the cost of more memory. For streaming jobs, enable it.
We used
fanout-enabled: truein the streaming job from yesterday’s post for exactly this reason — without it, out-of-order events across days would have multiplied our file count from the very first batch.
Two more write-path habits that pay off:
repartitionbefore batch writes. Line writers up with the partition spec so each task writes fewer, larger files:
from pyspark.sql.functions import to_date
def write_batch(batch_df, batch_id):
(batch_df
.repartition(to_date("event_time")) # align writers with days(event_time)
.writeTo("lakehouse.order_events")
.option("fanout-enabled", "true")
.append())
(query.writeStream
.foreachBatch(write_batch)
.option("checkpointLocation", checkpoint_path)
.trigger(availableNow=True)
.start())
- Trigger intervals. The one-minute trigger we used yesterday is great for freshness demos and terrible for file layout — on a low-volume topic it guarantees tiny files. Prefer
Trigger.AvailableNowor multi-minute processing-time triggers, and coalesce inside the batch function as above.
None of this eliminates compaction entirely — streaming tables will always drift — but it slows the drift from days to weeks.
Step 4: Compact What’s Already There
Yesterday’s post showed the one-line version — CALL lakehouse.system.rewrite_data_files(table => 'order_events'). Here’s the full picture. Iceberg’s compaction primitive rewrites selected data files into fewer, larger ones and commits the result as a new snapshot — fully online, no downtime; readers keep working against the old snapshot until the new one commits.
Binpack (default): fast and dumb, in a good way. It groups small files by partition and concatenates them up to the target size. No shuffle, no reordering — the cheapest possible rewrite:
-- Compact only the files that need it: small files in recent partitions
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'binpack',
where => 'event_time >= current_timestamp() - INTERVAL 7 DAYS',
options => map(
'target-file-size-bytes', '268435456', -- 256 MB
'min-input-files', '5' -- skip groups already healthy
)
);
Sort: slower, but fixes layout too. Binpack doesn’t touch data ordering. If your queries filter on event_time and customer_id but files were written in arrival order, column statistics are weak and pruning suffers. The sort strategy globally sorts before allocating files to tasks, tightening min/max ranges per file:
-- Merge + cluster by the columns your queries actually filter on
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'sort',
sort_order => 'event_time ASC NULLS LAST, customer_id ASC NULLS LAST',
options => map(
'target-file-size-bytes', '268435456',
'partial-progress.enabled', 'true',
'max-concurrent-file-group-rewrites', '5'
)
);
-- For multi-column filters with no single dominant column, Z-order instead:
-- sort_order => 'zorder(event_time, customer_id)'
(Iceberg 1.12 also adds Hilbert-curve clustering as a newer alternative to Z-ordering — worth evaluating once your engine supports it.)
The options that matter in production:
| Option | Default | What it does |
|---|---|---|
target-file-size-bytes |
table’s write.target-file-size-bytes (512 MB) |
Output file size target |
min-input-files |
5 | File groups with fewer files are skipped |
min-file-size-bytes |
75% of target | Files below this are always candidates |
max-file-size-bytes |
180% of target | Files above this are always candidates |
max-concurrent-file-group-rewrites |
5 | Parallelism of the rewrite; raise for large tables |
max-file-group-size-bytes |
100 GB | Splits the job into file groups |
partial-progress.enabled |
false | Commit file groups as they finish instead of one big commit |
partial-progress.max-commits |
10 | Cap on commits when partial progress is on |
rewrite-all |
false | Force-rewrite everything, ignoring the filters above |
rewrite-job-order |
none | Order groups: bytes-asc, bytes-desc, files-asc, files-desc |
Two of these deserve emphasis. partial-progress.enabled=true is the difference between losing hours of rewrite work on a mid-job failure and keeping what finished — always turn it on for large tables. And max-concurrent-file-group-rewrites is the knob real teams reach for at scale: Ancestry’s data team raised it from the default 5 to 100 to compact a 100-billion-row Iceberg table, because the default couldn’t keep up with their update frequency.
The follow-ups people forget. Compaction creates a new snapshot but leaves the old files on disk — old snapshots pin them. Run the cleanup chain, in this order:
-- 1. Expire old snapshots so their files can actually be deleted
-- (defaults: older_than = 5 days ago, retain_last = 1)
CALL lakehouse.system.expire_snapshots(
table => 'order_events',
older_than => TIMESTAMP '2026-10-01 00:00:00',
retain_last => 10
);
-- 2. Consolidate manifest files (metadata-only, fast)
CALL lakehouse.system.rewrite_manifests(table => 'order_events');
-- 3. Sweep up orphan files from failed/partial writes (default window: 3 days)
-- Use dry_run first, and never set older_than shorter than your longest write.
CALL lakehouse.system.remove_orphan_files(
table => 'order_events',
older_than => TIMESTAMP '2026-10-10 00:00:00',
dry_run => true
);
Compacting without ever expiring snapshots means storage grows forever. And
remove_orphan_fileswith too aggressive a window can delete files from in-flight writes — multi-day windows are normal; hours are not.
Step 5: Keep It Healthy — the Index-Card Checklist
The industry rule of thumb is 128–512 MB per file for analytical workloads, and Iceberg’s own defaults agree: write.target-file-size-bytes is 512 MB, and the read path combines splits toward 128 MB (read.split.target-size).
Five lines, taped to your monitor:
- Measure first. Query
tbl.filesmonthly (weekly for streaming tables). Alert when average file size drops below ~128 MB or file count crosses your threshold. - Fix the writer. Set
write.target-file-size-bytes, usedistribution-mode=hashandfanout-enabled=trueon streaming writes, and size your trigger intervals to the data volume. - Compact on a cadence that matches ingestion. Streaming tables often need compaction every few hours, not nightly; batch tables can go weekly.
- Always expire snapshots and rewrite manifests after compacting. Compaction without cleanup is half a job.
- Sweep orphans quarterly with a safe
older_thanwindow — on mature streaming lakes, orphans routinely account for a quarter or more of billable storage (source).
Variations Worth Knowing
Delete-heavy tables (merge-on-read)?
Watch delete-file counts in tbl.files too — compact position deletes with rewrite_position_delete_files, not just data files.
Sub-hour freshness needs?
Don’t chase 512 MB if it means holding micro-batches open. 128–256 MB is fine for streaming tables where freshness matters more than scan efficiency.
Heavy point lookups?
Smaller files with tight clustering (sort/Z-order) beat larger unsorted ones. File size is a means to an end — the end is pruning.
✅ Conclusion
Small files are not a moral failing — they’re the natural output of a healthy streaming pipeline. The failure is letting the write path run unsupervised. Measure the file layout like you’d measure query latency, and the problem stays a maintenance task instead of becoming an incident.
If your Kafka-to-Iceberg pipeline is past the demo stage, its next chapter isn’t a new feature — it’s maintenance. Measure
tbl.files, fix the writer, compact on cadence.
💬 Let’s Discuss
Are you compacting on a schedule, or only when queries get slow? Binpack or sort — which strategy do you reach for first? And what’s the worst small-files incident you’ve seen in production? Share your setup or war stories in the comments!
Related reading
- Unlocking Data Lakes with Apache Iceberg: The Open Table Format Revolutionizing Analytics
- MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
- HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
Additional Details
- Description: A field guide to diagnosing, preventing, and fixing the small-files problem in Spark + Apache Iceberg lakehouse tables — with real numbers, working SQL, and a maintenance checklist.
MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
Your AI Agent Can’t See Your Lakehouse - Let’s Fix That
How many times this year have you pasted a table schema into a chat window so the AI could help you write a query?
In my last post, we streamed Kafka events into an Apache Iceberg lakehouse with Spark Structured Streaming. The natural follow-up in 2026: instead of querying that table myself, can I just ask an AI agent what’s in it - and get a real answer from the live catalog, not a pasted screenshot?
That is exactly what the Model Context Protocol (MCP) is for. In this post, we’ll build a small MCP server that sits on top of an Iceberg catalog and exposes it to any MCP-capable agent (Claude, IDEs, your own agents):
- What MCP actually is, in data-engineering terms
- A working MCP server over an Iceberg catalog (Python, ~60 lines)
- Wiring it into an agent host like Claude
- The security lines you should not cross
Every code sample below was run end to end while writing this post - client, server, and a real Iceberg table.
Streaming from Kafka into an Iceberg Lakehouse, End to End
From Kafka Topics to a Lakehouse Table, the Right Way
Still landing Kafka data into HDFS or S3 as raw files and batching it later?
Back in 2015, I wrote about moving Twitter streams from Kafka to HDFS with Camus. A decade later, the goal is the same - get streaming data durably into storage - but the destination has changed completely. Today, we write straight into an Apache Iceberg table and get ACID commits, schema evolution, and time travel for free.
In this post, we’ll build the whole pipeline end to end:
- A Kafka topic producing JSON events
- A Spark Structured Streaming job that consumes the topic
- Streaming writes into an Iceberg table with exactly-once semantics
- Querying and maintaining the table (compaction, snapshots)
data engineering
The Small-Files Problem: A Field Guide
The Maintenance Chapter Your Streaming Pipeline Was Missing
Your streaming job writes 2 MB Parquet files every two minutes. Six months later, your dashboard queries are ten times slower — and nobody changed the data. Sound familiar?
Yesterday, in Streaming from Kafka into an Iceberg Lakehouse, End to End, we built the whole pipeline — Kafka topic, Spark Structured Streaming, Iceberg table — and Step 6 ended with a warning: one-minute micro-batches create lots of small files — the classic streaming lakehouse problem. This post picks up exactly there. The pipeline works; the file layout is what’s rotting underneath it.
Same stack as yesterday: Kafka 4.3.1, Spark 4.2.0 (Scala 2.13), Iceberg 1.12.0, PySpark — and the same lakehouse.order_events table, partitioned by days(event_time).
In this post, we’ll:
-
See why small files hurt — with real numbers, not vibes
-
Diagnose the problem using Iceberg’s own metadata tables
-
Stop writing small files at write time
-
Fix the damage with compaction (
rewrite_data_files, done right) -
Keep the layout healthy with a checklist that fits on an index card
Step 1: Understand Why Small Files Hurt
It’s tempting to treat file count as cosmetic. It isn’t. Every file in your table is a tax paid four times: at plan time, at task-scheduling time, at request time, and at footer-read time.
Query planning. Iceberg query planning reads manifest files to decide which data files to scan — the manifest and snapshot layer I broke down in the Iceberg intro is exactly what gets stressed here. Dremio’s engineers measured this directly: the same dataset stored in 2,000 files of 256 MB each typically plans in under one second, while a table fragmented into 100,000 files pays a planning cost an order of magnitude higher. Manifests fragment too — every micro-batch commit adds manifests, and the metadata tree stops being an index and becomes a maze.
Task scheduling. Each file becomes at least one execution task. Scheduling costs roughly 1–10 milliseconds per task — trivial, until you multiply: a query against 100,000 files creates at least 100,000 tasks. Even if partition pruning eliminates 95% of them, the surviving 5,000 tasks carry 5–50 seconds of pure scheduling overhead before any data moves. On JVM engines like Spark, giant task graphs also pile on GC pressure, adding unpredictable latency.
Object-store requests. S3 charges $0.0004 per 1,000 GET requests. Reading a file takes at minimum one GET, but Parquet readers also fetch the footer and individual row groups — conservatively, two requests per file. So 100,000 files means 200,000 requests, or $0.08 per query, in storage API costs alone. A table queried 10,000 times a day burns $800 a day — $24,000 a month — on requests. Compact to 2,000 files and that drops to about $1,600 a month, a 93% reduction (source).
Footer reads. Every Parquet file carries a footer — schema, row-group metadata, column statistics. Reading it is mandatory and costs roughly the same whether the file is 1 MB or 512 MB: one to several milliseconds plus a network round trip. For a 256 MB file that’s noise; for a 1 MB file it dominates. If 95% of your read time goes to footers instead of column data, you have a severe small-files problem (source).
How fast it accumulates. Streaming engines checkpoint every 1–10 minutes, and each checkpoint commits new Parquet files — one per partition. A table with 100 partitions on 10-minute checkpoints produces 14,400 new files per day, most of them a few megabytes each (source). A table with 500,000 small files queried 50 times a day generates roughly 25 million GET requests a month. The write path that felt so elegant on day one is a liability by month six.
Step 2: Diagnose — Is This Actually Your Problem?
Don’t compact on vibes. Iceberg gives you metadata tables that answer the question directly:
-- File health at a glance: count, average size, smallest/largest
SELECT
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb,
ROUND(MIN(file_size_in_bytes) / 1048576, 2) AS min_mb,
ROUND(MAX(file_size_in_bytes) / 1048576, 2) AS max_mb,
SUM(CASE WHEN file_size_in_bytes < 134217728
THEN 1 ELSE 0 END) AS files_under_128mb
FROM lakehouse.order_events.files
WHERE content = 0; -- 0 = DATA files; 1/2 = delete files
If a large share of files sits under 128 MB — or you see thousands of files averaging a few MB — you have the problem. Next, find where it hurts most, so your first compaction pass buys the biggest win:
-- Worst partitions first: file count and avg size per partition
SELECT
partition,
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb
FROM lakehouse.order_events.files
WHERE content = 0
GROUP BY partition
ORDER BY file_count DESC
LIMIT 30;
Two more signals worth checking:
-- Are snapshots piling up from streaming appends?
SELECT snapshot_id, committed_at, operation, summary
FROM lakehouse.order_events.snapshots
ORDER BY committed_at DESC
LIMIT 20;
A wall of append snapshots with no replace (compaction) in sight confirms the diagnosis. And don’t forget the manifests metadata table — if manifest count is climbing with file count, planning overhead is compounding.
Outside SQL, the symptoms are just as telling: Spark UI jobs where a stage fans out into tens of thousands of tiny tasks, query plans that take longer than the scan itself, and — the one people notice last — your S3 GetObject/ListObjects request metrics climbing month over month while data volume stays flat.
Step 3: Fix It at Write Time
Compaction is corrective. The cheaper move is to stop writing small files in the first place. Three things worth knowing about the write path:
1. Target file size. The table property write.target-file-size-bytes defaults to 512 MB. Spark writers roll a new file when buffered data crosses this threshold — but only if a single task actually accumulates that much. A streaming micro-batch with 50 MB of data never reaches it, which is exactly how you get 2 MB files:
-- Set once at the table level (256 MB is a common streaming-table choice)
ALTER TABLE lakehouse.order_events
SET TBLPROPERTIES ('write.target-file-size-bytes' = '268435456');
You can also override it per write with the target-file-size-bytes DataFrame option.
2. Distribution mode. write.distribution-mode decides how rows are shuffled before writing: none (no shuffle — each task writes whatever it has; the small-files factory), hash (hash-distribute by partition key), or range (range-distribute by partition or sort key). For streaming writes into a partitioned table, hash is usually the right call:
(df.writeStream
.format("iceberg")
.option("checkpointLocation", checkpoint_path)
.option("distribution-mode", "hash") # shuffle by partition key before writing
.option("fanout-enabled", "true") # keep partition writers open across batches
.toTable("lakehouse.order_events"))
3. The fanout writer. Streaming data arrives out of order across partitions. Without fanout, every partition switch inside a task closes the current file and opens a new one — file rollover churn. write.spark.fanout.enabled (default false) keeps multiple partition writers open simultaneously, at the cost of more memory. For streaming jobs, enable it.
We used
fanout-enabled: truein the streaming job from yesterday’s post for exactly this reason — without it, out-of-order events across days would have multiplied our file count from the very first batch.
Two more write-path habits that pay off:
repartitionbefore batch writes. Line writers up with the partition spec so each task writes fewer, larger files:
from pyspark.sql.functions import to_date
def write_batch(batch_df, batch_id):
(batch_df
.repartition(to_date("event_time")) # align writers with days(event_time)
.writeTo("lakehouse.order_events")
.option("fanout-enabled", "true")
.append())
(query.writeStream
.foreachBatch(write_batch)
.option("checkpointLocation", checkpoint_path)
.trigger(availableNow=True)
.start())
- Trigger intervals. The one-minute trigger we used yesterday is great for freshness demos and terrible for file layout — on a low-volume topic it guarantees tiny files. Prefer
Trigger.AvailableNowor multi-minute processing-time triggers, and coalesce inside the batch function as above.
None of this eliminates compaction entirely — streaming tables will always drift — but it slows the drift from days to weeks.
Step 4: Compact What’s Already There
Yesterday’s post showed the one-line version — CALL lakehouse.system.rewrite_data_files(table => 'order_events'). Here’s the full picture. Iceberg’s compaction primitive rewrites selected data files into fewer, larger ones and commits the result as a new snapshot — fully online, no downtime; readers keep working against the old snapshot until the new one commits.
Binpack (default): fast and dumb, in a good way. It groups small files by partition and concatenates them up to the target size. No shuffle, no reordering — the cheapest possible rewrite:
-- Compact only the files that need it: small files in recent partitions
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'binpack',
where => 'event_time >= current_timestamp() - INTERVAL 7 DAYS',
options => map(
'target-file-size-bytes', '268435456', -- 256 MB
'min-input-files', '5' -- skip groups already healthy
)
);
Sort: slower, but fixes layout too. Binpack doesn’t touch data ordering. If your queries filter on event_time and customer_id but files were written in arrival order, column statistics are weak and pruning suffers. The sort strategy globally sorts before allocating files to tasks, tightening min/max ranges per file:
-- Merge + cluster by the columns your queries actually filter on
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'sort',
sort_order => 'event_time ASC NULLS LAST, customer_id ASC NULLS LAST',
options => map(
'target-file-size-bytes', '268435456',
'partial-progress.enabled', 'true',
'max-concurrent-file-group-rewrites', '5'
)
);
-- For multi-column filters with no single dominant column, Z-order instead:
-- sort_order => 'zorder(event_time, customer_id)'
(Iceberg 1.12 also adds Hilbert-curve clustering as a newer alternative to Z-ordering — worth evaluating once your engine supports it.)
The options that matter in production:
| Option | Default | What it does |
|---|---|---|
target-file-size-bytes |
table’s write.target-file-size-bytes (512 MB) |
Output file size target |
min-input-files |
5 | File groups with fewer files are skipped |
min-file-size-bytes |
75% of target | Files below this are always candidates |
max-file-size-bytes |
180% of target | Files above this are always candidates |
max-concurrent-file-group-rewrites |
5 | Parallelism of the rewrite; raise for large tables |
max-file-group-size-bytes |
100 GB | Splits the job into file groups |
partial-progress.enabled |
false | Commit file groups as they finish instead of one big commit |
partial-progress.max-commits |
10 | Cap on commits when partial progress is on |
rewrite-all |
false | Force-rewrite everything, ignoring the filters above |
rewrite-job-order |
none | Order groups: bytes-asc, bytes-desc, files-asc, files-desc |
Two of these deserve emphasis. partial-progress.enabled=true is the difference between losing hours of rewrite work on a mid-job failure and keeping what finished — always turn it on for large tables. And max-concurrent-file-group-rewrites is the knob real teams reach for at scale: Ancestry’s data team raised it from the default 5 to 100 to compact a 100-billion-row Iceberg table, because the default couldn’t keep up with their update frequency.
The follow-ups people forget. Compaction creates a new snapshot but leaves the old files on disk — old snapshots pin them. Run the cleanup chain, in this order:
-- 1. Expire old snapshots so their files can actually be deleted
-- (defaults: older_than = 5 days ago, retain_last = 1)
CALL lakehouse.system.expire_snapshots(
table => 'order_events',
older_than => TIMESTAMP '2026-10-01 00:00:00',
retain_last => 10
);
-- 2. Consolidate manifest files (metadata-only, fast)
CALL lakehouse.system.rewrite_manifests(table => 'order_events');
-- 3. Sweep up orphan files from failed/partial writes (default window: 3 days)
-- Use dry_run first, and never set older_than shorter than your longest write.
CALL lakehouse.system.remove_orphan_files(
table => 'order_events',
older_than => TIMESTAMP '2026-10-10 00:00:00',
dry_run => true
);
Compacting without ever expiring snapshots means storage grows forever. And
remove_orphan_fileswith too aggressive a window can delete files from in-flight writes — multi-day windows are normal; hours are not.
Step 5: Keep It Healthy — the Index-Card Checklist
The industry rule of thumb is 128–512 MB per file for analytical workloads, and Iceberg’s own defaults agree: write.target-file-size-bytes is 512 MB, and the read path combines splits toward 128 MB (read.split.target-size).
Five lines, taped to your monitor:
- Measure first. Query
tbl.filesmonthly (weekly for streaming tables). Alert when average file size drops below ~128 MB or file count crosses your threshold. - Fix the writer. Set
write.target-file-size-bytes, usedistribution-mode=hashandfanout-enabled=trueon streaming writes, and size your trigger intervals to the data volume. - Compact on a cadence that matches ingestion. Streaming tables often need compaction every few hours, not nightly; batch tables can go weekly.
- Always expire snapshots and rewrite manifests after compacting. Compaction without cleanup is half a job.
- Sweep orphans quarterly with a safe
older_thanwindow — on mature streaming lakes, orphans routinely account for a quarter or more of billable storage (source).
Variations Worth Knowing
Delete-heavy tables (merge-on-read)?
Watch delete-file counts in tbl.files too — compact position deletes with rewrite_position_delete_files, not just data files.
Sub-hour freshness needs?
Don’t chase 512 MB if it means holding micro-batches open. 128–256 MB is fine for streaming tables where freshness matters more than scan efficiency.
Heavy point lookups?
Smaller files with tight clustering (sort/Z-order) beat larger unsorted ones. File size is a means to an end — the end is pruning.
✅ Conclusion
Small files are not a moral failing — they’re the natural output of a healthy streaming pipeline. The failure is letting the write path run unsupervised. Measure the file layout like you’d measure query latency, and the problem stays a maintenance task instead of becoming an incident.
If your Kafka-to-Iceberg pipeline is past the demo stage, its next chapter isn’t a new feature — it’s maintenance. Measure
tbl.files, fix the writer, compact on cadence.
💬 Let’s Discuss
Are you compacting on a schedule, or only when queries get slow? Binpack or sort — which strategy do you reach for first? And what’s the worst small-files incident you’ve seen in production? Share your setup or war stories in the comments!
Related reading
- Unlocking Data Lakes with Apache Iceberg: The Open Table Format Revolutionizing Analytics
- MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
- HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
Additional Details
- Description: A field guide to diagnosing, preventing, and fixing the small-files problem in Spark + Apache Iceberg lakehouse tables — with real numbers, working SQL, and a maintenance checklist.
MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
Your AI Agent Can’t See Your Lakehouse - Let’s Fix That
How many times this year have you pasted a table schema into a chat window so the AI could help you write a query?
In my last post, we streamed Kafka events into an Apache Iceberg lakehouse with Spark Structured Streaming. The natural follow-up in 2026: instead of querying that table myself, can I just ask an AI agent what’s in it - and get a real answer from the live catalog, not a pasted screenshot?
That is exactly what the Model Context Protocol (MCP) is for. In this post, we’ll build a small MCP server that sits on top of an Iceberg catalog and exposes it to any MCP-capable agent (Claude, IDEs, your own agents):
- What MCP actually is, in data-engineering terms
- A working MCP server over an Iceberg catalog (Python, ~60 lines)
- Wiring it into an agent host like Claude
- The security lines you should not cross
Every code sample below was run end to end while writing this post - client, server, and a real Iceberg table.
Streaming from Kafka into an Iceberg Lakehouse, End to End
From Kafka Topics to a Lakehouse Table, the Right Way
Still landing Kafka data into HDFS or S3 as raw files and batching it later?
Back in 2015, I wrote about moving Twitter streams from Kafka to HDFS with Camus. A decade later, the goal is the same - get streaming data durably into storage - but the destination has changed completely. Today, we write straight into an Apache Iceberg table and get ACID commits, schema evolution, and time travel for free.
In this post, we’ll build the whole pipeline end to end:
- A Kafka topic producing JSON events
- A Spark Structured Streaming job that consumes the topic
- Streaming writes into an Iceberg table with exactly-once semantics
- Querying and maintaining the table (compaction, snapshots)
Java Interview
Java : What does finalize do and How?
finalize method in the Object class is often a point of discussion regarding whether it should be used or not. Below are some important pointers on the finalize method:
String Interning – What ,Why and When ?
What is String Interning
String Interning is a method of storing only one copy of each distinct String Value, which must be immutable.
Pig
How-To : Use HCatalog with Pig
This post is a step by step guide on running HCatalog and using HCatalog with Apache Pig:
Hadoop : Getting Started with Pig
What is Apache Pig?
Apache Pig is a high level scripting language that is used with Apache Hadoop. It enables data analysts to write complex data transformations without knowing Java. Its simple SQL-like scripting language is called Pig Latin, and appeals to developers already familiar with scripting languages and SQL. Pig Scripts are converted into MapReduce Jobs which run on data stored in HDFS (refer to the diagram below).
metastore
How-To : Connect HiveServer2 service with JDBC Client ?
HiveServer2 (HS2) is a server interface that enables remote clients to execute queries against Hive and retrieve the results. The current implementation, based on Thrift RPC, is an improved version of HiveServer and supports multi-client concurrency and authentication. It is designed to provide better support for open API clients like JDBC and ODBC.
How-To : Configure MySQL Metastore for Hive ?
Hive by default comes with Derby as its metastore storage, which is suited only for testing purposes and in most of the production scenarios it is recommended to use MySQL as a metastore. This is a step by step guide on How to Configure MySQL Metastore for Hive in place of Derby Metastore (Default).
hbase
How-to : Write a CoProcessor in HBase
Coprocessor is a mechanism which helps to move computations closer to the data in HBase. It is like a Mapreduce framework to distribute tasks across the cluster.
Hive Strict Mode
![Sort By vs Order By vs Group By vs Cluster By in Hive]/assets/uploads/2015/01/images.jpg)
What is Hive Strict Mode?
Hive Strict Mode (hive.mapred.mode=strict) enables Hive to restrict certain performance intensive operations. Such as:
hcatalog
What is Apache HCatalog ?
A developer-friendly introduction to Apache HCatalog, its architecture, features, and how it fits into the Hadoop ecosystem.
How-To : Use HCatalog with Pig
This post is a step by step guide on running HCatalog and using HCatalog with Apache Pig:
kafka
How-To : Integrate Kafka with HDFS using Camus (Twitter Stream Example)
A step-by-step guide to building a Kafka-to-HDFS data pipeline using Camus and a Twitter stream. This guide is aimed at developers looking for a practical, detailed walkthrough.
How-To : Write a Kafka Producer using Twitter Stream ( Twitter HBC Client)
A step-by-step guide to building a Kafka producer that streams live tweets using Twitter’s Hosebird Client (HBC) and publishes them to a Kafka topic. This is a practical, developer-focused walkthrough with code, configuration, and troubleshooting tips.
java8
How to Configure Spark Application ( Scala and Java 8 Version with Maven ) in Eclipse.
A step-by-step, developer-friendly guide to setting up Apache Spark applications in Eclipse/Scala IDE using Maven, with both Java and Scala examples.
Hashmap Performance Improvements in Java 8
A developer-focused look at how Java 8 improved the performance of HashMap under high-collision scenarios, with code examples and practical explanations.
scala
Dataframe Operations in Spark using Scala
A comprehensive, developer-friendly guide to common DataFrame operations in Apache Spark using Scala, with code examples and explanations for each join type.
How to Configure Spark Application ( Scala and Java 8 Version with Maven ) in Eclipse.
A step-by-step, developer-friendly guide to setting up Apache Spark applications in Eclipse/Scala IDE using Maven, with both Java and Scala examples.
cloud
Top 10 Hadoop Shell Commands to manage HDFS
So you already know what Hadoop is? Why it is used for? What problems you can solve with it? And you want to know how you can deal with files on HDFS? Don’t worry, you are in the right place.
high availability
Top 10 Hadoop Shell Commands to manage HDFS
So you already know what Hadoop is? Why it is used for? What problems you can solve with it? And you want to know how you can deal with files on HDFS? Don’t worry, you are in the right place.
scalable
Top 10 Hadoop Shell Commands to manage HDFS
So you already know what Hadoop is? Why it is used for? What problems you can solve with it? And you want to know how you can deal with files on HDFS? Don’t worry, you are in the right place.
storage
Top 10 Hadoop Shell Commands to manage HDFS
So you already know what Hadoop is? Why it is used for? What problems you can solve with it? And you want to know how you can deal with files on HDFS? Don’t worry, you are in the right place.
apache
SOAP Webservices Using Apache CXF : Adding Custom Object as Header in Outgoing Requests
What is Apache CXF?
Apache CXF is an open source services framework. CXF helps you build and develop services using frontend programming APIs, like JAX-WS and JAX-RS. These services can speak a variety of protocols such as SOAP, XML/HTTP, RESTful HTTP, or CORBA and work over a variety of transports such as HTTP, JMS etc.
cxf
SOAP Webservices Using Apache CXF : Adding Custom Object as Header in Outgoing Requests
What is Apache CXF?
Apache CXF is an open source services framework. CXF helps you build and develop services using frontend programming APIs, like JAX-WS and JAX-RS. These services can speak a variety of protocols such as SOAP, XML/HTTP, RESTful HTTP, or CORBA and work over a variety of transports such as HTTP, JMS etc.
soap
SOAP Webservices Using Apache CXF : Adding Custom Object as Header in Outgoing Requests
What is Apache CXF?
Apache CXF is an open source services framework. CXF helps you build and develop services using frontend programming APIs, like JAX-WS and JAX-RS. These services can speak a variety of protocols such as SOAP, XML/HTTP, RESTful HTTP, or CORBA and work over a variety of transports such as HTTP, JMS etc.
webservice
SOAP Webservices Using Apache CXF : Adding Custom Object as Header in Outgoing Requests
What is Apache CXF?
Apache CXF is an open source services framework. CXF helps you build and develop services using frontend programming APIs, like JAX-WS and JAX-RS. These services can speak a variety of protocols such as SOAP, XML/HTTP, RESTful HTTP, or CORBA and work over a variety of transports such as HTTP, JMS etc.
Intern
String Interning – What ,Why and When ?
What is String Interning
String Interning is a method of storing only one copy of each distinct String Value, which must be immutable.
String
String Interning – What ,Why and When ?
What is String Interning
String Interning is a method of storing only one copy of each distinct String Value, which must be immutable.
Technology
String Interning – What ,Why and When ?
What is String Interning
String Interning is a method of storing only one copy of each distinct String Value, which must be immutable.
Pig Latin
Hadoop : Getting Started with Pig
What is Apache Pig?
Apache Pig is a high level scripting language that is used with Apache Hadoop. It enables data analysts to write complex data transformations without knowing Java. Its simple SQL-like scripting language is called Pig Latin, and appeals to developers already familiar with scripting languages and SQL. Pig Scripts are converted into MapReduce Jobs which run on data stored in HDFS (refer to the diagram below).
Centralised Logging
How-To : Setup Realtime Alalytics over Logs with ELK Stack : Elasticsearch, Logstash, Kibana?
Once we know something, we find it hard to imagine what it was like not to know it.
— Chip & Dan Heath, Authors of Made to Stick, Switch
What is the ELK stack?
The ELK stack consists of open source tools ElasticSearch, Logstash, and Kibana. These three provide a fully working real-time data analytics tool for getting wonderful information sitting on your data.
ElasticSearch
How-To : Setup Realtime Alalytics over Logs with ELK Stack : Elasticsearch, Logstash, Kibana?
Once we know something, we find it hard to imagine what it was like not to know it.
— Chip & Dan Heath, Authors of Made to Stick, Switch
What is the ELK stack?
The ELK stack consists of open source tools ElasticSearch, Logstash, and Kibana. These three provide a fully working real-time data analytics tool for getting wonderful information sitting on your data.
ELK
How-To : Setup Realtime Alalytics over Logs with ELK Stack : Elasticsearch, Logstash, Kibana?
Once we know something, we find it hard to imagine what it was like not to know it.
— Chip & Dan Heath, Authors of Made to Stick, Switch
What is the ELK stack?
The ELK stack consists of open source tools ElasticSearch, Logstash, and Kibana. These three provide a fully working real-time data analytics tool for getting wonderful information sitting on your data.
Kibana
How-To : Setup Realtime Alalytics over Logs with ELK Stack : Elasticsearch, Logstash, Kibana?
Once we know something, we find it hard to imagine what it was like not to know it.
— Chip & Dan Heath, Authors of Made to Stick, Switch
What is the ELK stack?
The ELK stack consists of open source tools ElasticSearch, Logstash, and Kibana. These three provide a fully working real-time data analytics tool for getting wonderful information sitting on your data.
LogStash
How-To : Setup Realtime Alalytics over Logs with ELK Stack : Elasticsearch, Logstash, Kibana?
Once we know something, we find it hard to imagine what it was like not to know it.
— Chip & Dan Heath, Authors of Made to Stick, Switch
What is the ELK stack?
The ELK stack consists of open source tools ElasticSearch, Logstash, and Kibana. These three provide a fully working real-time data analytics tool for getting wonderful information sitting on your data.
Api Docs
How-To : Generate Restful API Documentation with Swagger ?
Swagger is a specification and complete framework implementation for describing, producing, consuming, and visualizing RESTful web services. The goal of Swagger is to enable client and documentation systems to update at the same pace as the server. The documentation of methods, parameters, and models are tightly integrated into the server code, allowing APIs to always stay in sync.
documentation
How-To : Generate Restful API Documentation with Swagger ?
Swagger is a specification and complete framework implementation for describing, producing, consuming, and visualizing RESTful web services. The goal of Swagger is to enable client and documentation systems to update at the same pace as the server. The documentation of methods, parameters, and models are tightly integrated into the server code, allowing APIs to always stay in sync.
REST API
How-To : Generate Restful API Documentation with Swagger ?
Swagger is a specification and complete framework implementation for describing, producing, consuming, and visualizing RESTful web services. The goal of Swagger is to enable client and documentation systems to update at the same pace as the server. The documentation of methods, parameters, and models are tightly integrated into the server code, allowing APIs to always stay in sync.
Spring
How-To : Generate Restful API Documentation with Swagger ?
Swagger is a specification and complete framework implementation for describing, producing, consuming, and visualizing RESTful web services. The goal of Swagger is to enable client and documentation systems to update at the same pace as the server. The documentation of methods, parameters, and models are tightly integrated into the server code, allowing APIs to always stay in sync.
Spring Boot
How-To : Generate Restful API Documentation with Swagger ?
Swagger is a specification and complete framework implementation for describing, producing, consuming, and visualizing RESTful web services. The goal of Swagger is to enable client and documentation systems to update at the same pace as the server. The documentation of methods, parameters, and models are tightly integrated into the server code, allowing APIs to always stay in sync.
Swagger
How-To : Generate Restful API Documentation with Swagger ?
Swagger is a specification and complete framework implementation for describing, producing, consuming, and visualizing RESTful web services. The goal of Swagger is to enable client and documentation systems to update at the same pace as the server. The documentation of methods, parameters, and models are tightly integrated into the server code, allowing APIs to always stay in sync.
Finalize
Java : What does finalize do and How?
finalize method in the Object class is often a point of discussion regarding whether it should be used or not. Below are some important pointers on the finalize method:
Garbage collection
Java : What does finalize do and How?
finalize method in the Object class is often a point of discussion regarding whether it should be used or not. Below are some important pointers on the finalize method:
JVM
Java : What does finalize do and How?
finalize method in the Object class is often a point of discussion regarding whether it should be used or not. Below are some important pointers on the finalize method:
Poodle
What is POODLE vulnerability and How does it affect you ?
What is POODLE?
It stands for “Padding Oracle On Downgraded Legacy Encryption.” This means a protocol downgrade that allows exploits on an outdated form of encryption. It was first explained in Google Security Advisory.
ssl
What is POODLE vulnerability and How does it affect you ?
What is POODLE?
It stands for “Padding Oracle On Downgraded Legacy Encryption.” This means a protocol downgrade that allows exploits on an outdated form of encryption. It was first explained in Google Security Advisory.
tls
What is POODLE vulnerability and How does it affect you ?
What is POODLE?
It stands for “Padding Oracle On Downgraded Legacy Encryption.” This means a protocol downgrade that allows exploits on an outdated form of encryption. It was first explained in Google Security Advisory.
vulnerability
What is POODLE vulnerability and How does it affect you ?
What is POODLE?
It stands for “Padding Oracle On Downgraded Legacy Encryption.” This means a protocol downgrade that allows exploits on an outdated form of encryption. It was first explained in Google Security Advisory.
datawarehouse
How-To : Configure MySQL Metastore for Hive ?
Hive by default comes with Derby as its metastore storage, which is suited only for testing purposes and in most of the production scenarios it is recommended to use MySQL as a metastore. This is a step by step guide on How to Configure MySQL Metastore for Hive in place of Derby Metastore (Default).
mysql
How-To : Configure MySQL Metastore for Hive ?
Hive by default comes with Derby as its metastore storage, which is suited only for testing purposes and in most of the production scenarios it is recommended to use MySQL as a metastore. This is a step by step guide on How to Configure MySQL Metastore for Hive in place of Derby Metastore (Default).
hadoop interview
How-To : Connect HiveServer2 service with JDBC Client ?
HiveServer2 (HS2) is a server interface that enables remote clients to execute queries against Hive and retrieve the results. The current implementation, based on Thrift RPC, is an improved version of HiveServer and supports multi-client concurrency and authentication. It is designed to provide better support for open API clients like JDBC and ODBC.
hiveserver2
How-To : Connect HiveServer2 service with JDBC Client ?
HiveServer2 (HS2) is a server interface that enables remote clients to execute queries against Hive and retrieve the results. The current implementation, based on Thrift RPC, is an improved version of HiveServer and supports multi-client concurrency and authentication. It is designed to provide better support for open API clients like JDBC and ODBC.
jdbc
How-To : Connect HiveServer2 service with JDBC Client ?
HiveServer2 (HS2) is a server interface that enables remote clients to execute queries against Hive and retrieve the results. The current implementation, based on Thrift RPC, is an improved version of HiveServer and supports multi-client concurrency and authentication. It is designed to provide better support for open API clients like JDBC and ODBC.
cluster by
Hive : SORT BY vs ORDER BY vs DISTRIBUTE BY vs CLUSTER BY
In Apache Hive HQL, you can decide to order or sort your data differently based on ordering and distribution requirement. In this post we will look at how SORT BY, ORDER BY, DISTRIBUTE BY and CLUSTER BY behave differently in Hive. Let’s get started -
distribute by
Hive : SORT BY vs ORDER BY vs DISTRIBUTE BY vs CLUSTER BY
In Apache Hive HQL, you can decide to order or sort your data differently based on ordering and distribution requirement. In this post we will look at how SORT BY, ORDER BY, DISTRIBUTE BY and CLUSTER BY behave differently in Hive. Let’s get started -
hive interview
Hive : SORT BY vs ORDER BY vs DISTRIBUTE BY vs CLUSTER BY
In Apache Hive HQL, you can decide to order or sort your data differently based on ordering and distribution requirement. In this post we will look at how SORT BY, ORDER BY, DISTRIBUTE BY and CLUSTER BY behave differently in Hive. Let’s get started -
Hiveql
Hive : SORT BY vs ORDER BY vs DISTRIBUTE BY vs CLUSTER BY
In Apache Hive HQL, you can decide to order or sort your data differently based on ordering and distribution requirement. In this post we will look at how SORT BY, ORDER BY, DISTRIBUTE BY and CLUSTER BY behave differently in Hive. Let’s get started -
order by
Hive : SORT BY vs ORDER BY vs DISTRIBUTE BY vs CLUSTER BY
In Apache Hive HQL, you can decide to order or sort your data differently based on ordering and distribution requirement. In this post we will look at how SORT BY, ORDER BY, DISTRIBUTE BY and CLUSTER BY behave differently in Hive. Let’s get started -
sort by
Hive : SORT BY vs ORDER BY vs DISTRIBUTE BY vs CLUSTER BY
In Apache Hive HQL, you can decide to order or sort your data differently based on ordering and distribution requirement. In this post we will look at how SORT BY, ORDER BY, DISTRIBUTE BY and CLUSTER BY behave differently in Hive. Let’s get started -
spark sql
Hive : SORT BY vs ORDER BY vs DISTRIBUTE BY vs CLUSTER BY
In Apache Hive HQL, you can decide to order or sort your data differently based on ordering and distribution requirement. In this post we will look at how SORT BY, ORDER BY, DISTRIBUTE BY and CLUSTER BY behave differently in Hive. Let’s get started -
coprocessor
How-to : Write a CoProcessor in HBase
Coprocessor is a mechanism which helps to move computations closer to the data in HBase. It is like a Mapreduce framework to distribute tasks across the cluster.
storm
How-To : Write a Kafka Producer using Twitter Stream ( Twitter HBC Client)
A step-by-step guide to building a Kafka producer that streams live tweets using Twitter’s Hosebird Client (HBC) and publishes them to a Kafka topic. This is a practical, developer-focused walkthrough with code, configuration, and troubleshooting tips.
How-To : Write a Kafka Producer using Twitter Stream ( Twitter HBC Client)
A step-by-step guide to building a Kafka producer that streams live tweets using Twitter’s Hosebird Client (HBC) and publishes them to a Kafka topic. This is a practical, developer-focused walkthrough with code, configuration, and troubleshooting tips.
camus
How-To : Integrate Kafka with HDFS using Camus (Twitter Stream Example)
A step-by-step guide to building a Kafka-to-HDFS data pipeline using Camus and a Twitter stream. This guide is aimed at developers looking for a practical, detailed walkthrough.
kafka-hdfs
How-To : Integrate Kafka with HDFS using Camus (Twitter Stream Example)
A step-by-step guide to building a Kafka-to-HDFS data pipeline using Camus and a Twitter stream. This guide is aimed at developers looking for a practical, detailed walkthrough.
How-To : Integrate Kafka with HDFS using Camus (Twitter Stream Example)
A step-by-step guide to building a Kafka-to-HDFS data pipeline using Camus and a Twitter stream. This guide is aimed at developers looking for a practical, detailed walkthrough.
bg
Unix Job Control Commands – bg, fg, Ctrl+Z,jobs
A practical guide for developers and data engineers to manage long-running jobs in Unix, especially useful when working with Hadoop or other big data tools.
fg
Unix Job Control Commands – bg, fg, Ctrl+Z,jobs
A practical guide for developers and data engineers to manage long-running jobs in Unix, especially useful when working with Hadoop or other big data tools.
jobs
Unix Job Control Commands – bg, fg, Ctrl+Z,jobs
A practical guide for developers and data engineers to manage long-running jobs in Unix, especially useful when working with Hadoop or other big data tools.
process management
Unix Job Control Commands – bg, fg, Ctrl+Z,jobs
A practical guide for developers and data engineers to manage long-running jobs in Unix, especially useful when working with Hadoop or other big data tools.
unix
Unix Job Control Commands – bg, fg, Ctrl+Z,jobs
A practical guide for developers and data engineers to manage long-running jobs in Unix, especially useful when working with Hadoop or other big data tools.
collection
Hashmap Performance Improvements in Java 8
A developer-focused look at how Java 8 improved the performance of HashMap under high-collision scenarios, with code examples and practical explanations.
hashmap
Hashmap Performance Improvements in Java 8
A developer-focused look at how Java 8 improved the performance of HashMap under high-collision scenarios, with code examples and practical explanations.
fast
What is RDD in Spark ? and Why do we need it ?
A developer-friendly guide to understanding Resilient Distributed Datasets (RDDs) in Apache Spark, their properties, and why they are fundamental for fast, fault-tolerant distributed computing.
rdd
What is RDD in Spark ? and Why do we need it ?
A developer-friendly guide to understanding Resilient Distributed Datasets (RDDs) in Apache Spark, their properties, and why they are fundamental for fast, fault-tolerant distributed computing.
ELK Stack
My Book on ELK Stack : Learning ELK Stack
Learning ELK Stack
I am writing this post to announce the general availability of my book on ELK stack titled “ Learning ELK Stack “ with PacktPub publications. Book aims to provide individuals/technologists who seek to implement their own log and data analytics solutions using opensource stack of Elasticsearch, Logstash and Kibana popularly known as ELK stack.
Learning Elk Stack
My Book on ELK Stack : Learning ELK Stack
Learning ELK Stack
I am writing this post to announce the general availability of my book on ELK stack titled “ Learning ELK Stack “ with PacktPub publications. Book aims to provide individuals/technologists who seek to implement their own log and data analytics solutions using opensource stack of Elasticsearch, Logstash and Kibana popularly known as ELK stack.
PacktPub
My Book on ELK Stack : Learning ELK Stack
Learning ELK Stack
I am writing this post to announce the general availability of my book on ELK stack titled “ Learning ELK Stack “ with PacktPub publications. Book aims to provide individuals/technologists who seek to implement their own log and data analytics solutions using opensource stack of Elasticsearch, Logstash and Kibana popularly known as ELK stack.
Saurabh Chhajed
My Book on ELK Stack : Learning ELK Stack
Learning ELK Stack
I am writing this post to announce the general availability of my book on ELK stack titled “ Learning ELK Stack “ with PacktPub publications. Book aims to provide individuals/technologists who seek to implement their own log and data analytics solutions using opensource stack of Elasticsearch, Logstash and Kibana popularly known as ELK stack.
eclipse
How to Configure Spark Application ( Scala and Java 8 Version with Maven ) in Eclipse.
A step-by-step, developer-friendly guide to setting up Apache Spark applications in Eclipse/Scala IDE using Maven, with both Java and Scala examples.
lambda
How to Configure Spark Application ( Scala and Java 8 Version with Maven ) in Eclipse.
A step-by-step, developer-friendly guide to setting up Apache Spark applications in Eclipse/Scala IDE using Maven, with both Java and Scala examples.
maven
How to Configure Spark Application ( Scala and Java 8 Version with Maven ) in Eclipse.
A step-by-step, developer-friendly guide to setting up Apache Spark applications in Eclipse/Scala IDE using Maven, with both Java and Scala examples.
mapereduce
How to Use MultiThreadedMapper in MapReduce
A practical, developer-focused guide to using Hadoop’s MultithreadedMapper for parallelizing map tasks and improving performance in CPU-bound jobs.
MultithreadedMapper
How to Use MultiThreadedMapper in MapReduce
A practical, developer-focused guide to using Hadoop’s MultithreadedMapper for parallelizing map tasks and improving performance in CPU-bound jobs.
spark interview
What does Skipped Stage means in Spark WebUI ?
Skipped Stages in Spark UI
You must have come across various scenarios where you see a DAG like below, where you see a few stages shows greyed out with a text (skipped) after the stage name. What does this mean? Did Spark ignore one of your stage due to an error? or this is due to something else? Well, it’s actually a good thing. It means that particular stage in the lineage DAG doesn’t need to be re-evaluated as its already evaluated and cached. This will save computation time for that stage. If you want to see what data frame for that stage was stored in the cache, you can check in Storage tab in Spark UI.
spark on windows
Spark – How to Run Spark Applications on Windows
A step-by-step, developer-friendly guide to running Apache Spark applications on Windows, including configuration, environment setup, and troubleshooting tips.
winutils.exe
Spark – How to Run Spark Applications on Windows
A step-by-step, developer-friendly guide to running Apache Spark applications on Windows, including configuration, environment setup, and troubleshooting tips.
git
Git : How to Split Sub-Directory to Separate Repository
If you regret putting that git sub-directory inside a git repository and thinking about moving it out of the current repository to its own repository, you have come to the right place!
repository
Git : How to Split Sub-Directory to Separate Repository
If you regret putting that git sub-directory inside a git repository and thinking about moving it out of the current repository to its own repository, you have come to the right place!
split
Git : How to Split Sub-Directory to Separate Repository
If you regret putting that git sub-directory inside a git repository and thinking about moving it out of the current repository to its own repository, you have come to the right place!
catboost
How to Train and Score Catboost Model on Spark
A practical, developer-focused guide to distributed training and inference with CatBoost on Apache Spark, including code examples and best practices.
HDFS
HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
With the rise of cloud-native data architectures and serverless computing, data engineers face a crucial decision when designing big data pipelines: HDFS or Object Storage? This decision impacts performance, scalability, fault tolerance, and cost. In this post, we’ll conduct a deep technical analysis of both storage systems and explore how they align with modern big data processing patterns on the cloud.
Object Storage
HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
With the rise of cloud-native data architectures and serverless computing, data engineers face a crucial decision when designing big data pipelines: HDFS or Object Storage? This decision impacts performance, scalability, fault tolerance, and cost. In this post, we’ll conduct a deep technical analysis of both storage systems and explore how they align with modern big data processing patterns on the cloud.
Spark
HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
With the rise of cloud-native data architectures and serverless computing, data engineers face a crucial decision when designing big data pipelines: HDFS or Object Storage? This decision impacts performance, scalability, fault tolerance, and cost. In this post, we’ll conduct a deep technical analysis of both storage systems and explore how they align with modern big data processing patterns on the cloud.
Big Data
HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
With the rise of cloud-native data architectures and serverless computing, data engineers face a crucial decision when designing big data pipelines: HDFS or Object Storage? This decision impacts performance, scalability, fault tolerance, and cost. In this post, we’ll conduct a deep technical analysis of both storage systems and explore how they align with modern big data processing patterns on the cloud.
Cloud
HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
With the rise of cloud-native data architectures and serverless computing, data engineers face a crucial decision when designing big data pipelines: HDFS or Object Storage? This decision impacts performance, scalability, fault tolerance, and cost. In this post, we’ll conduct a deep technical analysis of both storage systems and explore how they align with modern big data processing patterns on the cloud.
Data Engineering
HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
With the rise of cloud-native data architectures and serverless computing, data engineers face a crucial decision when designing big data pipelines: HDFS or Object Storage? This decision impacts performance, scalability, fault tolerance, and cost. In this post, we’ll conduct a deep technical analysis of both storage systems and explore how they align with modern big data processing patterns on the cloud.
data lakes
Unlocking Data Lakes with Apache Iceberg: The Open Table Format Revolutionizing Analytics
Why Apache Iceberg is the Game-Changer Your Data Lake Needs
Still relying on traditional data lakes with unreliable reads, lack of ACID guarantees, and painful schema evolution?
Welcome to Apache Iceberg—an open table format purpose-built for modern, petabyte-scale analytics. With native support for engines like Apache Spark, Trino, Presto, Hive, and Flink, Iceberg solves the biggest challenges in big data processing.
In this post, we’ll break down:
- What Apache Iceberg is and why it matters
- How its architecture works (with a visual!)
- Key features that make it future-ready
- Real-world use cases
- A call to action for your next data platform decision
open table format
Unlocking Data Lakes with Apache Iceberg: The Open Table Format Revolutionizing Analytics
Why Apache Iceberg is the Game-Changer Your Data Lake Needs
Still relying on traditional data lakes with unreliable reads, lack of ACID guarantees, and painful schema evolution?
Welcome to Apache Iceberg—an open table format purpose-built for modern, petabyte-scale analytics. With native support for engines like Apache Spark, Trino, Presto, Hive, and Flink, Iceberg solves the biggest challenges in big data processing.
In this post, we’ll break down:
- What Apache Iceberg is and why it matters
- How its architecture works (with a visual!)
- Key features that make it future-ready
- Real-world use cases
- A call to action for your next data platform decision
data architecture
Unlocking Data Lakes with Apache Iceberg: The Open Table Format Revolutionizing Analytics
Why Apache Iceberg is the Game-Changer Your Data Lake Needs
Still relying on traditional data lakes with unreliable reads, lack of ACID guarantees, and painful schema evolution?
Welcome to Apache Iceberg—an open table format purpose-built for modern, petabyte-scale analytics. With native support for engines like Apache Spark, Trino, Presto, Hive, and Flink, Iceberg solves the biggest challenges in big data processing.
In this post, we’ll break down:
- What Apache Iceberg is and why it matters
- How its architecture works (with a visual!)
- Key features that make it future-ready
- Real-world use cases
- A call to action for your next data platform decision
Apache Kafka
Streaming from Kafka into an Iceberg Lakehouse, End to End
From Kafka Topics to a Lakehouse Table, the Right Way
Still landing Kafka data into HDFS or S3 as raw files and batching it later?
Back in 2015, I wrote about moving Twitter streams from Kafka to HDFS with Camus. A decade later, the goal is the same - get streaming data durably into storage - but the destination has changed completely. Today, we write straight into an Apache Iceberg table and get ACID commits, schema evolution, and time travel for free.
In this post, we’ll build the whole pipeline end to end:
- A Kafka topic producing JSON events
- A Spark Structured Streaming job that consumes the topic
- Streaming writes into an Iceberg table with exactly-once semantics
- Querying and maintaining the table (compaction, snapshots)
Spark Structured Streaming
Streaming from Kafka into an Iceberg Lakehouse, End to End
From Kafka Topics to a Lakehouse Table, the Right Way
Still landing Kafka data into HDFS or S3 as raw files and batching it later?
Back in 2015, I wrote about moving Twitter streams from Kafka to HDFS with Camus. A decade later, the goal is the same - get streaming data durably into storage - but the destination has changed completely. Today, we write straight into an Apache Iceberg table and get ACID commits, schema evolution, and time travel for free.
In this post, we’ll build the whole pipeline end to end:
- A Kafka topic producing JSON events
- A Spark Structured Streaming job that consumes the topic
- Streaming writes into an Iceberg table with exactly-once semantics
- Querying and maintaining the table (compaction, snapshots)
streaming
Streaming from Kafka into an Iceberg Lakehouse, End to End
From Kafka Topics to a Lakehouse Table, the Right Way
Still landing Kafka data into HDFS or S3 as raw files and batching it later?
Back in 2015, I wrote about moving Twitter streams from Kafka to HDFS with Camus. A decade later, the goal is the same - get streaming data durably into storage - but the destination has changed completely. Today, we write straight into an Apache Iceberg table and get ACID commits, schema evolution, and time travel for free.
In this post, we’ll build the whole pipeline end to end:
- A Kafka topic producing JSON events
- A Spark Structured Streaming job that consumes the topic
- Streaming writes into an Iceberg table with exactly-once semantics
- Querying and maintaining the table (compaction, snapshots)
Model Context Protocol
MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
Your AI Agent Can’t See Your Lakehouse - Let’s Fix That
How many times this year have you pasted a table schema into a chat window so the AI could help you write a query?
In my last post, we streamed Kafka events into an Apache Iceberg lakehouse with Spark Structured Streaming. The natural follow-up in 2026: instead of querying that table myself, can I just ask an AI agent what’s in it - and get a real answer from the live catalog, not a pasted screenshot?
That is exactly what the Model Context Protocol (MCP) is for. In this post, we’ll build a small MCP server that sits on top of an Iceberg catalog and exposes it to any MCP-capable agent (Claude, IDEs, your own agents):
- What MCP actually is, in data-engineering terms
- A working MCP server over an Iceberg catalog (Python, ~60 lines)
- Wiring it into an agent host like Claude
- The security lines you should not cross
Every code sample below was run end to end while writing this post - client, server, and a real Iceberg table.
MCP
MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
Your AI Agent Can’t See Your Lakehouse - Let’s Fix That
How many times this year have you pasted a table schema into a chat window so the AI could help you write a query?
In my last post, we streamed Kafka events into an Apache Iceberg lakehouse with Spark Structured Streaming. The natural follow-up in 2026: instead of querying that table myself, can I just ask an AI agent what’s in it - and get a real answer from the live catalog, not a pasted screenshot?
That is exactly what the Model Context Protocol (MCP) is for. In this post, we’ll build a small MCP server that sits on top of an Iceberg catalog and exposes it to any MCP-capable agent (Claude, IDEs, your own agents):
- What MCP actually is, in data-engineering terms
- A working MCP server over an Iceberg catalog (Python, ~60 lines)
- Wiring it into an agent host like Claude
- The security lines you should not cross
Every code sample below was run end to end while writing this post - client, server, and a real Iceberg table.
PyIceberg
MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
Your AI Agent Can’t See Your Lakehouse - Let’s Fix That
How many times this year have you pasted a table schema into a chat window so the AI could help you write a query?
In my last post, we streamed Kafka events into an Apache Iceberg lakehouse with Spark Structured Streaming. The natural follow-up in 2026: instead of querying that table myself, can I just ask an AI agent what’s in it - and get a real answer from the live catalog, not a pasted screenshot?
That is exactly what the Model Context Protocol (MCP) is for. In this post, we’ll build a small MCP server that sits on top of an Iceberg catalog and exposes it to any MCP-capable agent (Claude, IDEs, your own agents):
- What MCP actually is, in data-engineering terms
- A working MCP server over an Iceberg catalog (Python, ~60 lines)
- Wiring it into an agent host like Claude
- The security lines you should not cross
Every code sample below was run end to end while writing this post - client, server, and a real Iceberg table.
AI agents
MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
Your AI Agent Can’t See Your Lakehouse - Let’s Fix That
How many times this year have you pasted a table schema into a chat window so the AI could help you write a query?
In my last post, we streamed Kafka events into an Apache Iceberg lakehouse with Spark Structured Streaming. The natural follow-up in 2026: instead of querying that table myself, can I just ask an AI agent what’s in it - and get a real answer from the live catalog, not a pasted screenshot?
That is exactly what the Model Context Protocol (MCP) is for. In this post, we’ll build a small MCP server that sits on top of an Iceberg catalog and exposes it to any MCP-capable agent (Claude, IDEs, your own agents):
- What MCP actually is, in data-engineering terms
- A working MCP server over an Iceberg catalog (Python, ~60 lines)
- Wiring it into an agent host like Claude
- The security lines you should not cross
Every code sample below was run end to end while writing this post - client, server, and a real Iceberg table.
Claude
MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
Your AI Agent Can’t See Your Lakehouse - Let’s Fix That
How many times this year have you pasted a table schema into a chat window so the AI could help you write a query?
In my last post, we streamed Kafka events into an Apache Iceberg lakehouse with Spark Structured Streaming. The natural follow-up in 2026: instead of querying that table myself, can I just ask an AI agent what’s in it - and get a real answer from the live catalog, not a pasted screenshot?
That is exactly what the Model Context Protocol (MCP) is for. In this post, we’ll build a small MCP server that sits on top of an Iceberg catalog and exposes it to any MCP-capable agent (Claude, IDEs, your own agents):
- What MCP actually is, in data-engineering terms
- A working MCP server over an Iceberg catalog (Python, ~60 lines)
- Wiring it into an agent host like Claude
- The security lines you should not cross
Every code sample below was run end to end while writing this post - client, server, and a real Iceberg table.
Apache Spark
The Small-Files Problem: A Field Guide
The Maintenance Chapter Your Streaming Pipeline Was Missing
Your streaming job writes 2 MB Parquet files every two minutes. Six months later, your dashboard queries are ten times slower — and nobody changed the data. Sound familiar?
Yesterday, in Streaming from Kafka into an Iceberg Lakehouse, End to End, we built the whole pipeline — Kafka topic, Spark Structured Streaming, Iceberg table — and Step 6 ended with a warning: one-minute micro-batches create lots of small files — the classic streaming lakehouse problem. This post picks up exactly there. The pipeline works; the file layout is what’s rotting underneath it.
Same stack as yesterday: Kafka 4.3.1, Spark 4.2.0 (Scala 2.13), Iceberg 1.12.0, PySpark — and the same lakehouse.order_events table, partitioned by days(event_time).
In this post, we’ll:
-
See why small files hurt — with real numbers, not vibes
-
Diagnose the problem using Iceberg’s own metadata tables
-
Stop writing small files at write time
-
Fix the damage with compaction (
rewrite_data_files, done right) -
Keep the layout healthy with a checklist that fits on an index card
Step 1: Understand Why Small Files Hurt
It’s tempting to treat file count as cosmetic. It isn’t. Every file in your table is a tax paid four times: at plan time, at task-scheduling time, at request time, and at footer-read time.
Query planning. Iceberg query planning reads manifest files to decide which data files to scan — the manifest and snapshot layer I broke down in the Iceberg intro is exactly what gets stressed here. Dremio’s engineers measured this directly: the same dataset stored in 2,000 files of 256 MB each typically plans in under one second, while a table fragmented into 100,000 files pays a planning cost an order of magnitude higher. Manifests fragment too — every micro-batch commit adds manifests, and the metadata tree stops being an index and becomes a maze.
Task scheduling. Each file becomes at least one execution task. Scheduling costs roughly 1–10 milliseconds per task — trivial, until you multiply: a query against 100,000 files creates at least 100,000 tasks. Even if partition pruning eliminates 95% of them, the surviving 5,000 tasks carry 5–50 seconds of pure scheduling overhead before any data moves. On JVM engines like Spark, giant task graphs also pile on GC pressure, adding unpredictable latency.
Object-store requests. S3 charges $0.0004 per 1,000 GET requests. Reading a file takes at minimum one GET, but Parquet readers also fetch the footer and individual row groups — conservatively, two requests per file. So 100,000 files means 200,000 requests, or $0.08 per query, in storage API costs alone. A table queried 10,000 times a day burns $800 a day — $24,000 a month — on requests. Compact to 2,000 files and that drops to about $1,600 a month, a 93% reduction (source).
Footer reads. Every Parquet file carries a footer — schema, row-group metadata, column statistics. Reading it is mandatory and costs roughly the same whether the file is 1 MB or 512 MB: one to several milliseconds plus a network round trip. For a 256 MB file that’s noise; for a 1 MB file it dominates. If 95% of your read time goes to footers instead of column data, you have a severe small-files problem (source).
How fast it accumulates. Streaming engines checkpoint every 1–10 minutes, and each checkpoint commits new Parquet files — one per partition. A table with 100 partitions on 10-minute checkpoints produces 14,400 new files per day, most of them a few megabytes each (source). A table with 500,000 small files queried 50 times a day generates roughly 25 million GET requests a month. The write path that felt so elegant on day one is a liability by month six.
Step 2: Diagnose — Is This Actually Your Problem?
Don’t compact on vibes. Iceberg gives you metadata tables that answer the question directly:
-- File health at a glance: count, average size, smallest/largest
SELECT
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb,
ROUND(MIN(file_size_in_bytes) / 1048576, 2) AS min_mb,
ROUND(MAX(file_size_in_bytes) / 1048576, 2) AS max_mb,
SUM(CASE WHEN file_size_in_bytes < 134217728
THEN 1 ELSE 0 END) AS files_under_128mb
FROM lakehouse.order_events.files
WHERE content = 0; -- 0 = DATA files; 1/2 = delete files
If a large share of files sits under 128 MB — or you see thousands of files averaging a few MB — you have the problem. Next, find where it hurts most, so your first compaction pass buys the biggest win:
-- Worst partitions first: file count and avg size per partition
SELECT
partition,
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb
FROM lakehouse.order_events.files
WHERE content = 0
GROUP BY partition
ORDER BY file_count DESC
LIMIT 30;
Two more signals worth checking:
-- Are snapshots piling up from streaming appends?
SELECT snapshot_id, committed_at, operation, summary
FROM lakehouse.order_events.snapshots
ORDER BY committed_at DESC
LIMIT 20;
A wall of append snapshots with no replace (compaction) in sight confirms the diagnosis. And don’t forget the manifests metadata table — if manifest count is climbing with file count, planning overhead is compounding.
Outside SQL, the symptoms are just as telling: Spark UI jobs where a stage fans out into tens of thousands of tiny tasks, query plans that take longer than the scan itself, and — the one people notice last — your S3 GetObject/ListObjects request metrics climbing month over month while data volume stays flat.
Step 3: Fix It at Write Time
Compaction is corrective. The cheaper move is to stop writing small files in the first place. Three things worth knowing about the write path:
1. Target file size. The table property write.target-file-size-bytes defaults to 512 MB. Spark writers roll a new file when buffered data crosses this threshold — but only if a single task actually accumulates that much. A streaming micro-batch with 50 MB of data never reaches it, which is exactly how you get 2 MB files:
-- Set once at the table level (256 MB is a common streaming-table choice)
ALTER TABLE lakehouse.order_events
SET TBLPROPERTIES ('write.target-file-size-bytes' = '268435456');
You can also override it per write with the target-file-size-bytes DataFrame option.
2. Distribution mode. write.distribution-mode decides how rows are shuffled before writing: none (no shuffle — each task writes whatever it has; the small-files factory), hash (hash-distribute by partition key), or range (range-distribute by partition or sort key). For streaming writes into a partitioned table, hash is usually the right call:
(df.writeStream
.format("iceberg")
.option("checkpointLocation", checkpoint_path)
.option("distribution-mode", "hash") # shuffle by partition key before writing
.option("fanout-enabled", "true") # keep partition writers open across batches
.toTable("lakehouse.order_events"))
3. The fanout writer. Streaming data arrives out of order across partitions. Without fanout, every partition switch inside a task closes the current file and opens a new one — file rollover churn. write.spark.fanout.enabled (default false) keeps multiple partition writers open simultaneously, at the cost of more memory. For streaming jobs, enable it.
We used
fanout-enabled: truein the streaming job from yesterday’s post for exactly this reason — without it, out-of-order events across days would have multiplied our file count from the very first batch.
Two more write-path habits that pay off:
repartitionbefore batch writes. Line writers up with the partition spec so each task writes fewer, larger files:
from pyspark.sql.functions import to_date
def write_batch(batch_df, batch_id):
(batch_df
.repartition(to_date("event_time")) # align writers with days(event_time)
.writeTo("lakehouse.order_events")
.option("fanout-enabled", "true")
.append())
(query.writeStream
.foreachBatch(write_batch)
.option("checkpointLocation", checkpoint_path)
.trigger(availableNow=True)
.start())
- Trigger intervals. The one-minute trigger we used yesterday is great for freshness demos and terrible for file layout — on a low-volume topic it guarantees tiny files. Prefer
Trigger.AvailableNowor multi-minute processing-time triggers, and coalesce inside the batch function as above.
None of this eliminates compaction entirely — streaming tables will always drift — but it slows the drift from days to weeks.
Step 4: Compact What’s Already There
Yesterday’s post showed the one-line version — CALL lakehouse.system.rewrite_data_files(table => 'order_events'). Here’s the full picture. Iceberg’s compaction primitive rewrites selected data files into fewer, larger ones and commits the result as a new snapshot — fully online, no downtime; readers keep working against the old snapshot until the new one commits.
Binpack (default): fast and dumb, in a good way. It groups small files by partition and concatenates them up to the target size. No shuffle, no reordering — the cheapest possible rewrite:
-- Compact only the files that need it: small files in recent partitions
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'binpack',
where => 'event_time >= current_timestamp() - INTERVAL 7 DAYS',
options => map(
'target-file-size-bytes', '268435456', -- 256 MB
'min-input-files', '5' -- skip groups already healthy
)
);
Sort: slower, but fixes layout too. Binpack doesn’t touch data ordering. If your queries filter on event_time and customer_id but files were written in arrival order, column statistics are weak and pruning suffers. The sort strategy globally sorts before allocating files to tasks, tightening min/max ranges per file:
-- Merge + cluster by the columns your queries actually filter on
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'sort',
sort_order => 'event_time ASC NULLS LAST, customer_id ASC NULLS LAST',
options => map(
'target-file-size-bytes', '268435456',
'partial-progress.enabled', 'true',
'max-concurrent-file-group-rewrites', '5'
)
);
-- For multi-column filters with no single dominant column, Z-order instead:
-- sort_order => 'zorder(event_time, customer_id)'
(Iceberg 1.12 also adds Hilbert-curve clustering as a newer alternative to Z-ordering — worth evaluating once your engine supports it.)
The options that matter in production:
| Option | Default | What it does |
|---|---|---|
target-file-size-bytes |
table’s write.target-file-size-bytes (512 MB) |
Output file size target |
min-input-files |
5 | File groups with fewer files are skipped |
min-file-size-bytes |
75% of target | Files below this are always candidates |
max-file-size-bytes |
180% of target | Files above this are always candidates |
max-concurrent-file-group-rewrites |
5 | Parallelism of the rewrite; raise for large tables |
max-file-group-size-bytes |
100 GB | Splits the job into file groups |
partial-progress.enabled |
false | Commit file groups as they finish instead of one big commit |
partial-progress.max-commits |
10 | Cap on commits when partial progress is on |
rewrite-all |
false | Force-rewrite everything, ignoring the filters above |
rewrite-job-order |
none | Order groups: bytes-asc, bytes-desc, files-asc, files-desc |
Two of these deserve emphasis. partial-progress.enabled=true is the difference between losing hours of rewrite work on a mid-job failure and keeping what finished — always turn it on for large tables. And max-concurrent-file-group-rewrites is the knob real teams reach for at scale: Ancestry’s data team raised it from the default 5 to 100 to compact a 100-billion-row Iceberg table, because the default couldn’t keep up with their update frequency.
The follow-ups people forget. Compaction creates a new snapshot but leaves the old files on disk — old snapshots pin them. Run the cleanup chain, in this order:
-- 1. Expire old snapshots so their files can actually be deleted
-- (defaults: older_than = 5 days ago, retain_last = 1)
CALL lakehouse.system.expire_snapshots(
table => 'order_events',
older_than => TIMESTAMP '2026-10-01 00:00:00',
retain_last => 10
);
-- 2. Consolidate manifest files (metadata-only, fast)
CALL lakehouse.system.rewrite_manifests(table => 'order_events');
-- 3. Sweep up orphan files from failed/partial writes (default window: 3 days)
-- Use dry_run first, and never set older_than shorter than your longest write.
CALL lakehouse.system.remove_orphan_files(
table => 'order_events',
older_than => TIMESTAMP '2026-10-10 00:00:00',
dry_run => true
);
Compacting without ever expiring snapshots means storage grows forever. And
remove_orphan_fileswith too aggressive a window can delete files from in-flight writes — multi-day windows are normal; hours are not.
Step 5: Keep It Healthy — the Index-Card Checklist
The industry rule of thumb is 128–512 MB per file for analytical workloads, and Iceberg’s own defaults agree: write.target-file-size-bytes is 512 MB, and the read path combines splits toward 128 MB (read.split.target-size).
Five lines, taped to your monitor:
- Measure first. Query
tbl.filesmonthly (weekly for streaming tables). Alert when average file size drops below ~128 MB or file count crosses your threshold. - Fix the writer. Set
write.target-file-size-bytes, usedistribution-mode=hashandfanout-enabled=trueon streaming writes, and size your trigger intervals to the data volume. - Compact on a cadence that matches ingestion. Streaming tables often need compaction every few hours, not nightly; batch tables can go weekly.
- Always expire snapshots and rewrite manifests after compacting. Compaction without cleanup is half a job.
- Sweep orphans quarterly with a safe
older_thanwindow — on mature streaming lakes, orphans routinely account for a quarter or more of billable storage (source).
Variations Worth Knowing
Delete-heavy tables (merge-on-read)?
Watch delete-file counts in tbl.files too — compact position deletes with rewrite_position_delete_files, not just data files.
Sub-hour freshness needs?
Don’t chase 512 MB if it means holding micro-batches open. 128–256 MB is fine for streaming tables where freshness matters more than scan efficiency.
Heavy point lookups?
Smaller files with tight clustering (sort/Z-order) beat larger unsorted ones. File size is a means to an end — the end is pruning.
✅ Conclusion
Small files are not a moral failing — they’re the natural output of a healthy streaming pipeline. The failure is letting the write path run unsupervised. Measure the file layout like you’d measure query latency, and the problem stays a maintenance task instead of becoming an incident.
If your Kafka-to-Iceberg pipeline is past the demo stage, its next chapter isn’t a new feature — it’s maintenance. Measure
tbl.files, fix the writer, compact on cadence.
💬 Let’s Discuss
Are you compacting on a schedule, or only when queries get slow? Binpack or sort — which strategy do you reach for first? And what’s the worst small-files incident you’ve seen in production? Share your setup or war stories in the comments!
Related reading
- Unlocking Data Lakes with Apache Iceberg: The Open Table Format Revolutionizing Analytics
- MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
- HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
Additional Details
- Description: A field guide to diagnosing, preventing, and fixing the small-files problem in Spark + Apache Iceberg lakehouse tables — with real numbers, working SQL, and a maintenance checklist.
small files
The Small-Files Problem: A Field Guide
The Maintenance Chapter Your Streaming Pipeline Was Missing
Your streaming job writes 2 MB Parquet files every two minutes. Six months later, your dashboard queries are ten times slower — and nobody changed the data. Sound familiar?
Yesterday, in Streaming from Kafka into an Iceberg Lakehouse, End to End, we built the whole pipeline — Kafka topic, Spark Structured Streaming, Iceberg table — and Step 6 ended with a warning: one-minute micro-batches create lots of small files — the classic streaming lakehouse problem. This post picks up exactly there. The pipeline works; the file layout is what’s rotting underneath it.
Same stack as yesterday: Kafka 4.3.1, Spark 4.2.0 (Scala 2.13), Iceberg 1.12.0, PySpark — and the same lakehouse.order_events table, partitioned by days(event_time).
In this post, we’ll:
-
See why small files hurt — with real numbers, not vibes
-
Diagnose the problem using Iceberg’s own metadata tables
-
Stop writing small files at write time
-
Fix the damage with compaction (
rewrite_data_files, done right) -
Keep the layout healthy with a checklist that fits on an index card
Step 1: Understand Why Small Files Hurt
It’s tempting to treat file count as cosmetic. It isn’t. Every file in your table is a tax paid four times: at plan time, at task-scheduling time, at request time, and at footer-read time.
Query planning. Iceberg query planning reads manifest files to decide which data files to scan — the manifest and snapshot layer I broke down in the Iceberg intro is exactly what gets stressed here. Dremio’s engineers measured this directly: the same dataset stored in 2,000 files of 256 MB each typically plans in under one second, while a table fragmented into 100,000 files pays a planning cost an order of magnitude higher. Manifests fragment too — every micro-batch commit adds manifests, and the metadata tree stops being an index and becomes a maze.
Task scheduling. Each file becomes at least one execution task. Scheduling costs roughly 1–10 milliseconds per task — trivial, until you multiply: a query against 100,000 files creates at least 100,000 tasks. Even if partition pruning eliminates 95% of them, the surviving 5,000 tasks carry 5–50 seconds of pure scheduling overhead before any data moves. On JVM engines like Spark, giant task graphs also pile on GC pressure, adding unpredictable latency.
Object-store requests. S3 charges $0.0004 per 1,000 GET requests. Reading a file takes at minimum one GET, but Parquet readers also fetch the footer and individual row groups — conservatively, two requests per file. So 100,000 files means 200,000 requests, or $0.08 per query, in storage API costs alone. A table queried 10,000 times a day burns $800 a day — $24,000 a month — on requests. Compact to 2,000 files and that drops to about $1,600 a month, a 93% reduction (source).
Footer reads. Every Parquet file carries a footer — schema, row-group metadata, column statistics. Reading it is mandatory and costs roughly the same whether the file is 1 MB or 512 MB: one to several milliseconds plus a network round trip. For a 256 MB file that’s noise; for a 1 MB file it dominates. If 95% of your read time goes to footers instead of column data, you have a severe small-files problem (source).
How fast it accumulates. Streaming engines checkpoint every 1–10 minutes, and each checkpoint commits new Parquet files — one per partition. A table with 100 partitions on 10-minute checkpoints produces 14,400 new files per day, most of them a few megabytes each (source). A table with 500,000 small files queried 50 times a day generates roughly 25 million GET requests a month. The write path that felt so elegant on day one is a liability by month six.
Step 2: Diagnose — Is This Actually Your Problem?
Don’t compact on vibes. Iceberg gives you metadata tables that answer the question directly:
-- File health at a glance: count, average size, smallest/largest
SELECT
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb,
ROUND(MIN(file_size_in_bytes) / 1048576, 2) AS min_mb,
ROUND(MAX(file_size_in_bytes) / 1048576, 2) AS max_mb,
SUM(CASE WHEN file_size_in_bytes < 134217728
THEN 1 ELSE 0 END) AS files_under_128mb
FROM lakehouse.order_events.files
WHERE content = 0; -- 0 = DATA files; 1/2 = delete files
If a large share of files sits under 128 MB — or you see thousands of files averaging a few MB — you have the problem. Next, find where it hurts most, so your first compaction pass buys the biggest win:
-- Worst partitions first: file count and avg size per partition
SELECT
partition,
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb
FROM lakehouse.order_events.files
WHERE content = 0
GROUP BY partition
ORDER BY file_count DESC
LIMIT 30;
Two more signals worth checking:
-- Are snapshots piling up from streaming appends?
SELECT snapshot_id, committed_at, operation, summary
FROM lakehouse.order_events.snapshots
ORDER BY committed_at DESC
LIMIT 20;
A wall of append snapshots with no replace (compaction) in sight confirms the diagnosis. And don’t forget the manifests metadata table — if manifest count is climbing with file count, planning overhead is compounding.
Outside SQL, the symptoms are just as telling: Spark UI jobs where a stage fans out into tens of thousands of tiny tasks, query plans that take longer than the scan itself, and — the one people notice last — your S3 GetObject/ListObjects request metrics climbing month over month while data volume stays flat.
Step 3: Fix It at Write Time
Compaction is corrective. The cheaper move is to stop writing small files in the first place. Three things worth knowing about the write path:
1. Target file size. The table property write.target-file-size-bytes defaults to 512 MB. Spark writers roll a new file when buffered data crosses this threshold — but only if a single task actually accumulates that much. A streaming micro-batch with 50 MB of data never reaches it, which is exactly how you get 2 MB files:
-- Set once at the table level (256 MB is a common streaming-table choice)
ALTER TABLE lakehouse.order_events
SET TBLPROPERTIES ('write.target-file-size-bytes' = '268435456');
You can also override it per write with the target-file-size-bytes DataFrame option.
2. Distribution mode. write.distribution-mode decides how rows are shuffled before writing: none (no shuffle — each task writes whatever it has; the small-files factory), hash (hash-distribute by partition key), or range (range-distribute by partition or sort key). For streaming writes into a partitioned table, hash is usually the right call:
(df.writeStream
.format("iceberg")
.option("checkpointLocation", checkpoint_path)
.option("distribution-mode", "hash") # shuffle by partition key before writing
.option("fanout-enabled", "true") # keep partition writers open across batches
.toTable("lakehouse.order_events"))
3. The fanout writer. Streaming data arrives out of order across partitions. Without fanout, every partition switch inside a task closes the current file and opens a new one — file rollover churn. write.spark.fanout.enabled (default false) keeps multiple partition writers open simultaneously, at the cost of more memory. For streaming jobs, enable it.
We used
fanout-enabled: truein the streaming job from yesterday’s post for exactly this reason — without it, out-of-order events across days would have multiplied our file count from the very first batch.
Two more write-path habits that pay off:
repartitionbefore batch writes. Line writers up with the partition spec so each task writes fewer, larger files:
from pyspark.sql.functions import to_date
def write_batch(batch_df, batch_id):
(batch_df
.repartition(to_date("event_time")) # align writers with days(event_time)
.writeTo("lakehouse.order_events")
.option("fanout-enabled", "true")
.append())
(query.writeStream
.foreachBatch(write_batch)
.option("checkpointLocation", checkpoint_path)
.trigger(availableNow=True)
.start())
- Trigger intervals. The one-minute trigger we used yesterday is great for freshness demos and terrible for file layout — on a low-volume topic it guarantees tiny files. Prefer
Trigger.AvailableNowor multi-minute processing-time triggers, and coalesce inside the batch function as above.
None of this eliminates compaction entirely — streaming tables will always drift — but it slows the drift from days to weeks.
Step 4: Compact What’s Already There
Yesterday’s post showed the one-line version — CALL lakehouse.system.rewrite_data_files(table => 'order_events'). Here’s the full picture. Iceberg’s compaction primitive rewrites selected data files into fewer, larger ones and commits the result as a new snapshot — fully online, no downtime; readers keep working against the old snapshot until the new one commits.
Binpack (default): fast and dumb, in a good way. It groups small files by partition and concatenates them up to the target size. No shuffle, no reordering — the cheapest possible rewrite:
-- Compact only the files that need it: small files in recent partitions
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'binpack',
where => 'event_time >= current_timestamp() - INTERVAL 7 DAYS',
options => map(
'target-file-size-bytes', '268435456', -- 256 MB
'min-input-files', '5' -- skip groups already healthy
)
);
Sort: slower, but fixes layout too. Binpack doesn’t touch data ordering. If your queries filter on event_time and customer_id but files were written in arrival order, column statistics are weak and pruning suffers. The sort strategy globally sorts before allocating files to tasks, tightening min/max ranges per file:
-- Merge + cluster by the columns your queries actually filter on
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'sort',
sort_order => 'event_time ASC NULLS LAST, customer_id ASC NULLS LAST',
options => map(
'target-file-size-bytes', '268435456',
'partial-progress.enabled', 'true',
'max-concurrent-file-group-rewrites', '5'
)
);
-- For multi-column filters with no single dominant column, Z-order instead:
-- sort_order => 'zorder(event_time, customer_id)'
(Iceberg 1.12 also adds Hilbert-curve clustering as a newer alternative to Z-ordering — worth evaluating once your engine supports it.)
The options that matter in production:
| Option | Default | What it does |
|---|---|---|
target-file-size-bytes |
table’s write.target-file-size-bytes (512 MB) |
Output file size target |
min-input-files |
5 | File groups with fewer files are skipped |
min-file-size-bytes |
75% of target | Files below this are always candidates |
max-file-size-bytes |
180% of target | Files above this are always candidates |
max-concurrent-file-group-rewrites |
5 | Parallelism of the rewrite; raise for large tables |
max-file-group-size-bytes |
100 GB | Splits the job into file groups |
partial-progress.enabled |
false | Commit file groups as they finish instead of one big commit |
partial-progress.max-commits |
10 | Cap on commits when partial progress is on |
rewrite-all |
false | Force-rewrite everything, ignoring the filters above |
rewrite-job-order |
none | Order groups: bytes-asc, bytes-desc, files-asc, files-desc |
Two of these deserve emphasis. partial-progress.enabled=true is the difference between losing hours of rewrite work on a mid-job failure and keeping what finished — always turn it on for large tables. And max-concurrent-file-group-rewrites is the knob real teams reach for at scale: Ancestry’s data team raised it from the default 5 to 100 to compact a 100-billion-row Iceberg table, because the default couldn’t keep up with their update frequency.
The follow-ups people forget. Compaction creates a new snapshot but leaves the old files on disk — old snapshots pin them. Run the cleanup chain, in this order:
-- 1. Expire old snapshots so their files can actually be deleted
-- (defaults: older_than = 5 days ago, retain_last = 1)
CALL lakehouse.system.expire_snapshots(
table => 'order_events',
older_than => TIMESTAMP '2026-10-01 00:00:00',
retain_last => 10
);
-- 2. Consolidate manifest files (metadata-only, fast)
CALL lakehouse.system.rewrite_manifests(table => 'order_events');
-- 3. Sweep up orphan files from failed/partial writes (default window: 3 days)
-- Use dry_run first, and never set older_than shorter than your longest write.
CALL lakehouse.system.remove_orphan_files(
table => 'order_events',
older_than => TIMESTAMP '2026-10-10 00:00:00',
dry_run => true
);
Compacting without ever expiring snapshots means storage grows forever. And
remove_orphan_fileswith too aggressive a window can delete files from in-flight writes — multi-day windows are normal; hours are not.
Step 5: Keep It Healthy — the Index-Card Checklist
The industry rule of thumb is 128–512 MB per file for analytical workloads, and Iceberg’s own defaults agree: write.target-file-size-bytes is 512 MB, and the read path combines splits toward 128 MB (read.split.target-size).
Five lines, taped to your monitor:
- Measure first. Query
tbl.filesmonthly (weekly for streaming tables). Alert when average file size drops below ~128 MB or file count crosses your threshold. - Fix the writer. Set
write.target-file-size-bytes, usedistribution-mode=hashandfanout-enabled=trueon streaming writes, and size your trigger intervals to the data volume. - Compact on a cadence that matches ingestion. Streaming tables often need compaction every few hours, not nightly; batch tables can go weekly.
- Always expire snapshots and rewrite manifests after compacting. Compaction without cleanup is half a job.
- Sweep orphans quarterly with a safe
older_thanwindow — on mature streaming lakes, orphans routinely account for a quarter or more of billable storage (source).
Variations Worth Knowing
Delete-heavy tables (merge-on-read)?
Watch delete-file counts in tbl.files too — compact position deletes with rewrite_position_delete_files, not just data files.
Sub-hour freshness needs?
Don’t chase 512 MB if it means holding micro-batches open. 128–256 MB is fine for streaming tables where freshness matters more than scan efficiency.
Heavy point lookups?
Smaller files with tight clustering (sort/Z-order) beat larger unsorted ones. File size is a means to an end — the end is pruning.
✅ Conclusion
Small files are not a moral failing — they’re the natural output of a healthy streaming pipeline. The failure is letting the write path run unsupervised. Measure the file layout like you’d measure query latency, and the problem stays a maintenance task instead of becoming an incident.
If your Kafka-to-Iceberg pipeline is past the demo stage, its next chapter isn’t a new feature — it’s maintenance. Measure
tbl.files, fix the writer, compact on cadence.
💬 Let’s Discuss
Are you compacting on a schedule, or only when queries get slow? Binpack or sort — which strategy do you reach for first? And what’s the worst small-files incident you’ve seen in production? Share your setup or war stories in the comments!
Related reading
- Unlocking Data Lakes with Apache Iceberg: The Open Table Format Revolutionizing Analytics
- MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
- HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
Additional Details
- Description: A field guide to diagnosing, preventing, and fixing the small-files problem in Spark + Apache Iceberg lakehouse tables — with real numbers, working SQL, and a maintenance checklist.
compaction
The Small-Files Problem: A Field Guide
The Maintenance Chapter Your Streaming Pipeline Was Missing
Your streaming job writes 2 MB Parquet files every two minutes. Six months later, your dashboard queries are ten times slower — and nobody changed the data. Sound familiar?
Yesterday, in Streaming from Kafka into an Iceberg Lakehouse, End to End, we built the whole pipeline — Kafka topic, Spark Structured Streaming, Iceberg table — and Step 6 ended with a warning: one-minute micro-batches create lots of small files — the classic streaming lakehouse problem. This post picks up exactly there. The pipeline works; the file layout is what’s rotting underneath it.
Same stack as yesterday: Kafka 4.3.1, Spark 4.2.0 (Scala 2.13), Iceberg 1.12.0, PySpark — and the same lakehouse.order_events table, partitioned by days(event_time).
In this post, we’ll:
-
See why small files hurt — with real numbers, not vibes
-
Diagnose the problem using Iceberg’s own metadata tables
-
Stop writing small files at write time
-
Fix the damage with compaction (
rewrite_data_files, done right) -
Keep the layout healthy with a checklist that fits on an index card
Step 1: Understand Why Small Files Hurt
It’s tempting to treat file count as cosmetic. It isn’t. Every file in your table is a tax paid four times: at plan time, at task-scheduling time, at request time, and at footer-read time.
Query planning. Iceberg query planning reads manifest files to decide which data files to scan — the manifest and snapshot layer I broke down in the Iceberg intro is exactly what gets stressed here. Dremio’s engineers measured this directly: the same dataset stored in 2,000 files of 256 MB each typically plans in under one second, while a table fragmented into 100,000 files pays a planning cost an order of magnitude higher. Manifests fragment too — every micro-batch commit adds manifests, and the metadata tree stops being an index and becomes a maze.
Task scheduling. Each file becomes at least one execution task. Scheduling costs roughly 1–10 milliseconds per task — trivial, until you multiply: a query against 100,000 files creates at least 100,000 tasks. Even if partition pruning eliminates 95% of them, the surviving 5,000 tasks carry 5–50 seconds of pure scheduling overhead before any data moves. On JVM engines like Spark, giant task graphs also pile on GC pressure, adding unpredictable latency.
Object-store requests. S3 charges $0.0004 per 1,000 GET requests. Reading a file takes at minimum one GET, but Parquet readers also fetch the footer and individual row groups — conservatively, two requests per file. So 100,000 files means 200,000 requests, or $0.08 per query, in storage API costs alone. A table queried 10,000 times a day burns $800 a day — $24,000 a month — on requests. Compact to 2,000 files and that drops to about $1,600 a month, a 93% reduction (source).
Footer reads. Every Parquet file carries a footer — schema, row-group metadata, column statistics. Reading it is mandatory and costs roughly the same whether the file is 1 MB or 512 MB: one to several milliseconds plus a network round trip. For a 256 MB file that’s noise; for a 1 MB file it dominates. If 95% of your read time goes to footers instead of column data, you have a severe small-files problem (source).
How fast it accumulates. Streaming engines checkpoint every 1–10 minutes, and each checkpoint commits new Parquet files — one per partition. A table with 100 partitions on 10-minute checkpoints produces 14,400 new files per day, most of them a few megabytes each (source). A table with 500,000 small files queried 50 times a day generates roughly 25 million GET requests a month. The write path that felt so elegant on day one is a liability by month six.
Step 2: Diagnose — Is This Actually Your Problem?
Don’t compact on vibes. Iceberg gives you metadata tables that answer the question directly:
-- File health at a glance: count, average size, smallest/largest
SELECT
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb,
ROUND(MIN(file_size_in_bytes) / 1048576, 2) AS min_mb,
ROUND(MAX(file_size_in_bytes) / 1048576, 2) AS max_mb,
SUM(CASE WHEN file_size_in_bytes < 134217728
THEN 1 ELSE 0 END) AS files_under_128mb
FROM lakehouse.order_events.files
WHERE content = 0; -- 0 = DATA files; 1/2 = delete files
If a large share of files sits under 128 MB — or you see thousands of files averaging a few MB — you have the problem. Next, find where it hurts most, so your first compaction pass buys the biggest win:
-- Worst partitions first: file count and avg size per partition
SELECT
partition,
COUNT(*) AS file_count,
ROUND(AVG(file_size_in_bytes) / 1048576, 2) AS avg_mb
FROM lakehouse.order_events.files
WHERE content = 0
GROUP BY partition
ORDER BY file_count DESC
LIMIT 30;
Two more signals worth checking:
-- Are snapshots piling up from streaming appends?
SELECT snapshot_id, committed_at, operation, summary
FROM lakehouse.order_events.snapshots
ORDER BY committed_at DESC
LIMIT 20;
A wall of append snapshots with no replace (compaction) in sight confirms the diagnosis. And don’t forget the manifests metadata table — if manifest count is climbing with file count, planning overhead is compounding.
Outside SQL, the symptoms are just as telling: Spark UI jobs where a stage fans out into tens of thousands of tiny tasks, query plans that take longer than the scan itself, and — the one people notice last — your S3 GetObject/ListObjects request metrics climbing month over month while data volume stays flat.
Step 3: Fix It at Write Time
Compaction is corrective. The cheaper move is to stop writing small files in the first place. Three things worth knowing about the write path:
1. Target file size. The table property write.target-file-size-bytes defaults to 512 MB. Spark writers roll a new file when buffered data crosses this threshold — but only if a single task actually accumulates that much. A streaming micro-batch with 50 MB of data never reaches it, which is exactly how you get 2 MB files:
-- Set once at the table level (256 MB is a common streaming-table choice)
ALTER TABLE lakehouse.order_events
SET TBLPROPERTIES ('write.target-file-size-bytes' = '268435456');
You can also override it per write with the target-file-size-bytes DataFrame option.
2. Distribution mode. write.distribution-mode decides how rows are shuffled before writing: none (no shuffle — each task writes whatever it has; the small-files factory), hash (hash-distribute by partition key), or range (range-distribute by partition or sort key). For streaming writes into a partitioned table, hash is usually the right call:
(df.writeStream
.format("iceberg")
.option("checkpointLocation", checkpoint_path)
.option("distribution-mode", "hash") # shuffle by partition key before writing
.option("fanout-enabled", "true") # keep partition writers open across batches
.toTable("lakehouse.order_events"))
3. The fanout writer. Streaming data arrives out of order across partitions. Without fanout, every partition switch inside a task closes the current file and opens a new one — file rollover churn. write.spark.fanout.enabled (default false) keeps multiple partition writers open simultaneously, at the cost of more memory. For streaming jobs, enable it.
We used
fanout-enabled: truein the streaming job from yesterday’s post for exactly this reason — without it, out-of-order events across days would have multiplied our file count from the very first batch.
Two more write-path habits that pay off:
repartitionbefore batch writes. Line writers up with the partition spec so each task writes fewer, larger files:
from pyspark.sql.functions import to_date
def write_batch(batch_df, batch_id):
(batch_df
.repartition(to_date("event_time")) # align writers with days(event_time)
.writeTo("lakehouse.order_events")
.option("fanout-enabled", "true")
.append())
(query.writeStream
.foreachBatch(write_batch)
.option("checkpointLocation", checkpoint_path)
.trigger(availableNow=True)
.start())
- Trigger intervals. The one-minute trigger we used yesterday is great for freshness demos and terrible for file layout — on a low-volume topic it guarantees tiny files. Prefer
Trigger.AvailableNowor multi-minute processing-time triggers, and coalesce inside the batch function as above.
None of this eliminates compaction entirely — streaming tables will always drift — but it slows the drift from days to weeks.
Step 4: Compact What’s Already There
Yesterday’s post showed the one-line version — CALL lakehouse.system.rewrite_data_files(table => 'order_events'). Here’s the full picture. Iceberg’s compaction primitive rewrites selected data files into fewer, larger ones and commits the result as a new snapshot — fully online, no downtime; readers keep working against the old snapshot until the new one commits.
Binpack (default): fast and dumb, in a good way. It groups small files by partition and concatenates them up to the target size. No shuffle, no reordering — the cheapest possible rewrite:
-- Compact only the files that need it: small files in recent partitions
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'binpack',
where => 'event_time >= current_timestamp() - INTERVAL 7 DAYS',
options => map(
'target-file-size-bytes', '268435456', -- 256 MB
'min-input-files', '5' -- skip groups already healthy
)
);
Sort: slower, but fixes layout too. Binpack doesn’t touch data ordering. If your queries filter on event_time and customer_id but files were written in arrival order, column statistics are weak and pruning suffers. The sort strategy globally sorts before allocating files to tasks, tightening min/max ranges per file:
-- Merge + cluster by the columns your queries actually filter on
CALL lakehouse.system.rewrite_data_files(
table => 'order_events',
strategy => 'sort',
sort_order => 'event_time ASC NULLS LAST, customer_id ASC NULLS LAST',
options => map(
'target-file-size-bytes', '268435456',
'partial-progress.enabled', 'true',
'max-concurrent-file-group-rewrites', '5'
)
);
-- For multi-column filters with no single dominant column, Z-order instead:
-- sort_order => 'zorder(event_time, customer_id)'
(Iceberg 1.12 also adds Hilbert-curve clustering as a newer alternative to Z-ordering — worth evaluating once your engine supports it.)
The options that matter in production:
| Option | Default | What it does |
|---|---|---|
target-file-size-bytes |
table’s write.target-file-size-bytes (512 MB) |
Output file size target |
min-input-files |
5 | File groups with fewer files are skipped |
min-file-size-bytes |
75% of target | Files below this are always candidates |
max-file-size-bytes |
180% of target | Files above this are always candidates |
max-concurrent-file-group-rewrites |
5 | Parallelism of the rewrite; raise for large tables |
max-file-group-size-bytes |
100 GB | Splits the job into file groups |
partial-progress.enabled |
false | Commit file groups as they finish instead of one big commit |
partial-progress.max-commits |
10 | Cap on commits when partial progress is on |
rewrite-all |
false | Force-rewrite everything, ignoring the filters above |
rewrite-job-order |
none | Order groups: bytes-asc, bytes-desc, files-asc, files-desc |
Two of these deserve emphasis. partial-progress.enabled=true is the difference between losing hours of rewrite work on a mid-job failure and keeping what finished — always turn it on for large tables. And max-concurrent-file-group-rewrites is the knob real teams reach for at scale: Ancestry’s data team raised it from the default 5 to 100 to compact a 100-billion-row Iceberg table, because the default couldn’t keep up with their update frequency.
The follow-ups people forget. Compaction creates a new snapshot but leaves the old files on disk — old snapshots pin them. Run the cleanup chain, in this order:
-- 1. Expire old snapshots so their files can actually be deleted
-- (defaults: older_than = 5 days ago, retain_last = 1)
CALL lakehouse.system.expire_snapshots(
table => 'order_events',
older_than => TIMESTAMP '2026-10-01 00:00:00',
retain_last => 10
);
-- 2. Consolidate manifest files (metadata-only, fast)
CALL lakehouse.system.rewrite_manifests(table => 'order_events');
-- 3. Sweep up orphan files from failed/partial writes (default window: 3 days)
-- Use dry_run first, and never set older_than shorter than your longest write.
CALL lakehouse.system.remove_orphan_files(
table => 'order_events',
older_than => TIMESTAMP '2026-10-10 00:00:00',
dry_run => true
);
Compacting without ever expiring snapshots means storage grows forever. And
remove_orphan_fileswith too aggressive a window can delete files from in-flight writes — multi-day windows are normal; hours are not.
Step 5: Keep It Healthy — the Index-Card Checklist
The industry rule of thumb is 128–512 MB per file for analytical workloads, and Iceberg’s own defaults agree: write.target-file-size-bytes is 512 MB, and the read path combines splits toward 128 MB (read.split.target-size).
Five lines, taped to your monitor:
- Measure first. Query
tbl.filesmonthly (weekly for streaming tables). Alert when average file size drops below ~128 MB or file count crosses your threshold. - Fix the writer. Set
write.target-file-size-bytes, usedistribution-mode=hashandfanout-enabled=trueon streaming writes, and size your trigger intervals to the data volume. - Compact on a cadence that matches ingestion. Streaming tables often need compaction every few hours, not nightly; batch tables can go weekly.
- Always expire snapshots and rewrite manifests after compacting. Compaction without cleanup is half a job.
- Sweep orphans quarterly with a safe
older_thanwindow — on mature streaming lakes, orphans routinely account for a quarter or more of billable storage (source).
Variations Worth Knowing
Delete-heavy tables (merge-on-read)?
Watch delete-file counts in tbl.files too — compact position deletes with rewrite_position_delete_files, not just data files.
Sub-hour freshness needs?
Don’t chase 512 MB if it means holding micro-batches open. 128–256 MB is fine for streaming tables where freshness matters more than scan efficiency.
Heavy point lookups?
Smaller files with tight clustering (sort/Z-order) beat larger unsorted ones. File size is a means to an end — the end is pruning.
✅ Conclusion
Small files are not a moral failing — they’re the natural output of a healthy streaming pipeline. The failure is letting the write path run unsupervised. Measure the file layout like you’d measure query latency, and the problem stays a maintenance task instead of becoming an incident.
If your Kafka-to-Iceberg pipeline is past the demo stage, its next chapter isn’t a new feature — it’s maintenance. Measure
tbl.files, fix the writer, compact on cadence.
💬 Let’s Discuss
Are you compacting on a schedule, or only when queries get slow? Binpack or sort — which strategy do you reach for first? And what’s the worst small-files incident you’ve seen in production? Share your setup or war stories in the comments!
Related reading
- Unlocking Data Lakes with Apache Iceberg: The Open Table Format Revolutionizing Analytics
- MCP for Data Engineers: Connecting AI Agents to Your Iceberg Lakehouse
- HDFS vs Object Storage: Deep Dive for Big Data Processing in the Cloud
Additional Details
- Description: A field guide to diagnosing, preventing, and fixing the small-files problem in Spark + Apache Iceberg lakehouse tables — with real numbers, working SQL, and a maintenance checklist.