<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: joni sar</title>
    <description>The latest articles on DEV Community by joni sar (@jonisar).</description>
    <link>https://dev.to/jonisar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F13629%2F82514506-b658-415e-8114-a7f2e04c4ff3.png</url>
      <title>DEV Community: joni sar</title>
      <link>https://dev.to/jonisar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jonisar"/>
    <language>en</language>
    <item>
      <title>Apache Iceberg Query Performance: A Practical Guide</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Sun, 02 Aug 2026 09:31:14 +0000</pubDate>
      <link>https://dev.to/jonisar/apache-iceberg-query-performance-a-practical-guide-1pd6</link>
      <guid>https://dev.to/jonisar/apache-iceberg-query-performance-a-practical-guide-1pd6</guid>
      <description>&lt;p&gt;Iceberg is the standard. Snowflake, Databricks, AWS, and every major query engine read and write it natively. The format question is settled.&lt;/p&gt;

&lt;p&gt;The performance question is not.&lt;/p&gt;

&lt;p&gt;Most production Iceberg tables are slower than they need to be — not because of anything wrong with the format, but because the physical state of the table has degraded over time. Thousands of small files from streaming writes. Manifests fragmented across hundreds of snapshots. Data scattered randomly across files with no correlation to how it's actually queried. Delete files piling up from CDC pipelines.&lt;/p&gt;

&lt;p&gt;The result: engines scan 5–10x more data than necessary. Query planning takes seconds instead of milliseconds. S3 API costs spike. And every engine — Trino, Spark, Snowflake, Athena, DuckDB — is equally affected because they all read the same physical data layout.&lt;/p&gt;

&lt;p&gt;This guide covers what determines Iceberg query performance, how to fix it, and the two paths available: intelligent continuous optimization through a control plane, and the manual approach with SQL and cron. Both paths target the same physical levers — the difference is whether they adapt over time or require ongoing human attention.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Iceberg scan planning works
&lt;/h2&gt;

&lt;p&gt;Understanding the performance machinery is essential for both approaches. When a query engine receives a &lt;code&gt;SELECT&lt;/code&gt; against an Iceberg table, it runs a &lt;strong&gt;scan planning&lt;/strong&gt; process on a single node that determines which files to read. This is one of Iceberg's core advantages over Hive-style directory listing — no distributed scan needed just to find your data.&lt;/p&gt;

&lt;p&gt;Planning operates in three progressive levels of elimination:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 1: Manifest list pruning.&lt;/strong&gt; The engine reads the snapshot's manifest list and checks partition value ranges in each manifest's summary. If a manifest's date range doesn't overlap with the query's filter, it's skipped entirely — along with all data files it references. One check eliminates hundreds of manifest files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 2: File-level data skipping.&lt;/strong&gt; For surviving manifests, the engine reads per-file column statistics (min/max bounds, null counts, row counts) and evaluates predicates against them. A query for &lt;code&gt;amount &amp;gt; 500&lt;/code&gt; skips every file where the &lt;code&gt;amount&lt;/code&gt; column's maximum is below 500. This is where &lt;strong&gt;sort order&lt;/strong&gt; determines everything — tight, non-overlapping min/max ranges mean more files eliminated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 3: Row group pruning.&lt;/strong&gt; Within surviving files, Parquet row group statistics and bloom filters skip sub-file data blocks. If only one of four row groups matches, 75% of the file's I/O is avoided.&lt;/p&gt;

&lt;p&gt;The effectiveness of each level depends entirely on the &lt;em&gt;physical state&lt;/em&gt; of the table. A &lt;a href="https://lakeops.dev/blog/apache-iceberg-query-planning" rel="noopener noreferrer"&gt;detailed walkthrough&lt;/a&gt; covers these internals in depth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why tables degrade
&lt;/h2&gt;

&lt;p&gt;Iceberg tables don't start slow. They become slow because the physical layout drifts away from what's optimal for the queries hitting them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming writes&lt;/strong&gt; create files at every checkpoint interval (every 1–5 minutes). A table receiving continuous events generates hundreds or thousands of files per day — each one tiny, each one adding a manifest entry that must be evaluated during planning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static sort orders&lt;/strong&gt; lose relevance. A sort key chosen at table creation doesn't reflect what queries actually filter on six months later. As access patterns evolve, the sort becomes meaningless and min/max statistics become wide and useless for pruning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance runs in isolation.&lt;/strong&gt; Compaction, snapshot expiration, manifest rewriting, and orphan cleanup each affect each other's effectiveness. Running them independently — or in the wrong order — means wasted work and suboptimal results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDC pipelines accumulate delete files.&lt;/strong&gt; Every update writes a small delete file. After weeks, hundreds of delete files must be reconciled at read time — each query pays the cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No feedback loop.&lt;/strong&gt; Without observing which columns queries actually filter on, which tables are degrading fastest, and which maintenance actions produce the most benefit — optimization is guesswork.&lt;/p&gt;

&lt;p&gt;These problems don't happen once and get fixed. They're continuous. They happen every day, on every table, as long as data is being written and queried.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two paths to query performance
&lt;/h2&gt;

&lt;p&gt;There are fundamentally two approaches to keeping Iceberg tables performant:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 1: Intelligent continuous optimization.&lt;/strong&gt; A system that observes query patterns, understands table state, and takes the right action at the right time — autonomously, adaptively, and in the correct sequence. This is the control plane approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 2: The manual path.&lt;/strong&gt; SQL procedures, cron jobs, and configuration tuning. You run the same operations — compaction, manifest rewrites, snapshot expiration — but you decide when, how, and on which tables. You choose the sort order. You monitor for degradation. You sequence the operations correctly.&lt;/p&gt;

&lt;p&gt;Both paths target the same physical levers described above. The difference is not &lt;em&gt;what&lt;/em&gt; gets done — it's whether it adapts continuously or requires ongoing engineering attention.&lt;/p&gt;




&lt;h2&gt;
  
  
  Path 1: Intelligent continuous optimization
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; is an autonomous control plane for Apache Iceberg. It connects to your existing catalogs and query engines, observes table state and query patterns, and continuously applies the optimizations that make queries faster — without human intervention, without Spark clusters, and without static configurations that go stale.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvalj1icwwlaf3bzw6nq1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvalj1icwwlaf3bzw6nq1.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what it does and why each piece improves query performance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbb7lq70n3qrnfa22v46j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbb7lq70n3qrnfa22v46j.png" alt=" " width="799" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Query-aware data layout
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fesa9s1e8kzyzh8q54etj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fesa9s1e8kzyzh8q54etj.png" alt=" " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the highest-impact capability. Sort order is the single most impactful lever for Iceberg query performance — sorted tables scan &lt;a href="https://lakeops.dev/solutions/compaction" rel="noopener noreferrer"&gt;51% less data&lt;/a&gt; per query than unsorted ones. But choosing the &lt;em&gt;right&lt;/em&gt; sort order requires knowing which columns production queries actually filter on. That knowledge changes over time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foz0pasmkzsa1jxx21jnz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foz0pasmkzsa1jxx21jnz.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LakeOps captures query telemetry from every connected engine — Spark, Trino, Flink, Snowflake, Athena, DuckDB, StarRocks — and identifies which columns appear in &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, and &lt;code&gt;GROUP BY&lt;/code&gt; clauses for each table. During compaction, data is physically re-sorted by those columns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3duymjnos25rxx0x32yw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3duymjnos25rxx0x32yw.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The result: Parquet row-group min/max statistics become maximally tight. The scan planner eliminates the vast majority of files at Level 2 because each file covers a narrow, non-overlapping value range for the columns queries actually use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqnomwacqkxda55ybinjt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqnomwacqkxda55ybinjt.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sort strategy adapts. If a new dashboard starts filtering on &lt;code&gt;customer_segment&lt;/code&gt; alongside &lt;code&gt;event_date&lt;/code&gt;, the next compaction pass incorporates it. No manual &lt;code&gt;ALTER TABLE SET SORT ORDER&lt;/code&gt; needed. On three consecutive runs of a 1.2 TB table, the system improved runtime from 22 min → 18 min → 11 min as it converged on optimal column ordering — zero configuration changes.&lt;/p&gt;

&lt;p&gt;Production numbers: a table with 47,000 scattered files, after query-aware sort compaction to 280 files, saw query time drop from &lt;strong&gt;52 seconds to 5.8 seconds&lt;/strong&gt;. A 9x improvement from layout optimization alone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl46kdraz2zia8g03yzt3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl46kdraz2zia8g03yzt3.png" alt=" " width="800" height="637"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Compaction at streaming speed
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa7hemqzw3lxqgywcgg5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa7hemqzw3lxqgywcgg5d.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sort compaction is the most impactful maintenance operation — it solves both the small-file problem and the sort-order problem in one pass. But on Spark, it's slow and expensive. A 200 GB sort compaction takes ~25 minutes and costs ~$3.50 on EMR. At that speed, running it multiple times per day on streaming tables is impractical. Most teams default to binpack (size-only) because sort is too expensive — leaving the biggest performance lever unused.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flpmjbb07b1e006zggdgn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flpmjbb07b1e006zggdgn.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LakeOps's compaction engine is built in Rust on &lt;a href="https://datafusion.apache.org/" rel="noopener noreferrer"&gt;Apache DataFusion&lt;/a&gt;. No JVM. No garbage collection. No OOM crashes. The speed difference makes sort compaction viable even for streaming tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binpack: &lt;strong&gt;221 seconds&lt;/strong&gt; vs 1,612s (Spark) vs 6,300s (S3 Tables) on identical 200 GB dataset&lt;/li&gt;
&lt;li&gt;Sort: &lt;strong&gt;780 seconds&lt;/strong&gt; vs estimated 3,000+ for Spark&lt;/li&gt;
&lt;li&gt;Cost: &lt;strong&gt;$0.21&lt;/strong&gt; vs $1.54 for 200 GB binpack&lt;/li&gt;
&lt;li&gt;Peak throughput: &lt;strong&gt;2,522 MB/s&lt;/strong&gt; — TB-scale tables in minutes&lt;/li&gt;
&lt;li&gt;Bounded memory: spills to disk gracefully, no OOM regardless of table size&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because it's fast enough to run frequently, tables stay both consolidated &lt;em&gt;and&lt;/em&gt; sorted continuously — never degrading between maintenance windows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coordinated maintenance pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1pzq0ozwbw41jkcq7t8w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1pzq0ozwbw41jkcq7t8w.png" alt=" " width="799" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Individual maintenance operations interact. The order matters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expire snapshots&lt;/strong&gt; — release references to old data files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove orphans&lt;/strong&gt; — delete unreferenced files from storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compact&lt;/strong&gt; — merge remaining files with optimal sort order&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite manifests&lt;/strong&gt; — consolidate metadata over the new, clean file set&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running these in the wrong order wastes work. Compacting before expiring snapshots means you might rewrite files that should be deleted. Rewriting manifests before compaction means the structure changes again during the compaction commit.&lt;/p&gt;

&lt;p&gt;LakeOps sequences operations automatically. Each step produces a clean input for the next. The manifest rewrite runs on an already-consolidated, properly-sorted file set — producing the leanest possible metadata layer. This coordination eliminates the redundant work that independent cron jobs inevitably create.&lt;/p&gt;

&lt;h3&gt;
  
  
  Event-driven triggers
&lt;/h3&gt;

&lt;p&gt;A streaming table that accumulates 500 files per hour needs compaction multiple times per day. A stable batch table with weekly loads needs compaction monthly. A CDC table with accumulating delete files needs cleanup when the read-time overhead crosses a threshold.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqxnei94dugsrd5ogfuge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqxnei94dugsrd5ogfuge.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LakeOps triggers operations based on actual table state — file count thresholds, delete-file ratios, manifest fragmentation, partition skew — not arbitrary cron schedules. Tables get exactly the maintenance they need, when they need it. No wasted runs on healthy tables, no missed runs on degrading ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-engine query routing and workload optimization
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffgjffikob2oibxmwap7o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffgjffikob2oibxmwap7o.png" alt=" " width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Different engines have different performance profiles: a point lookup takes 0.5s on DuckDB vs 2.3s on Athena. A full-table scan costs $5 on Athena vs $50 on Trino. Without routing, every query goes to the same engine regardless of its shape.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbbp2atn2tsg1pdn1rwq1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbbp2atn2tsg1pdn1rwq1.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LakeOps includes &lt;a href="https://queryflux.dev" rel="noopener noreferrer"&gt;QueryFlux&lt;/a&gt; — an open-source, Rust-based SQL proxy with 0.35ms overhead — for &lt;a href="https://lakeops.dev/solutions/query-routing" rel="noopener noreferrer"&gt;multi-engine routing&lt;/a&gt;. Queries route based on shape, latency targets, cost ceilings, engine availability, and table health status. The routing layer learns from execution history: if a query shape consistently runs faster on one engine, future executions route there.&lt;/p&gt;

&lt;p&gt;In benchmarking, workload-aware routing reduced total query cost by &lt;strong&gt;up to 80%&lt;/strong&gt;, with individual queries sometimes dropping by 90%.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy7dwkub18hlenclrxol0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy7dwkub18hlenclrxol0.png" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability and self-improvement
&lt;/h3&gt;

&lt;p&gt;You can't optimize what you can't see. LakeOps classifies every table into health tiers (Critical, Warning, Healthy) based on file fragmentation, manifest depth, snapshot velocity, delete ratios, and sort-order staleness. &lt;a href="https://lakeops.dev/blog/iceberg-lakehouse-observability-guide" rel="noopener noreferrer"&gt;Insights surface at four severity levels&lt;/a&gt; — before degradation becomes noticeable in query times.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd31vjc41np08nz0fzckn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd31vjc41np08nz0fzckn.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The system records per-table throughput, partition structure, and memory usage from each compaction run. Subsequent passes execute faster as the planner converges on optimal resource allocation. Every operation feeds back into the next decision.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Froq4jsy6maqf848vxexs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Froq4jsy6maqf848vxexs.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What setup looks like
&lt;/h3&gt;

&lt;p&gt;Connect your catalogs (AWS Glue, REST catalogs like Polaris/Nessie/Lakekeeper, S3 Tables) and storage. ~10 minutes. No agents to deploy, no data movement, no pipeline changes. Your data stays in your account. The system discovers tables, classifies health, begins autonomous maintenance according to policies you define.&lt;/p&gt;

&lt;p&gt;Production results across customers: up to &lt;strong&gt;12x average query acceleration&lt;/strong&gt;, up to 80% total cost reduction, 786+ tables managed autonomously across 112+ PB.&lt;/p&gt;




&lt;h2&gt;
  
  
  Path 2: The manual approach
&lt;/h2&gt;

&lt;p&gt;Every optimization LakeOps automates is also achievable with SQL procedures, engine configuration, and scheduling. Here's how to implement each one by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Diagnosing the bottleneck
&lt;/h3&gt;

&lt;p&gt;Before optimizing, use Iceberg's metadata tables to identify what's actually wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- File health: count, size distribution, small file ratio&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_size_in_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avg_mb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;file_size_in_bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;67108864&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;small_files&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Manifest health: fragmentation&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;manifest_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;added_data_files_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avg_files_per_manifest&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Delete file accumulation (MoR tables)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;delete_file_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete_files&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Snapshot accumulation&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;snapshot_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;committed_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;oldest&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshots&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Thresholds:&lt;/strong&gt; avg file size &amp;lt; 128 MB → compaction needed. Manifest count &amp;gt; 200 → rewrite manifests. Delete files &amp;gt; 50 → compact to apply. Snapshots &amp;gt; 1,000 → expire.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;EXPLAIN&lt;/code&gt; to verify predicates push down to the scan level rather than appearing as residual filters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sort order configuration
&lt;/h3&gt;

&lt;p&gt;Choose columns based on your most common query patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Set sort order (first column gets tightest clustering)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WRITE&lt;/span&gt; &lt;span class="n"&gt;ORDERED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Sort compaction to apply the order to existing data&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'prod.db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sort'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sort_order&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'event_date ASC NULLS LAST, user_id ASC NULLS LAST'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- For tables with diverse multi-column filters, use Z-order&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'prod.db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sort'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sort_order&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'zorder(event_date, user_id, region)'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guidelines for choosing sort columns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First key:&lt;/strong&gt; Column most frequently in &lt;code&gt;WHERE&lt;/code&gt; clauses. Gets tightest min/max ranges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Second key:&lt;/strong&gt; Diminishing returns, but meaningful for two-column filters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Z-order:&lt;/strong&gt; When no single column dominates filter patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The challenge: you must manually identify which columns queries filter on, and update the sort order when access patterns change. Most teams don't revisit this after initial setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  File consolidation (compaction)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Binpack: merge small files by size (fast, no reordering)&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'prod.db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'binpack'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'268435456'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'min-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'67108864'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Sort compaction: merge + apply sort order (slower, better for queries)&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'prod.db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sort'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sort_order&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'event_date ASC NULLS LAST, user_id ASC NULLS LAST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'268435456'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Target: 256–512 MB files for analytical workloads. Schedule via Airflow/cron — daily for streaming tables, weekly for batch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preventing small files at write time
&lt;/h3&gt;

&lt;p&gt;The cheapest compaction is the one you never need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'write.target-file-size-bytes'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'268435456'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;-- 256 MB&lt;/span&gt;
  &lt;span class="s1"&gt;'write.distribution-mode'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'hash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;-- Group by partition before write&lt;/span&gt;
  &lt;span class="s1"&gt;'write.parquet.compression-codec'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'zstd'&lt;/span&gt;       &lt;span class="c1"&gt;-- Better compression&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; &lt;code&gt;write.distribution-mode = 'none'&lt;/code&gt; on partitioned tables is the #1 source of small files. With 200 Spark tasks writing to 365 daily partitions, you get 73,000 tiny files per job. Always use &lt;code&gt;hash&lt;/code&gt; for partitioned tables.&lt;/p&gt;

&lt;p&gt;For Spark, align AQE with your target file size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.adaptive.enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.adaptive.coalescePartitions.enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.adaptive.advisoryPartitionSizeInBytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;268435456&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Flink streaming writes, increase checkpoint intervals to reduce file frequency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="s1"&gt;'execution.checkpointing.interval'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5min'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="s1"&gt;'sink.committer.operator.commit-interval'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5min'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bloom filters for point lookups
&lt;/h3&gt;

&lt;p&gt;When queries use equality predicates on high-cardinality columns (&lt;code&gt;WHERE user_id = 12345&lt;/code&gt;), bloom filters eliminate row groups that provably don't contain the value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'write.parquet.bloom-filter-enabled.column.user_id'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'write.parquet.bloom-filter-enabled.column.session_id'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'write.parquet.bloom-filter-fpp.column.user_id'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'0.05'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Generate filters on existing data via compaction&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'prod.db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'write.parquet.bloom-filter-enabled.column.user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'write.parquet.bloom-filter-fpp.column.user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0.05'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Limit to 2–3 columns per table (each adds ~1 MB to every file footer). Only useful for equality predicates — range queries don't benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manifest consolidation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check if needed&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Consolidate (run AFTER compaction)&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_manifests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'prod.db.events'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Target: 10–50 manifests. Run after compaction stabilizes the file set — not before.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snapshot expiration and orphan cleanup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Expire snapshots older than 7 days, keep at least 5&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expire_snapshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'prod.db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;older_than&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-26 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;retain_last&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Remove orphan files from failed writes&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove_orphan_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'prod.db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;older_than&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-26 00:00:00'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guidelines: 3–7 days retention for streaming tables, 30–90 days for batch. Always &lt;code&gt;retain_last &amp;gt;= 2&lt;/code&gt; to avoid breaking concurrent readers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete file cleanup (MoR tables)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Compact to apply pending deletes&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'prod.db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'delete-file-threshold'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'3'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor regularly. A table accumulating 200+ delete files/day will see read times increase from 3s → 22s over two weeks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partition strategy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Set partition (at table creation or via evolution)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;iceberg&lt;/span&gt;
&lt;span class="n"&gt;PARTITIONED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- Evolve without data rewrite (metadata-only, instant)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;SPEC&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules: each partition should hold at least 128 MB. Daily partitioning on a 10 MB/day table → use monthly. &lt;code&gt;bucket(col, 1024)&lt;/code&gt; on a 50 GB table → try 16–64 buckets instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Puffin statistics for join planning
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Generate NDV statistics for cost-based optimization&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compute_table_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'prod.db.events'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Or via ANALYZE TABLE (Spark)&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;COMPUTE&lt;/span&gt; &lt;span class="k"&gt;STATISTICS&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;COLUMNS&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recompute after major data changes. For details: &lt;a href="https://lakeops.dev/blog/iceberg-puffin-statistics-guide" rel="noopener noreferrer"&gt;Puffin Statistics Guide&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parquet-level tuning
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'write.parquet.row-group-size-bytes'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'134217728'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- 128 MB row groups&lt;/span&gt;
  &lt;span class="s1"&gt;'write.parquet.page-size-bytes'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1048576'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 1 MB pages&lt;/span&gt;
  &lt;span class="s1"&gt;'write.parquet.dict-size-bytes'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2097152'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 2 MB dict threshold&lt;/span&gt;

  &lt;span class="c1"&gt;-- Disable stats for columns never filtered on (reduces manifest bloat)&lt;/span&gt;
  &lt;span class="s1"&gt;'write.metadata.metrics.column.raw_payload'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'write.metadata.metrics.column.debug_info'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For read-side tuning in Spark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.files.maxPartitionBytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;268435456&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 256 MB per task
&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.files.openCostInBytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4194304&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# 4 MB open cost
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Copy-on-Write vs. Merge-on-Read
&lt;/h3&gt;

&lt;p&gt;Choose your update strategy based on your read/write ratio:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Batch reporting (updated nightly, read all day)&lt;/td&gt;
&lt;td&gt;Copy-on-Write&lt;/td&gt;
&lt;td&gt;Zero read overhead, clean files always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming CDC (Debezium/Kafka)&lt;/td&gt;
&lt;td&gt;Merge-on-Read + compaction&lt;/td&gt;
&lt;td&gt;Write speed critical, compact to restore reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrequent GDPR/compliance deletes&lt;/td&gt;
&lt;td&gt;Merge-on-Read + periodic cleanup&lt;/td&gt;
&lt;td&gt;Fast compliance, clean up later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-frequency MERGE INTO&lt;/td&gt;
&lt;td&gt;Deletion Vectors (V3)&lt;/td&gt;
&lt;td&gt;Best write + read performance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The correct maintenance sequence
&lt;/h2&gt;

&lt;p&gt;Whether automated or manual, the order of operations matters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expire snapshots&lt;/strong&gt; → releases references to dead files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove orphans&lt;/strong&gt; → deletes unreferenced files from storage (cheaper subsequent compaction)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compact (with sort)&lt;/strong&gt; → merges files, applies sort order, resolves delete files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite manifests&lt;/strong&gt; → consolidates metadata over the clean, final file set&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running these out of order wastes compute. Compacting before expiring rewrites files that should be deleted. Manifest rewrites before compaction get invalidated immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance troubleshooting checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Slow range queries&lt;/td&gt;
&lt;td&gt;Unsorted data, many small files&lt;/td&gt;
&lt;td&gt;Sort compaction on filter columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow point lookups&lt;/td&gt;
&lt;td&gt;No bloom filters&lt;/td&gt;
&lt;td&gt;Enable per-column bloom filters + compact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow query planning (&amp;gt;5s)&lt;/td&gt;
&lt;td&gt;Too many manifests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rewrite_manifests&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full table scans&lt;/td&gt;
&lt;td&gt;Wrong partition scheme&lt;/td&gt;
&lt;td&gt;Partition evolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradually slowing reads&lt;/td&gt;
&lt;td&gt;Accumulating delete files&lt;/td&gt;
&lt;td&gt;Compact to apply deletes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High S3 API costs&lt;/td&gt;
&lt;td&gt;Thousands of small files&lt;/td&gt;
&lt;td&gt;Compaction + write-side tuning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard timeouts&lt;/td&gt;
&lt;td&gt;Multiple degradation factors&lt;/td&gt;
&lt;td&gt;Full maintenance pipeline + routing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Choosing your path
&lt;/h2&gt;

&lt;p&gt;The manual path works. Every SQL procedure shown above is production-proven. The challenge is sustaining it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choosing the right sort order requires analyzing query logs across all engines — and updating when patterns change&lt;/li&gt;
&lt;li&gt;Scheduling compaction requires different frequencies for different tables — streaming tables need it hourly, batch tables weekly&lt;/li&gt;
&lt;li&gt;Sequencing operations correctly requires understanding their dependencies&lt;/li&gt;
&lt;li&gt;Monitoring 50+ tables for degradation signals requires dashboard infrastructure&lt;/li&gt;
&lt;li&gt;Scaling to hundreds of tables means the maintenance burden grows linearly with your data estate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For teams with fewer than 10–20 tables and stable access patterns, the manual path is manageable. Beyond that, the operational burden compounds — and the sort order inevitably goes stale because nobody has time to analyze query patterns per table per quarter.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;control plane approach&lt;/a&gt; eliminates this scaling problem. It handles the continuous observation, adaptation, and execution at any table count — and because its compaction engine is 95% faster than Spark, it can apply sort compaction at frequencies that would be cost-prohibitive manually.&lt;/p&gt;

&lt;p&gt;Both paths lead to the same destination: tables where queries are 12x faster because the physical layout matches how data is actually accessed. The question is whether you want that as a continuous property of your lake, or as a point-in-time optimization that needs periodic human re-tuning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Iceberg query performance is determined by the physical state of your tables — not by the format spec, not by the engine, and not by hardware. A well-maintained table is fast on every engine. A degraded table is slow on all of them.&lt;/p&gt;

&lt;p&gt;The physical levers that matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sort order aligned to query patterns&lt;/strong&gt; — 51% less data scanned, up to 12x faster queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File consolidation&lt;/strong&gt; — 5–10x faster planning, reduced I/O&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bloom filters&lt;/strong&gt; — near-instant elimination for equality lookups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifest optimization&lt;/strong&gt; — 2–5x faster planning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-side configuration&lt;/strong&gt; — prevent degradation at source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete file cleanup&lt;/strong&gt; — restore read performance on MoR tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot lifecycle&lt;/strong&gt; — lean metadata, lower storage costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partition strategy&lt;/strong&gt; — coarse-grained elimination at the foundation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-engine routing&lt;/strong&gt; — right engine per query, up to 56% cost reduction&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The intelligent path keeps all of these optimal continuously. The manual path gives you the tools to do it yourself. Either way — the performance ceiling is the physical state of your tables, and now you know how to raise it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Further reading:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lakeops.dev/blog/optimizing-iceberg-lakehouse-performance" rel="noopener noreferrer"&gt;Optimizing Iceberg Lakehouse Performance — Layers That Compound&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lakeops.dev/blog/apache-iceberg-query-planning" rel="noopener noreferrer"&gt;Apache Iceberg Query Planning Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lakeops.dev/blog/iceberg-compaction-strategies" rel="noopener noreferrer"&gt;Iceberg Compaction Strategies: A Practical Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lakeops.dev/blog/iceberg-small-files-guide" rel="noopener noreferrer"&gt;Fixing Small Files in Apache Iceberg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lakeops.dev/blog/routing-multiple-query-engines-with-iceberg" rel="noopener noreferrer"&gt;Routing Multiple Query Engines with Iceberg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lakeops.dev/blog/iceberg-puffin-statistics-guide" rel="noopener noreferrer"&gt;Apache Iceberg Puffin Statistics Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lakeops.dev/blog/iceberg-delete-files-merge-on-read" rel="noopener noreferrer"&gt;Apache Iceberg Delete Files: Reducing MoR Overhead&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>data</category>
      <category>dataengineering</category>
      <category>devops</category>
      <category>programmers</category>
    </item>
    <item>
      <title>AI Data Lake and Lakehouse</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Fri, 24 Jul 2026 14:07:16 +0000</pubDate>
      <link>https://dev.to/jonisar/ai-data-lake-and-lakehouse-gbi</link>
      <guid>https://dev.to/jonisar/ai-data-lake-and-lakehouse-gbi</guid>
      <description>&lt;p&gt;The word "AI" appears in every data platform pitch now. Most of the time, it means nothing — a chatbot bolted onto a dashboard, an LLM that generates SQL, or a marketing slide that says "AI-powered" next to a cron job.&lt;/p&gt;

&lt;p&gt;This article is about something different: what happens when you put actual intelligence — machine learning, adaptive optimization, closed-loop feedback systems — at the core of how a data lakehouse operates. Not AI as a feature. AI as the operating model.&lt;/p&gt;

&lt;p&gt;The result is a lakehouse that understands its own workloads, optimizes its own storage layouts, routes its own queries, heals its own tables, and continuously improves — without human intervention. That is what an AI data lake actually is. Everything else is automation with better branding.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Automation Is Not Intelligence&lt;/li&gt;
&lt;li&gt;The AI Control Plane&lt;/li&gt;
&lt;li&gt;How AI Transforms Each Layer of the Lakehouse&lt;/li&gt;
&lt;li&gt;AI-Driven Compaction: Learning How Data Is Read&lt;/li&gt;
&lt;li&gt;AI-Driven Maintenance: Predicting What Breaks&lt;/li&gt;
&lt;li&gt;AI-Driven Query Routing: Matching Queries to Engines&lt;/li&gt;
&lt;li&gt;AI-Driven Observability: From Dashboards to Diagnosis&lt;/li&gt;
&lt;li&gt;AI-Driven Governance: Policies That Adapt&lt;/li&gt;
&lt;li&gt;The Closed Loop: How Every Query Makes the Lake Smarter&lt;/li&gt;
&lt;li&gt;AI Agents as Lake Citizens&lt;/li&gt;
&lt;li&gt;AI-Native vs. AI-Washed: How to Tell the Difference&lt;/li&gt;
&lt;li&gt;The Compounding Effect&lt;/li&gt;
&lt;li&gt;Where This Is Heading&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Automation Is Not Intelligence
&lt;/h2&gt;

&lt;p&gt;Most data lake "automation" today works like this: a cron job runs compaction every night at 2 AM. It merges small files into bigger ones. It does not know if the table needs compaction. It does not know which partitions are degraded. It does not know what columns queries filter on. It does not know whether last night's compaction actually helped.&lt;/p&gt;

&lt;p&gt;That is automation. It follows a fixed rule, regardless of context.&lt;/p&gt;

&lt;p&gt;Intelligence is different. Intelligence observes, learns, decides, acts, measures the outcome, and adjusts. A system that compacts a table because its file health crossed a threshold, sorts by the columns that real queries actually filter on, sequences the operation after snapshot expiration to avoid wasted work, and then verifies that query latency improved — &lt;em&gt;that&lt;/em&gt; is intelligent.&lt;/p&gt;

&lt;p&gt;The distinction matters because data lakes are not static systems. Workloads shift. New dashboards appear. AI agents start querying tables that were designed for nightly batch. Streaming pipelines change write patterns. A schema evolves.&lt;/p&gt;

&lt;p&gt;Fixed rules cannot keep up. Intelligence can.&lt;/p&gt;




&lt;h2&gt;
  
  
  The AI Control Plane
&lt;/h2&gt;

&lt;p&gt;An AI data lake needs a brain — a system that continuously ingests telemetry from every table, every engine, and every query, and uses that signal to drive optimization decisions across the entire lake.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; is that system. It is an autonomous control plane for Apache Iceberg that puts AI at the center of lakehouse operations — compaction, maintenance, query routing, observability, governance, and agent enablement, all driven by continuous learning rather than static rules.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3cpc37zlrlb53gq51892.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3cpc37zlrlb53gq51892.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What makes it a &lt;em&gt;control plane&lt;/em&gt; and not another tool: LakeOps is not an engine, not a catalog, and not storage. It connects to whatever you already run — AWS Glue, Polaris, Nessie, Gravitino, Lakekeeper, or S3 Tables as catalogs; Trino, Spark, Snowflake, Athena, DuckDB, Flink as engines; S3, GCS, or ADLS as storage — and adds the intelligence layer on top. No data movement. No code changes. No vendor lock-in. Setup takes about ten minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7o3csmexsm63b5s9gb8x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7o3csmexsm63b5s9gb8x.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What makes it &lt;em&gt;AI-native&lt;/em&gt;: every decision LakeOps makes is informed by learned behavior, not hardcoded rules. It observes query patterns across all engines, predicts table degradation from structural signals, adapts data layouts as workloads shift, learns which engine performs best for each query shape, and measures the outcome of every optimization to improve the next one. It also exposes a native &lt;a href="https://lakeops.dev/solutions/agentic-ai" rel="noopener noreferrer"&gt;MCP (Model Context Protocol)&lt;/a&gt; server so AI agents can query the lake directly — with schema discovery, intelligent routing, and layered guardrails built in. The more the lake is used — by humans and agents alike — the smarter the control plane gets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F83xp8kuc4r1kzpum7tpq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F83xp8kuc4r1kzpum7tpq.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three operating modes let teams choose how much autonomy the AI gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autopilot&lt;/strong&gt; — the AI decides and executes. Define policies and thresholds; the system handles everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendations&lt;/strong&gt; — the AI analyzes and recommends with full context (what, why, expected impact). A human approves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy-bounded&lt;/strong&gt; — the AI operates within declared boundaries, escalating edge cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F161x2275c2ct1v1a1du7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F161x2275c2ct1v1a1du7.png" alt=" " width="800" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most teams start with recommendations to build trust, then move tables to autopilot as confidence grows. The intelligence is the same in all modes — the only difference is who clicks "execute."&lt;/p&gt;

&lt;p&gt;With that foundation, here is how AI transforms each layer of lakehouse operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  How AI Transforms Each Layer of the Lakehouse
&lt;/h2&gt;

&lt;p&gt;When we talk about an "AI data lake," we are talking about specific capabilities that require learning, prediction, and adaptation — not features that could be implemented with a shell script.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What AI Does&lt;/th&gt;
&lt;th&gt;What Scripts Cannot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compaction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Learns query patterns across engines, sorts data by the columns actually used in WHERE/JOIN/GROUP BY, adapts as patterns change&lt;/td&gt;
&lt;td&gt;Static sort keys, fixed file-size targets, blind to query patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Predicts which tables will degrade next, sequences operations by dependency, prioritizes by business impact&lt;/td&gt;
&lt;td&gt;Fixed schedules, independent jobs, no awareness of health trajectory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query routing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Learns which engine performs best for each query shape, uses LLMs to reason about new templates, adapts as tables improve&lt;/td&gt;
&lt;td&gt;Static rules, manual engine assignment, no cost optimization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Correlates structural signals with query performance, predicts degradation before user impact&lt;/td&gt;
&lt;td&gt;Threshold alerts, no predictive capability, no cross-signal correlation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Governance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adapts policy enforcement to workload patterns, recommends changes from observed behavior&lt;/td&gt;
&lt;td&gt;Static policies, manual updates, no feedback from operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Feeds agent telemetry back into optimization, shapes the lake around AI access patterns&lt;/td&gt;
&lt;td&gt;No awareness of agents, no closed-loop optimization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of these deserves a deeper look.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI-Driven Compaction: Learning How Data Is Read
&lt;/h2&gt;

&lt;p&gt;Traditional compaction is a janitor: it sweeps up small files and merges them. It does not know &lt;em&gt;why&lt;/em&gt; the files exist or &lt;em&gt;how&lt;/em&gt; they are read. An intelligent compaction engine does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4lg12nx6xse33fatq7gf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4lg12nx6xse33fatq7gf.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Query Pattern Analysis
&lt;/h3&gt;

&lt;p&gt;LakeOps continuously observes every query that hits every table — across Trino, Spark, Snowflake, Athena, DuckDB, Flink, and any other connected engine. It extracts which columns appear in &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, and &lt;code&gt;GROUP BY&lt;/code&gt; clauses. It tracks how these patterns change over time. It weights recent queries more heavily than old ones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnxp3scbxdhvln64vaymu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnxp3scbxdhvln64vaymu.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not a one-time analysis. It is a continuous learning loop.&lt;/p&gt;

&lt;p&gt;From this analysis, the AI knows — per table — which columns matter most for data layout. A table where 80% of queries filter on &lt;code&gt;event_date&lt;/code&gt; and &lt;code&gt;region&lt;/code&gt; should have its data physically sorted by those columns. A table where queries mostly join on &lt;code&gt;customer_id&lt;/code&gt; should sort differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  From Analysis to Action
&lt;/h3&gt;

&lt;p&gt;During compaction, data files are physically re-sorted by the learned columns. The result: Parquet row-group min/max statistics become effective, and engines skip irrelevant data without reading it. Predicate pushdown and column pruning actually work — not because someone manually configured sort keys, but because the AI figured out what the right sort order is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3fhqdtq8nmmm6rw9cai.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3fhqdtq8nmmm6rw9cai.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On production tables, this AI-driven sorting reduces data scanned by &lt;strong&gt;51%&lt;/strong&gt; and delivers &lt;strong&gt;12x faster queries&lt;/strong&gt; compared to unsorted compaction. The numbers are large because the difference between "sort by whatever was configured at table creation" and "sort by what queries actually need" is enormous.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adaptive Sort Evolution
&lt;/h3&gt;

&lt;p&gt;Here is where intelligence really separates from automation. When query patterns shift — a new dashboard starts filtering on &lt;code&gt;product_category&lt;/code&gt; instead of &lt;code&gt;region&lt;/code&gt;, a data science team changes their join keys, AI agents start accessing different columns — the sort strategy evolves on the next compaction pass. No configuration change. No ticket. No human intervention.&lt;/p&gt;

&lt;p&gt;A table whose workload stabilizes gets compacted less frequently — the AI recognizes that the current layout is already optimal and avoids redundant work. A table whose workload spikes gets prioritized. This is resource allocation driven by learned behavior, not fixed schedules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed as an AI Enabler
&lt;/h3&gt;

&lt;p&gt;The AI's compaction decisions need an engine fast enough to execute them continuously — not queue them for a nightly batch window. LakeOps runs compaction on a purpose-built Rust engine powered by Apache DataFusion: &lt;strong&gt;221 seconds&lt;/strong&gt; where Spark takes &lt;strong&gt;1,612 seconds&lt;/strong&gt; (95% faster), at roughly &lt;strong&gt;$5/TB versus $50/TB&lt;/strong&gt; (90% cheaper). Peak throughput reaches 2,522 MB/s. Tables that OOM Spark — a 1.2 TB table with 12,000 files, for example — complete without special configuration because memory is bounded, not heap-dependent.&lt;/p&gt;

&lt;p&gt;That speed is not just a performance metric. It is the enabler for AI-driven compaction: when compaction takes minutes instead of hours, the system can respond to workload changes on the same day they happen rather than falling behind. The AI can iterate. Fast execution makes continuous learning practical.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI-Driven Maintenance: Predicting What Breaks
&lt;/h2&gt;

&lt;p&gt;Iceberg tables require six maintenance operations: data compaction, snapshot expiration, manifest consolidation, orphan cleanup, position delete resolution, and statistics computation. Every platform team knows this. The question is &lt;em&gt;when&lt;/em&gt; and &lt;em&gt;in what order&lt;/em&gt; to run them — and that is where AI changes the game.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fchs7ffjzt0ntuo77y14w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fchs7ffjzt0ntuo77y14w.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Health Prediction, Not Health Reaction
&lt;/h3&gt;

&lt;p&gt;Traditional monitoring tells you a table is degraded &lt;em&gt;after&lt;/em&gt; queries slow down. By then, users have already filed tickets and data pipelines are late.&lt;/p&gt;

&lt;p&gt;LakeOps predicts degradation before it hits. The AI continuously scores every table on structural signals — file count trajectory, partition balance trends, manifest growth rate, snapshot accumulation velocity, delete-file ratios. It sees the pattern: "this table's small-file ratio is climbing at 4% per day; at current trajectory it will hit Critical in 72 hours."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftq8qb35i9jncnjkc60h8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftq8qb35i9jncnjkc60h8.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a classification and prediction problem that humans cannot solve manually at scale. A platform team with 800 tables across multiple catalogs cannot eyeball metadata trends for each one. An AI system that ingests every commit, every query, and every structural change can.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intelligent Sequencing
&lt;/h3&gt;

&lt;p&gt;The order of maintenance operations matters. Running orphan cleanup before snapshot expiration risks deleting referenced files. Running manifest rewrites before compaction wastes the rewrite since compaction will invalidate file references.&lt;/p&gt;

&lt;p&gt;The AI learns the correct dependency graph and sequences operations per table:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compaction first (merge small files, apply learned sort order)&lt;/li&gt;
&lt;li&gt;Snapshot expiration after (remove old references safely, respecting active readers)&lt;/li&gt;
&lt;li&gt;Manifest consolidation next (consolidate after file references stabilize)&lt;/li&gt;
&lt;li&gt;Orphan cleanup last (reclaim newly unreferenced storage)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not just a fixed ordering — the AI evaluates &lt;em&gt;whether&lt;/em&gt; each step is needed based on current table state. A table with healthy manifests skips manifest consolidation. A table with no orphans skips cleanup. The maintenance plan is tailored per table, per cycle, based on real signals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operation Outcome Learning
&lt;/h3&gt;

&lt;p&gt;After each operation completes, the AI measures the outcome: did compaction actually reduce query latency? Did snapshot expiration free the expected storage? Did manifest consolidation improve planning time?&lt;/p&gt;

&lt;p&gt;These outcomes feed back into future decisions. If compacting a particular table's hot partition consistently delivers a 5x query speedup, the system learns to prioritize that partition. If expiring snapshots on a rarely-queried table has minimal impact, it deprioritizes that work. Over time, the maintenance engine gets &lt;em&gt;better at deciding what to do&lt;/em&gt; — not just executing a predefined list.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI-Driven Query Routing: Matching Queries to Engines
&lt;/h2&gt;

&lt;p&gt;Most production lakehouses use multiple query engines. Trino for interactive analytics. Spark for heavy ETL. Snowflake for BI dashboards. DuckDB for ad-hoc exploration. Athena for pay-per-query workloads. The problem is deciding which query goes where.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdamwvmo8hwnj9guvxybm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdamwvmo8hwnj9guvxybm.png" alt=" " width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without intelligence, this is a manual decision — or worse, no decision at all, and every query hits the same engine regardless of whether it is a 50 MB selective filter or a 500 GB full table scan.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Three-Router AI Stack
&lt;/h3&gt;

&lt;p&gt;LakeOps implements intelligent routing through &lt;a href="https://queryflux.dev" rel="noopener noreferrer"&gt;QueryFlux&lt;/a&gt;, an open-source Rust-based SQL proxy, extended with a three-layer AI stack:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Adaptive routing (learned behavior).&lt;/strong&gt; The AI tracks historical execution data for every query template — which engine ran it, how long it took, how much it cost, whether it succeeded. For previously seen query shapes, routing is instant: the system already knows which engine delivers the best cost-performance ratio. Cached decisions take &lt;strong&gt;0ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3pq94ombi6xgm7j2ft0g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3pq94ombi6xgm7j2ft0g.png" alt=" " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. LLM-based routing (reasoning).&lt;/strong&gt; For new query templates the system has not seen before, an LLM reasons over the query structure combined with live table statistics — file count, data size, sort order, partition layout, health score. It predicts which engine is the best fit. This handles the cold-start problem that pure historical routing cannot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Semantic routing (intent matching).&lt;/strong&gt; When neither historical data nor structural analysis alone is sufficient, semantic matching infers the query's intent — exploratory analysis, dashboard refresh, ETL pipeline, or AI agent retrieval — and routes accordingly.&lt;/p&gt;

&lt;p&gt;The three routers compose. Over time, more query templates enter the adaptive cache and routing gets faster and more accurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data-Quality-Aware Routing
&lt;/h3&gt;

&lt;p&gt;This is where AI routing connects to AI compaction and AI observability. The routing layer knows — from LakeOps's health scoring — whether a table is well-compacted, fragmented, or degraded. It adjusts routing decisions accordingly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A well-compacted table with good sort order? Route to a lightweight, cheaper engine that benefits from efficient data skipping.&lt;/li&gt;
&lt;li&gt;A degraded table with 40,000 small files? Route to an engine that can handle the overhead while compaction runs in the background.&lt;/li&gt;
&lt;li&gt;A table that was just compacted and is now healthy? Update the routing weights so future queries use cheaper engines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As the AI compaction engine improves table health, the AI routing engine shifts queries to cheaper engines. The cost savings compound. Production benchmarks show &lt;strong&gt;up to 56% workload cost reduction&lt;/strong&gt; from intelligent routing alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI-Driven Observability: From Dashboards to Diagnosis
&lt;/h2&gt;

&lt;p&gt;Traditional observability is a dashboard: it shows metrics, you interpret them. AI-driven observability is a diagnostic system: it interprets the metrics &lt;em&gt;for you&lt;/em&gt;, tells you what is wrong, explains why, and either fixes it or recommends a fix.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fglhsy0o91h3rvo772if4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fglhsy0o91h3rvo772if4.png" alt=" " width="799" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-Signal Correlation
&lt;/h3&gt;

&lt;p&gt;LakeOps does not monitor file counts and query latency independently. The AI correlates signals across layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage signals&lt;/strong&gt; — file count, size distribution, access patterns, orphan volume&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata signals&lt;/strong&gt; — manifest count and depth, snapshot velocity, partition skew, statistics coverage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engine signals&lt;/strong&gt; — query latency, scan volume, CPU utilization, per-engine field access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operation signals&lt;/strong&gt; — maintenance duration, compaction throughput, health score changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From these, it produces actionable insights with causal chains. Example: "Query latency on &lt;code&gt;raw_clickstream&lt;/code&gt; increased 8x because 312 partitions exceed the file count threshold — scan amplification is 8x. Compaction is recommended and will reduce query time to baseline."&lt;/p&gt;

&lt;p&gt;That is not a threshold alert. That is a diagnosis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Severity-Ranked Insights
&lt;/h3&gt;

&lt;p&gt;The AI surfaces per-table findings at four severity levels — CRITICAL, HIGH, WARNING, LOW — each with the specific metric, current value, threshold violated, and a remediation action. Critical and High Insights trigger automated remediation. Warning and Low Insights surface for human review.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Finoqyvskn0a0cnfndjrl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Finoqyvskn0a0cnfndjrl.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key AI element: the system does not just check "is file count above X?" It evaluates the &lt;em&gt;combination&lt;/em&gt; of signals. A table with many small files but no queries gets a lower severity than a table with fewer small files but high query frequency. Business impact is inferred from workload patterns, not just structural metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Predictive Degradation
&lt;/h3&gt;

&lt;p&gt;Instead of alerting after a table degrades, the AI predicts &lt;em&gt;when&lt;/em&gt; it will degrade based on trajectory. A table whose snapshot accumulation rate will hit its retention limit in 48 hours triggers an alert now — with enough lead time to act before users notice.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI-Driven Governance: Policies That Adapt
&lt;/h2&gt;

&lt;p&gt;Static governance policies are set-and-forget — which usually means set-and-break. Workloads change, new tables appear, query patterns shift, and the policies that made sense six months ago no longer match reality.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foszx6ldvck0kuvfj76zo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foszx6ldvck0kuvfj76zo.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Intelligent Policy Recommendations
&lt;/h3&gt;

&lt;p&gt;LakeOps's AI observes actual workload behavior and recommends policy adjustments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Table &lt;code&gt;orders_archive&lt;/code&gt; has not been queried in 90 days. Current retention policy keeps 30 days of snapshots. Recommend reducing to 7 days to reclaim 18 GB of metadata."&lt;/li&gt;
&lt;li&gt;"Namespace &lt;code&gt;analytics.*&lt;/code&gt; consistently produces small files from streaming writes. Recommend adding an auto-compaction policy with a 1-hour trigger window."&lt;/li&gt;
&lt;li&gt;"Table &lt;code&gt;customer_events&lt;/code&gt; is queried by 3 AI agents with PII columns. Recommend adding PIIMaskGuard policy to agent-facing routing groups."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not generic suggestions. They are derived from observed behavior — query patterns, write patterns, access patterns, and health trajectories specific to each table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fefratr2eh7igdbia0exu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fefratr2eh7igdbia0exu.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Policy Inheritance with Workload Awareness
&lt;/h3&gt;

&lt;p&gt;Policies cascade from organization to catalog to namespace to table — but the AI ensures inheritance makes sense. A compaction policy designed for batch tables is not blindly applied to streaming tables in the same namespace. The system flags conflicts between inherited policies and actual workload patterns, and either adjusts automatically (in autopilot mode) or recommends changes (in manual mode).&lt;/p&gt;

&lt;p&gt;Policies apply uniformly across catalogs — AWS Glue, Polaris, Nessie, Gravitino, Lakekeeper — without per-catalog scripts or engine-specific logic. Every policy is auditable, versioned, and controllable with a single toggle.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Closed Loop: How Every Query Makes the Lake Smarter
&lt;/h2&gt;

&lt;p&gt;The most important concept in an AI data lake is the &lt;strong&gt;closed-loop feedback system&lt;/strong&gt;. This is what separates an intelligent system from a collection of automated scripts — and it is the core of how LakeOps operates.&lt;/p&gt;

&lt;p&gt;Here is the loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    Queries hit the lake
    (engines, agents, dashboards, pipelines)
            │
            ▼
    Telemetry captures patterns
    (columns, engines, latency, cost, frequency)
            │
            ▼
    AI analyzes and learns
    (sort order, routing weights, health scores,
     maintenance priorities, policy fitness)
            │
            ▼
    Optimizer acts
    (compact with new sort, reroute queries,
     adjust policies, trigger maintenance)
            │
            ▼
    Outcomes measured
    (query faster? cost lower? health improved?)
            │
            ▼
    Learnings feed back ──────► next cycle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every query — whether from a BI dashboard, a Spark ETL job, or an AI agent — feeds signal into this loop. The more the lake is used, the better it gets at organizing itself. This is the defining characteristic of an AI data lake: &lt;strong&gt;usage improves performance&lt;/strong&gt;, rather than degrading it.&lt;/p&gt;

&lt;p&gt;In a traditional lake, usage degrades performance — more queries means more load, more writes means more small files, more engines means more fragmentation. In an AI data lake, usage is signal. More queries means better sort-order decisions. More engines means richer routing data. More writes means more opportunities for the optimizer to learn and act.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI Agents as Lake Citizens
&lt;/h2&gt;

&lt;p&gt;AI agents introduce a fundamentally new workload pattern. They issue SQL iteratively — dozens of queries per reasoning step — at high frequency, without human review, with expectations of sub-second latency. A single customer-support agent answering one ticket can issue 30–50 SQL statements across user history, orders, product metadata, and support logs. Multiply that across hundreds of concurrent agent sessions and the workload profile looks nothing like the BI traffic your lake was designed for.&lt;/p&gt;

&lt;p&gt;Read: &lt;a href="https://lakeops.dev/blog/routing-multiple-query-engines-with-iceberg" rel="noopener noreferrer"&gt;Routing Multiple Query Engines with Iceberg&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Infrastructure Problem
&lt;/h3&gt;

&lt;p&gt;Agents querying uncompacted tables pay a &lt;strong&gt;5–10x latency penalty&lt;/strong&gt;. When an agent runs 12 sequential queries in a reasoning loop, each 3-second query delay adds up to 36 seconds of wall-clock time for a single interaction. Users perceive this as the agent being broken. And unlike a human analyst who learns to avoid expensive queries, an agent in a loop repeats the same mistake indefinitely — compounding costs every iteration.&lt;/p&gt;

&lt;h3&gt;
  
  
  The LakeOps MCP: Agent-Native Connectivity
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://lakeops.dev/solutions/agentic-ai" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; is the emerging standard for how AI agents interact with data infrastructure — filling the same role for agent-to-lakehouse communication that REST catalogs fill for engine-to-catalog communication.&lt;/p&gt;

&lt;p&gt;Read: &lt;a href="https://lakeops.dev/blog/mcp-iceberg-lakeops" rel="noopener noreferrer"&gt;Agentic AI MCP for Iceberg&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LakeOps exposes a native MCP server that any compatible agent — Claude, LangChain, LlamaIndex, or custom — can connect to with zero integration code. The MCP server provides four purpose-built tools rather than a generic "run anything" endpoint (agents that get a single &lt;code&gt;execute_sql&lt;/code&gt; tool tend to skip schema discovery and hallucinate table names):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;list_schemas&lt;/code&gt;&lt;/strong&gt; — returns all available schemas and databases across connected engines. Agents call this before constructing queries to avoid guessing at namespace structures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;describe_table&lt;/code&gt;&lt;/strong&gt; — returns column names, types, partition specs, sort orders, and optionally sample rows in a single call. Eliminates the multiple round-trips agents typically make to understand a table's shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;execute_query&lt;/code&gt;&lt;/strong&gt; — runs SQL through the full routing and guardrail pipeline, with optional &lt;code&gt;engine_hint&lt;/code&gt; and enforced &lt;code&gt;max_rows&lt;/code&gt; to prevent unbounded result sets from flooding the LLM context window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;explain_query&lt;/code&gt;&lt;/strong&gt; — returns estimated cost, row count, and execution plan without running the query. Agents use this for pre-flight cost checks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each routing endpoint gets a stable URL — for example, &lt;code&gt;storefront-analytics.lakeops.dev&lt;/code&gt;. Agents connect to that URL and inherit the full policy stack (guardrails, routing rules, cost limits) automatically. No per-agent configuration. Wire compatibility spans PostgreSQL, MySQL, and Arrow Flight SQL, plus async query support with SSE streaming so agents in tool-use loops can handle long-running queries gracefully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intelligent Guardrails
&lt;/h3&gt;

&lt;p&gt;Unsupervised agents need enforcement at the query level, not at the application level. LakeOps provides a composable guard chain — every query passes through it regardless of which agent or frontend issued it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ReadOnlyGuard&lt;/strong&gt; — blocks DDL and DML from agent sessions using SQL parsing (not string matching), catching CTEs that wrap mutations and function calls with side effects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RowLimitGuard&lt;/strong&gt; — injects &lt;code&gt;LIMIT N&lt;/code&gt; when a query lacks one. Agents in reasoning loops often forget to limit results, leading to multi-gigabyte result sets that overflow LLM context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CostEstimateGuard&lt;/strong&gt; — issues EXPLAIN before execution and rejects queries where estimated scanned bytes exceed a threshold. This is the primary defense against the cartesian-join and missing-WHERE patterns that agents frequently generate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIIMaskGuard&lt;/strong&gt; — rewrites queries to exclude, hash, or null-out columns tagged as sensitive. PII never enters the LLM context window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HumanApprovalGuard&lt;/strong&gt; — pauses high-stakes queries and sends a webhook (Slack, email, or custom) for manual review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Guards stack per routing group. A typical agent-facing configuration: &lt;code&gt;read_only → row_limit → cost_estimate → pii_mask&lt;/code&gt;. A data engineering agent gets: &lt;code&gt;cost_estimate → human_approval&lt;/code&gt; for DDL. Every guard action is recorded with full auditability — what fired, what was rewritten, what was rejected.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI Solving the AI Problem
&lt;/h3&gt;

&lt;p&gt;This is where the AI data lake concept becomes recursive. The same intelligence that optimizes the lake for human workloads also optimizes it for AI workloads — and agents produce particularly rich signal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Higher query frequency&lt;/strong&gt; — more data points for the adaptive optimizer per unit of time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictable patterns&lt;/strong&gt; — roughly 80% of agent queries are repeated templates (same SQL structure, different literal values), making sort-order learning fast&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent context propagation&lt;/strong&gt; — every query carries structured metadata (&lt;code&gt;agent_id&lt;/code&gt;, &lt;code&gt;conversation_id&lt;/code&gt;, &lt;code&gt;step_index&lt;/code&gt;, &lt;code&gt;tool_call_id&lt;/code&gt;) that enables per-agent cost attribution, session replay for debugging, and workload-specific routing optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The feedback loop tightens: agents query the lake → telemetry captures their patterns → the AI reshapes data to match → agents get faster results → the agents produce even better signal → the cycle accelerates. Production deployments show agent query p95 latency dropping from 5–10 seconds to &lt;strong&gt;under 500ms&lt;/strong&gt; as tables are compacted and sorted for observed access patterns, with per-query compute cost dropping &lt;strong&gt;65%&lt;/strong&gt; through intelligent routing.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI-Native vs. AI-Washed: How to Tell the Difference
&lt;/h2&gt;

&lt;p&gt;Not every product that claims "AI-powered" actually uses intelligence. Here is how to evaluate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;AI-Native&lt;/th&gt;
&lt;th&gt;AI-Washed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compaction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Learns sort order from cross-engine query patterns; adapts as patterns change&lt;/td&gt;
&lt;td&gt;Fixed sort keys configured manually; schedule-based execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Predicts degradation; sequences by dependency; learns from outcomes&lt;/td&gt;
&lt;td&gt;Runs on cron; independent jobs; no feedback loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Routing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Learns cost-performance per query shape; uses LLM for cold-start; adapts weights&lt;/td&gt;
&lt;td&gt;Static rules; manual engine assignment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Correlates signals across layers; predicts issues; diagnoses root cause&lt;/td&gt;
&lt;td&gt;Threshold alerts; dashboard without interpretation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Governance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Recommends policy changes from observed behavior; adapts inheritance&lt;/td&gt;
&lt;td&gt;Static policies; manual updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Feedback loop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Closed-loop: every query improves the system&lt;/td&gt;
&lt;td&gt;Open-loop: no learning from outcomes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native MCP with intelligent guardrails and self-optimization&lt;/td&gt;
&lt;td&gt;REST API bolt-on; no workload-aware optimization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The litmus test: &lt;strong&gt;does the system get better over time without human configuration changes?&lt;/strong&gt; If yes, there is actual intelligence. If no, it is automation with a marketing label.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Compounding Effect
&lt;/h2&gt;

&lt;p&gt;The individual components are valuable. The compound effect is transformational.&lt;/p&gt;

&lt;p&gt;AI-driven compaction reduces data scanned by 51%. That means every query across every engine benefits. Routing then shifts those improved queries to cheaper engines, because well-compacted tables do not need heavyweight compute. Storage costs drop because compaction reduces total file count and orphan cleanup reclaims unreferenced data. Observability improves because the AI has better signal to work with. Agents respond faster, which means fewer retries, lower token costs, and better AI application quality.&lt;/p&gt;

&lt;p&gt;Each AI component amplifies the others. Better compaction → better routing → lower cost → more headroom for AI agents → richer signal → better compaction. The loop runs continuously.&lt;/p&gt;

&lt;p&gt;Production numbers from LakeOps deployments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;80% reduction&lt;/strong&gt; in compute and storage costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12x faster queries&lt;/strong&gt; after AI-driven compaction and sorting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;95% faster compaction&lt;/strong&gt; than Spark (221s vs. 1,612s on 200 GB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;90% cheaper&lt;/strong&gt; compaction ($5/TB vs. $50/TB for Spark)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;56% workload cost reduction&lt;/strong&gt; from intelligent routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;100% of tables&lt;/strong&gt; continuously monitored and maintained&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~10 minutes&lt;/strong&gt; to connect and start optimizing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These numbers compound because the system is a closed loop, not a collection of independent tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This Is Heading
&lt;/h2&gt;

&lt;p&gt;The trajectory is clear. Data lakehouses are moving from manually operated infrastructure to self-managing systems where intelligence handles the operational layer end-to-end.&lt;/p&gt;

&lt;p&gt;Today, LakeOps's AI handles compaction, maintenance sequencing, query routing, health prediction, and policy adaptation. The direction extends to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic schema evolution&lt;/strong&gt; — the AI detects that a new column is consistently added to queries and recommends schema changes or materialized views&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictive capacity planning&lt;/strong&gt; — forecasting storage and compute needs based on workload growth trends and seasonal patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-lake optimization&lt;/strong&gt; — intelligence that spans multiple lakes, regions, and clouds, optimizing data placement and replication based on global access patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent-native data contracts&lt;/strong&gt; — AI agents negotiate data freshness, latency, and cost SLAs directly with the control plane, and the system optimizes to meet them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The foundation for all of this is the closed-loop feedback system. A lake that learns from every query is a lake that can evolve to meet requirements that do not yet exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;An AI data lake is not a data lake with an AI chatbot. It is a data lake where intelligence drives operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI decides how to compact&lt;/strong&gt; — learning from cross-engine query patterns, not static configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI decides when to maintain&lt;/strong&gt; — predicting degradation, sequencing by dependency, learning from outcomes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI decides where to route&lt;/strong&gt; — matching query shapes to engines using learned cost-performance models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI diagnoses problems&lt;/strong&gt; — correlating signals across storage, metadata, engines, and operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI adapts governance&lt;/strong&gt; — recommending policy changes based on observed behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI optimizes for agents&lt;/strong&gt; — feeding agent telemetry back into the optimization loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The closed-loop feedback system is the defining characteristic. Usage improves performance. Every query makes the lake smarter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; is the autonomous control plane that implements this intelligence stack for Apache Iceberg — connecting to your existing catalogs, engines, and storage in minutes, with no data movement and no vendor lock-in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;Explore the platform&lt;/a&gt; · &lt;a href="https://lakeops.dev/docs" rel="noopener noreferrer"&gt;Read the docs&lt;/a&gt; · &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;Get a demo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>dataengineering</category>
      <category>programmers</category>
      <category>lakehouae</category>
    </item>
    <item>
      <title>Managed Data Lake: A Guide for 2026</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Tue, 21 Jul 2026 11:32:48 +0000</pubDate>
      <link>https://dev.to/jonisar/managed-data-lake-a-guide-for-2026-72l</link>
      <guid>https://dev.to/jonisar/managed-data-lake-a-guide-for-2026-72l</guid>
      <description>&lt;p&gt;&lt;em&gt;There are two ways to run a data lake. You can manage every table by hand — writing Airflow DAGs, scheduling Spark compaction jobs, building monitoring dashboards, and staffing a platform team that grows linearly with table count. Or you can deploy a control plane that continuously observes every table in the lake, classifies health, sequences maintenance operations, optimizes data layout for real query patterns, enforces policies at lake scale, and adapts as workloads change — while your data stays in your storage account and your engines stay yours. This guide covers both strategies and explains why teams that start with the first keep arriving at the second.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The data lake format question is settled. Apache Iceberg is the default table format for the modern lakehouse. Every major engine — Spark, Trino, Snowflake, Athena, DuckDB, Flink, Databricks — reads and writes Iceberg natively. Adoption is near-universal among data platform teams building on open storage.&lt;/p&gt;

&lt;p&gt;The operations question is not settled. Iceberg gives you ACID semantics, schema evolution, time travel, hidden partitioning, and partition evolution on your own object storage. What it does not give you is a system that keeps tables healthy. Compaction, snapshot expiration, orphan cleanup, manifest optimization, sort-order alignment, governance, and lake-wide observability are left to you — the &lt;a href="https://iceberg.apache.org/docs/latest/maintenance/" rel="noopener noreferrer"&gt;official maintenance guide&lt;/a&gt; is explicit about that. At a handful of tables, scripts work. At hundreds of tables across multiple engines, catalogs, and write patterns, the lake quietly degrades into a swamp — and the engineering hours spent holding it together scale faster than the team.&lt;/p&gt;

&lt;p&gt;"Managed data lake" is an overloaded term. Some products use it to mean a closed platform. Others mean one automated job. This guide defines what managed actually requires in 2026, then walks through two strategies to get there. First, the control-plane approach — using &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt;, a dedicated autonomous control plane for Apache Iceberg that handles observability, compaction, snapshot lifecycle, orphan cleanup, manifest optimization, governance policies, multi-engine query routing, and agentic AI readiness across your existing stack without moving data or replacing engines. Then, the manual approach using open-source tooling and custom scripts. Both paths are real. The difference is in what scales.&lt;/p&gt;

&lt;h2&gt;
  
  
  In this article
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What "managed" actually means in 2026&lt;/li&gt;
&lt;li&gt;Why data lakes degrade — the mechanics of silent failure&lt;/li&gt;
&lt;li&gt;Strategy 1: Smart automation with a control plane&lt;/li&gt;
&lt;li&gt;Strategy 2: Manual management with open-source tooling&lt;/li&gt;
&lt;li&gt;Fix the writers, not just the table&lt;/li&gt;
&lt;li&gt;Choosing your strategy&lt;/li&gt;
&lt;li&gt;The migration path: manual to control plane&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What "managed" actually means in 2026
&lt;/h2&gt;

&lt;p&gt;Iceberg on S3 is not a managed data lake. A nightly Spark &lt;code&gt;rewrite_data_files&lt;/code&gt; job is not either. "Managed" means the operational contract is closed continuously, safely, and at lake scale — without engineering time growing linearly with table count.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/irRsF9VYP20"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;An open data lake has four architectural layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt; — S3, GCS, ADLS, or on-premises object storage. Commodity infrastructure you own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table format + catalog&lt;/strong&gt; — Iceberg plus a catalog (Glue, Polaris, Nessie, Gravitino, Lakekeeper, S3 Tables, Unity Catalog). The structural contract every engine speaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute engines&lt;/strong&gt; — Spark, Trino, Snowflake, Athena, DuckDB, Flink, Databricks, ClickHouse. Chosen per workload, not locked to a single vendor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operations control plane&lt;/strong&gt; — the layer that observes table health, decides what maintenance to run, executes it safely, enforces policy, and learns from query patterns across engines. This is the layer that most lakes are missing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Closed platforms like Databricks and Snowflake bundle the last three layers. Open lakes decouple them on purpose — you pick the best engine for each workload, keep data in your own storage, and avoid vendor lock-in. The trade-off: you inherit the operations that the closed platform handled for you. That trade-off is the subject of this guide.&lt;/p&gt;

&lt;p&gt;In practice, a managed data lake means: tables stay compact and sorted for real query patterns. Snapshots and orphans do not inflate storage. Manifests stay lean enough that planning never dominates query time. Policies apply lake-wide and new tables inherit them automatically. Health is visible in one place. Correct results are the floor. Predictable cost and latency are the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why data lakes degrade — the mechanics of silent failure
&lt;/h2&gt;

&lt;p&gt;The failure mode is predictable, well-documented, and present in nearly every Iceberg lake that has been running for more than a few months without dedicated maintenance. The financial impact compounds from four directions at once: storage costs balloon from orphan files and stale snapshots, compute costs spike as engines scan thousands of small files instead of a few optimally-sized ones, query latency degrades as tables that returned results in seconds now take minutes, and engineering time evaporates as someone has to write, maintain, debug, and be on-call for the scripts that hold it all together. Understanding the mechanics is necessary for both strategies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small file accumulation.&lt;/strong&gt; Streaming writers (Flink, Spark Structured Streaming, Kafka Connect Iceberg sink, CDC connectors) produce files sized by checkpoint throughput, not by optimal read size. A streaming pipeline at 1 commit per minute across 10 partitions creates approximately 14,400 files per day. Each query pays an object-store GET request, a Parquet footer parse, and task setup overhead per file. Cost and latency scale with file count, not with data volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot accumulation.&lt;/strong&gt; Every Iceberg commit creates a new snapshot. A streaming table at 5-minute commits creates ~288 snapshots per day. Without expiration, snapshot counts reach tens of thousands within weeks. Query planning reads the snapshot chain; above ~1,000–2,000 retained snapshots, planning time degrades measurably — from sub-second to 30+ seconds. The mechanism is detailed in &lt;a href="https://lakeops.dev/blog/apache-iceberg-query-planning" rel="noopener noreferrer"&gt;how Iceberg query planning works&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manifest fragmentation.&lt;/strong&gt; Every commit adds at least one manifest file to the manifest list. Streaming tables accumulate thousands of manifests per month. Each manifest must be read during planning; at 500+ manifests, planner latency alone can exceed multiple seconds. This is the layer most teams diagnose last because the symptom (slow EXPLAIN, slow first row) does not point at data files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orphan file growth.&lt;/strong&gt; Failed writes, aborted Spark jobs, interrupted compaction, and schema-evolution retries leave data files on object storage that no snapshot references. These orphans are invisible to Iceberg but billable by the cloud provider. Production orphan sweeps routinely reclaim a significant share of billable storage on affected prefixes. A detailed breakdown of these nine degradation vectors and how to address each is in the &lt;a href="https://lakeops.dev/blog/managed-iceberg-2026" rel="noopener noreferrer"&gt;Managed Iceberg in 2026&lt;/a&gt; guide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete file amplification.&lt;/strong&gt; Merge-on-read tables accumulate position and equality delete files with every UPDATE or DELETE. Without compaction that physically applies deletes, every read must reconcile live rows against all accumulated delete files. Above a certain delete-to-data file ratio, reads pay measurable reconciliation tax and users feel it on every query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layout drift.&lt;/strong&gt; Binpack compaction normalizes file sizes but does not sort data. Engines cannot skip irrelevant file groups because Parquet min/max statistics span wide ranges. A table compacted by size but not sorted by its most-filtered columns still forces full scans on common predicates. And the sort order that accelerated one workload six months ago may not match today's query patterns — layout needs to adapt as usage evolves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write amplification in metadata.&lt;/strong&gt; Every Iceberg write — even a tiny append — produces a new &lt;code&gt;metadata.json&lt;/code&gt;, a new manifest list, and one or more new manifest files. For a batch job running hourly, this overhead is invisible. For a streaming job committing every few seconds, metadata writing dominates I/O. Object storage starts throttling. Query planning suffers badly because every new metadata version adds to the chain the planner must traverse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Governance entropy.&lt;/strong&gt; Some tables have compaction configured; others do not. Some have snapshot retention; others use defaults that never expire. Retention windows are too aggressive for long-running queries, or too conservative for GDPR. No single view shows the health of the entire lake across catalogs and engines. Every new table starts from scratch unless someone remembers to configure it.&lt;/p&gt;

&lt;p&gt;These failure modes do not trigger alarms. They compound silently over weeks and months — and the first real symptom is usually an analyst asking why their dashboard takes 40 seconds instead of 3. By the time someone investigates, the debt has accumulated across dozens of tables and unwinding it is a project, not a task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 1: Smart automation with a control plane
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78bsrw60gd8tr34clx5j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78bsrw60gd8tr34clx5j.png" alt=" " width="799" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A control plane is an architectural layer that sits between your storage, catalogs, and engines — observing the state of every table, understanding cross-engine query patterns, and applying the right maintenance at the right time. It does not replace anything in your stack. It adds the operational intelligence that open-source components do not provide on their own. Netflix built this layer in-house over years — Autotune, janitors, Metacat, Polaris. That story is in &lt;a href="https://lakeops.dev/blog/intelligent-lakehouse-like-netflix" rel="noopener noreferrer"&gt;Intelligent Lakehouse: Build Like Netflix&lt;/a&gt;. Most teams cannot staff that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8xlnjke6g8d3ox64o6n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8xlnjke6g8d3ox64o6n.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; is a dedicated control plane for Apache Iceberg — the only product that exists purely as this ops layer. Not an engine, not storage, not a catalog. It connects to your existing catalogs (AWS Glue, Polaris, Nessie, Gravitino, Lakekeeper, S3 Tables) and query engines (Trino, Spark, Flink, Snowflake, Athena, DuckDB) without moving data or changing pipelines. Setup takes roughly ten minutes. Your data stays in your storage account. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flo9aielfypw9afcjl8q4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flo9aielfypw9afcjl8q4.png" alt=" " width="799" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What you get is a closed-loop system covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lake-wide observability&lt;/strong&gt; — continuous health scoring (Critical / Warning / Healthy) across every table and catalog, proactive insights ranked by severity, and cross-engine telemetry in one dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fekv26341i3o4tqhrk628.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fekv26341i3o4tqhrk628.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3mjxkstlhq899sugln9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3mjxkstlhq899sugln9.png" alt=" " width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fegq89r0rn1yezqg31930.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fegq89r0rn1yezqg31930.png" alt=" " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query-aware compaction&lt;/strong&gt; — a purpose-built Rust/DataFusion engine that sorts data by the columns your queries actually filter on, orders of magnitude faster and cheaper than Spark-based compaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot lifecycle management&lt;/strong&gt; — automated, concurrency-safe expiration with configurable retention policies that respect active readers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fynntn07r9j83i3n41802.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fynntn07r9j83i3n41802.png" alt=" " width="800" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orphan file cleanup&lt;/strong&gt; — safe detection and removal with age-threshold protection, sequenced after expiration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fps5oedgbdh5i2ynegttn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fps5oedgbdh5i2ynegttn.png" alt=" " width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manifest and metadata optimization&lt;/strong&gt; — consolidation, position delete rewrites, and Puffin statistics computation, triggered automatically after compaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbxzqmytpi3d0wswaog3p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbxzqmytpi3d0wswaog3p.png" alt=" " width="800" height="675"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Declarative policies and governance&lt;/strong&gt; — compaction, retention, cleanup, and configuration rules scoped at organization, catalog, namespace, or table level with cascade inheritance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fir2fvcg0flanrb55zz38.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fir2fvcg0flanrb55zz38.png" alt=" " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-engine query routing&lt;/strong&gt; — route queries across engines optimized for cost, latency, or throughput via &lt;a href="https://lakeops.dev/blog/routing-multiple-query-engines-with-iceberg" rel="noopener noreferrer"&gt;QueryFlux&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frbmsziuwie06vq2otfql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frbmsziuwie06vq2otfql.png" alt=" " width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agentic AI readiness&lt;/strong&gt; — native MCP interface with schema discovery, layered guardrails (ReadOnly, CostEstimate, PIIMask, HumanApproval), and agent query telemetry feeding back into optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwy8ge92fcwbwj4drymry.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwy8ge92fcwbwj4drymry.png" alt=" " width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layout simulations&lt;/strong&gt; — preview compaction impact on Iceberg branches before committing to production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9evcjpf563rzuuurj002.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9evcjpf563rzuuurj002.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The operating model repeats continuously in four phases: connect and collect telemetry, classify table health and raise insights, execute maintenance in the correct sequence (expire snapshots → remove orphans → compact data files → rewrite manifests), and observe outcomes while enforcing governance. Each cycle the system learns — sort orders adapt as query patterns evolve, the compaction planner improves throughput on repeated runs, and policies self-enforce as new tables are created.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability and health classification
&lt;/h3&gt;

&lt;p&gt;After connecting a catalog, every table is continuously scored and classified. The dashboard shows aggregate health across catalogs, recent operations, storage trends, and a summary of proactive insights. Platform teams can triage the entire lake in one screen without writing a single query.&lt;/p&gt;

&lt;p&gt;Health is computed from the same Iceberg signals that matter in manual debugging: file count and size distribution per partition, manifest count and depth, snapshot accumulation, delete-file ratio, partition skew, and sort-order alignment with real query patterns. The difference is that the control plane evaluates these signals continuously across every table — not one table at a time when someone asks "why is this slow?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proactive insights&lt;/strong&gt; go beyond classification to tell you &lt;em&gt;why&lt;/em&gt; a table is degraded and &lt;em&gt;what to do about it&lt;/em&gt;. Each insight is tied to a specific table, a severity level (CRITICAL, HIGH, WARNING, LOW), and a recommended action. In a manual workflow, you discover these conditions &lt;em&gt;after&lt;/em&gt; a user files a ticket. With Insights, you discover them while they are still at Warning — before they escalate to a production incident. Details on the &lt;a href="https://lakeops.dev/solutions/observability" rel="noopener noreferrer"&gt;observability surface&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intelligent compaction
&lt;/h3&gt;

&lt;p&gt;Compaction is the highest-impact maintenance operation. LakeOps approaches it differently from generic Spark-based compaction in four ways.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzz21n4pqeeuc488iu2if.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzz21n4pqeeuc488iu2if.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query-aware sorting.&lt;/strong&gt; LakeOps tracks which columns appear in WHERE, JOIN, and GROUP BY clauses across every connected engine for every table. During compaction, data is physically sorted by those columns — so Parquet row-group statistics enable engines to skip irrelevant data without reading it. When query patterns change, the sort order adapts on subsequent compaction passes without manual configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rust/DataFusion engine.&lt;/strong&gt; Compaction runs on a purpose-built engine written in Rust with Apache DataFusion. Arrow columnar buffers, bounded memory, zero garbage collection, lock-free parallelism, native Parquet I/O with predicate pushdown. Orders of magnitude faster and cheaper than Spark-based compaction on identical datasets. Partitions that OOM Spark complete in minutes because memory is bounded, not heap-dependent. Full benchmark details are in the &lt;a href="https://lakeops.dev/solutions/compaction" rel="noopener noreferrer"&gt;compaction product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This cost reduction is what makes frequent compaction economically viable. Instead of one nightly job that lets streaming tables degrade for 24 hours between passes, LakeOps runs compaction multiple times per day when file health signals warrant it. Tables stay continuously healthy instead of oscillating between degradation and cleanup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflict-aware execution.&lt;/strong&gt; The system inspects active writer state and targets only cold partitions — partitions without active streaming appends. If a boundary-case conflict occurs, only the affected file group retries on the next cycle. No data loss, no full-job restarts, no &lt;code&gt;CommitFailedException&lt;/code&gt; storms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-improving planner.&lt;/strong&gt; The compaction planner learns from workload telemetry across consecutive runs. Same table, zero config changes — throughput and runtime improve with each pass as the planner adapts its internal parallelism, file-group sizing, and sort strategy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxojrgk5smt6hxjk1o9v1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxojrgk5smt6hxjk1o9v1.png" alt=" " width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Snapshot, orphan, and manifest lifecycle
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8y4ilcwtriu23duhxlf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8y4ilcwtriu23duhxlf.png" alt=" " width="799" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LakeOps automates the full maintenance lifecycle with correct sequencing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot expiration.&lt;/strong&gt; Policy-based retention (time and count) enforced continuously. Concurrency-safe — expiration respects active readers and in-flight queries, preventing the most common production failure in teams that automate snapshot expiration without accounting for concurrent readers. Version history, snapshot comparison, and one-click rollback remain available within the retention window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orphan cleanup.&lt;/strong&gt; Age-threshold safety (7+ days by default) protects in-flight writes. Sequenced after expiration so newly dereferenced files are captured in the same sweep. Full audit trail of every removed file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manifest and metadata optimization.&lt;/strong&gt; Three operations triggered automatically after compaction: manifest consolidation (fewer manifests = faster planning), position delete file rewrite (clean reads for merge-on-read tables), and Puffin statistics computation (column-level NDV, min/max, null counts for aggressive file-level pruning). Manifests are rewritten after compaction, not before, so the index reflects the final file set.&lt;/p&gt;

&lt;p&gt;Every maintenance operation is logged lake-wide and per table with duration, impact, and status — the audit trail that answers "what ran, when, and did it succeed?" during incident response, without parsing executor logs across multiple systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Policies, routing, and AI readiness
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Declarative policies&lt;/strong&gt; scope compaction targets, snapshot retention, orphan cleanup, and manifest optimization at four levels with cascade inheritance: organization → catalog → namespace → table. Adaptive Maintenance bundles the full lifecycle into one data-driven policy — the system decides which operations each table needs from structural signals and runs them in the correct sequence. Cross-catalog enforcement works uniformly across AWS Glue, Polaris, Nessie, and Gravitino.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-engine routing&lt;/strong&gt; via &lt;a href="https://lakeops.dev/blog/routing-multiple-query-engines-with-iceberg" rel="noopener noreferrer"&gt;QueryFlux&lt;/a&gt; connects all engines to a unified routing layer that makes per-query decisions based on cost, latency, table health, and historical patterns. Without routing, teams either standardize on one engine (losing optimization) or let each user choose (creating cost chaos). Routing decisions are enriched by table health — if a table is currently degraded, queries can be directed to engines that handle fragmentation better until compaction resolves the issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agentic AI readiness&lt;/strong&gt; provides a native MCP (Model Context Protocol) interface for AI agents with schema discovery, wire compatibility (PostgreSQL, MySQL, Arrow Flight), and layered guardrails configurable per agent session. Agent query telemetry feeds back into compaction and sort-order decisions — when agents start filtering on columns that no human dashboard uses, the compaction planner detects the new pattern and adjusts accordingly. Details in the &lt;a href="https://lakeops.dev/blog/iceberg-ai-agents-guide" rel="noopener noreferrer"&gt;AI agents guide&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The compound effect
&lt;/h3&gt;

&lt;p&gt;The capabilities above are not independent features. They form a closed loop where each component's output feeds the next: observability detects degradation, the system runs expiration to unpin files, orphan cleanup captures newly dereferenced objects, compaction runs sorted by the columns that real queries filter on, manifests are rewritten against the final file set, Puffin statistics are recomputed, the table moves from Warning to Healthy, and the event is logged with before/after metrics. Next cycle, the compaction planner runs faster because it learned from the previous pass.&lt;/p&gt;

&lt;p&gt;The engineering hours that used to go into writing, scheduling, monitoring, and debugging maintenance scripts are returned to product work. Policies are defined once and the system executes them continuously — every action logged, auditable, and reversible. Data stays in your storage account. No pipeline rewrites. Minutes to install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 2: Manual management with open-source tooling
&lt;/h2&gt;

&lt;p&gt;Not every team needs a control plane on day one. If you have a handful of Iceberg tables, one or two engines, batch-only writes, and a platform engineer who owns maintenance, manual management works. This section is the complete playbook — the same methodology that works whether you have a control plane or not.&lt;/p&gt;

&lt;h3&gt;
  
  
  The manual stack
&lt;/h3&gt;

&lt;p&gt;The typical setup: an orchestrator (Airflow, Dagster, or Prefect) scheduling maintenance as DAG tasks, Spark (on EMR, Dataproc, or Kubernetes) running Iceberg maintenance procedures, a catalog (Glue, Polaris, Nessie), custom SQL queries against metadata tables piped into Grafana or Datadog, and threshold-based alerting. This stack is well-understood and runs at many organizations today. The constraint is not capability but coverage: every new table, every new write pattern, and every new engine requires incremental engineering work to bring under management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compaction by hand
&lt;/h3&gt;

&lt;p&gt;Use Spark's &lt;code&gt;rewrite_data_files&lt;/code&gt; procedure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'binpack'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'event_date &amp;gt;= current_date() - INTERVAL 7 DAYS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'268435456'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'min-input-files'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'3'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'partial-progress.enabled'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'partial-progress.max-commits'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'20'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'max-concurrent-file-group-rewrites'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'15'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key decisions: &lt;code&gt;binpack&lt;/code&gt; for file-size normalization, &lt;code&gt;sort&lt;/code&gt; for query-pattern optimization, &lt;code&gt;zorder&lt;/code&gt; for multi-column predicates. Target 256–512 MB files. Always enable &lt;code&gt;partial-progress&lt;/code&gt;. Never compact the active write partition on a streaming table — compacting the hot partition while Flink is appending is the primary cause of &lt;code&gt;CommitFailedException&lt;/code&gt; storms. Run compaction on a separate cluster. Schedule cadence must match write cadence — if you write 14,400 files per day and compact once at midnight, the table spends 23 hours degraded. Decision frameworks are in the &lt;a href="https://lakeops.dev/blog/iceberg-compaction-strategies" rel="noopener noreferrer"&gt;compaction strategies guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For sort compaction, you must determine sort columns yourself by analyzing query patterns across engines. When patterns change, you must notice and update the sort order manually. This is the gap a control plane closes automatically with cross-engine telemetry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sort'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sort_order&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'event_date ASC, region ASC, user_id ASC'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'event_date &amp;gt;= current_date() - INTERVAL 7 DAYS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'268435456'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'partial-progress.enabled'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'partial-progress.max-commits'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'20'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'max-file-group-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'10737418240'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Snapshot expiration, orphan cleanup, and manifests by hand
&lt;/h3&gt;

&lt;p&gt;Expire snapshots regularly. Retention window must exceed your longest-running query — a Spark job that runs for 3 hours will fail if its snapshot is expired mid-flight. This is one of the most common production failures in teams that automate aggressively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expire_snapshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;older_than&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-14 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;retain_last&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'write.metadata.delete-after-commit.enabled'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'write.metadata.previous-versions-max'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'100'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Orphan cleanup runs after expiration, never before. Always dry-run first. The &lt;code&gt;older_than&lt;/code&gt; threshold must be far enough in the past to protect in-flight writes — production teams use 7+ days. Verify URI schemes match (&lt;code&gt;s3://&lt;/code&gt; vs &lt;code&gt;s3a://&lt;/code&gt; vs &lt;code&gt;s3n://&lt;/code&gt;) before your first run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove_orphan_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;older_than&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-07 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dry_run&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rewrite manifests after every compaction cycle. This is the step most teams skip. Without rewriting, planning time remains elevated even though the data files are healthy. On extreme fragmentation, rewriting alone can drop planning from 30+ seconds to under a second. Full methodology in the &lt;a href="https://lakeops.dev/blog/iceberg-orphan-files-cleanup" rel="noopener noreferrer"&gt;orphan cleanup guide&lt;/a&gt; and the &lt;a href="https://lakeops.dev/blog/iceberg-table-health-maintenance" rel="noopener noreferrer"&gt;table health guide&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_manifests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Health monitoring and the safe sequence
&lt;/h3&gt;

&lt;p&gt;Manual monitoring means querying Iceberg metadata tables and setting alert thresholds. Start with four numbers per table: file count and average size per partition, snapshot count, manifest count, and delete-file ratio for MOR/CDC tables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_size_in_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_size_mb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;partition&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;snapshot_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshots&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;manifest_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correct maintenance order — consistent across the &lt;a href="https://iceberg.apache.org/docs/latest/maintenance/" rel="noopener noreferrer"&gt;official Iceberg docs&lt;/a&gt; and the &lt;a href="https://lakeops.dev/blog/automating-iceberg-table-maintenance" rel="noopener noreferrer"&gt;automating table maintenance guide&lt;/a&gt; — is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expire snapshots&lt;/strong&gt; — release metadata references to files nobody needs for time travel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove orphan files&lt;/strong&gt; — delete unreferenced objects from storage (with 7+ day safety window)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compact data files&lt;/strong&gt; — merge small files and physically apply deletes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite manifests&lt;/strong&gt; — consolidate the metadata index against the final file set&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute statistics&lt;/strong&gt; (optional) — refresh Puffin stats so planners see current NDVs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compacting before expiration rewrites files about to be discarded — wasted compute. Orphan cleanup before expiration misses the largest reclaimable set. Manifest rewrite before compaction produces an index stale minutes later. Never invert the order under pressure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where manual management breaks down
&lt;/h3&gt;

&lt;p&gt;Manual management does not fail because the tools are inadequate. Spark procedures work. Airflow can schedule them. Grafana can chart health. The breakdown happens at the system level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coverage gap.&lt;/strong&gt; Every new table must be added to the DAG, configured, monitored, and validated. New tables slip through. Policies do not inherit automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workload blindness.&lt;/strong&gt; Manual compaction sorts by columns you choose at configuration time. When a new dashboard goes live, when a new team starts querying with different filters, when AI agents start accessing columns nobody filtered before — the sort order becomes stale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequence violations under pressure.&lt;/strong&gt; At 3 AM during an incident, the instinct is to compact first. Compacting before expiration wastes compute. Running orphan cleanup with a too-short safety window corrupts data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrency danger.&lt;/strong&gt; Snapshot expiration that does not respect active readers kills long-running queries. Compaction that targets active write partitions causes commit conflicts. Manual scripts do not know what other processes are doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost of execution.&lt;/strong&gt; Spark compaction means spinning up JVM clusters, allocating executors, and paying for compute that is orders of magnitude more expensive than a purpose-built engine. Most teams run on over-provisioned clusters because under-provisioning causes failures and 2 AM pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linear engineering scaling.&lt;/strong&gt; Maintenance time grows linearly with table count. The team size does not. The &lt;a href="https://lakeops.dev/blog/from-data-swamp-to-modern-iceberg-lakehouse" rel="noopener noreferrer"&gt;data swamp to modern lakehouse&lt;/a&gt; guide walks through the transition in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix the writers, not just the table
&lt;/h2&gt;

&lt;p&gt;This section applies to both strategies. Many data lake incidents are writer-configuration debt — the pipeline creates structural damage faster than maintenance can repair it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint interval.&lt;/strong&gt; The single most impactful setting for streaming Iceberg tables. Flink at 30-second checkpoints with 16 write tasks produces ~46,000 files per day per table. Move to 3–5 minute checkpoints and file creation drops dramatically. For staging tables, feature stores, and compliance archives, 10–15 minute intervals are often appropriate. Details in the &lt;a href="https://lakeops.dev/blog/flink-iceberg-optimization" rel="noopener noreferrer"&gt;Flink Iceberg optimization guide&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="s1"&gt;'execution.checkpointing.interval'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5min'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="s1"&gt;'execution.checkpointing.mode'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'EXACTLY_ONCE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="s1"&gt;'execution.checkpointing.timeout'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'10min'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Write distribution mode.&lt;/strong&gt; Set &lt;code&gt;write.distribution-mode=hash&lt;/code&gt; for partitioned streaming tables. Hash distribution shuffles records by partition key before writing, cutting file count by the parallelism factor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'write.distribution-mode'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'hash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'write.target-file-size-bytes'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'268435456'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sink parallelism.&lt;/strong&gt; Decouple source parallelism from sink parallelism. A Kafka topic with 64 partitions does not need 64 writer subtasks. Setting source at 64 and sink at 8–16 with hash distribution yields far fewer files while maintaining consumption throughput.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partition grain.&lt;/strong&gt; Over-partitioning is the leading cause of small files at scale. Prefer &lt;code&gt;days(ts)&lt;/code&gt; over &lt;code&gt;hours(ts)&lt;/code&gt;. Use &lt;code&gt;bucket(N, col)&lt;/code&gt; instead of identity transforms on high-cardinality columns. Decision frameworks in the &lt;a href="https://lakeops.dev/blog/iceberg-partitioning-best-practices" rel="noopener noreferrer"&gt;partitioning best practices guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete mode vs mutation rate.&lt;/strong&gt; Merge-on-read (MOR) is right for high-churn CDC where write latency matters more than read performance. Copy-on-write (COW) is right when read latency is sacred. Mixing MOR with no compaction policy is how delete-ratio incidents start.&lt;/p&gt;

&lt;p&gt;Fixing writers halves the maintenance burden for both strategies. Skipping this step turns your control plane into a very expensive mop — and makes your manual scripts a full-time job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing your strategy
&lt;/h2&gt;

&lt;p&gt;Both strategies work. The question is scale, cost, and where engineering time delivers the most value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy 2 (manual) fits when:&lt;/strong&gt; you have fewer than ~50 tables, one or two engines, batch-only or low-frequency writes, a platform engineer who owns maintenance, and predictable query patterns that do not change frequently. The metadata queries and safe sequence in this guide are your on-call playbook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy 1 (control plane) fits when:&lt;/strong&gt; table count crosses ~50, you run streaming or CDC pipelines, multiple engines hit the same tables, engineering time spent on maintenance scripts exceeds the cost of a purpose-built system, GDPR or compliance requirements need policy-based enforcement, or query patterns change and layout optimization must adapt without manual intervention.&lt;/p&gt;

&lt;p&gt;The strategies compose. Many teams start with manual management, adopt a control plane for the highest-traffic catalogs first, and expand as value is proven.&lt;/p&gt;

&lt;p&gt;The decision framework reduces to a few questions: Does the system see all my catalogs in one place? Is compaction query-aware across engines? Are operations coordinated in the correct sequence? Do policies inherit to new tables automatically? Does maintenance compete with my query clusters for compute? Can I simulate layouts before committing? Are agent guardrails first-class? Do I have to move data?&lt;/p&gt;

&lt;p&gt;If most answers are "no" on an open multi-engine Iceberg stack, you do not have a managed data lake yet — you have Iceberg plus hope.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration path: manual to control plane
&lt;/h2&gt;

&lt;p&gt;The transition from manual management to a control plane is incremental, not disruptive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connect a catalog.&lt;/strong&gt; Discovery and health classification begin immediately without touching existing maintenance infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect and learn.&lt;/strong&gt; Use the observability dashboard to understand the current state of your lake. Teams often discover tables they did not know were degraded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run maintenance on the worst tables.&lt;/strong&gt; Trigger compaction, expiration, or orphan cleanup on the most Critical tables. Watch the before/after metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable policies per namespace.&lt;/strong&gt; Define compaction targets, retention windows, and cleanup thresholds. Every table in scope inherits automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable Adaptive Maintenance for streaming tables.&lt;/strong&gt; Let the system decide when to compact based on file health signals rather than cron schedules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retire manual scripts.&lt;/strong&gt; As each namespace is covered by policies, disable the corresponding Airflow tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expand to remaining catalogs.&lt;/strong&gt; Each step is independently valuable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Teams running the control-plane model report far fewer Iceberg incidents — not because the runbook got faster, but because file explosions, manifest storms, and orphan cliffs are resolved while still at Warning. The real end state of a managed data lake is not faster debugging. It is rarely needing to debug at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;A managed data lake in 2026 is not a storage product, not a single job, and not a platform you must adopt wholesale. It is an operational contract: tables stay healthy, costs stay predictable, queries stay fast, policies apply consistently, and AI agents get the data they need with appropriate guardrails. The question is who upholds that contract — your engineering team writing scripts, or a dedicated system that does it continuously.&lt;/p&gt;

&lt;p&gt;Strategy 2 works. It is well-understood, uses open-source tooling, and scales to a moderate number of tables with dedicated platform engineering. The manual playbook in this guide — safe sequence, metadata queries, writer configuration, per-table monitoring — is the same playbook that production teams use every day.&lt;/p&gt;

&lt;p&gt;Strategy 1 works better at scale. &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; closes the operational loop that manual scripts cannot: continuous health classification across every table and catalog, query-aware compaction on a purpose-built Rust engine, coordinated maintenance that respects the safe sequence, policies that inherit to every new table, cross-engine telemetry that adapts layout to real query patterns, multi-engine routing that sends each query to the right engine, and agentic AI readiness with layered guardrails. It runs on your existing stack without moving data or replacing engines.&lt;/p&gt;

&lt;p&gt;The format already gave you ACID and time travel. The operations layer is what determines whether you have a data lake that works — or a data lake that worked once and has been quietly degrading since. Choose the strategy that matches your current scale. Build toward the one that matches where you are headed.&lt;/p&gt;

&lt;p&gt;For deeper technical breakdowns: &lt;a href="https://lakeops.dev/blog/managed-iceberg-2026" rel="noopener noreferrer"&gt;Managed Iceberg in 2026&lt;/a&gt; covers the nine control-plane components. &lt;a href="https://lakeops.dev/blog/intelligent-lakehouse-like-netflix" rel="noopener noreferrer"&gt;Intelligent Lakehouse: Build Like Netflix&lt;/a&gt; shows how Netflix solved these problems internally. &lt;a href="https://lakeops.dev/blog/routing-multiple-query-engines-with-iceberg" rel="noopener noreferrer"&gt;Routing Multiple Query Engines with Iceberg&lt;/a&gt; covers the multi-engine routing architecture. &lt;a href="https://lakeops.dev/blog/automating-iceberg-table-maintenance" rel="noopener noreferrer"&gt;Automating Table Maintenance&lt;/a&gt; walks through the progression from scripts to autonomous operations. &lt;a href="https://lakeops.dev/blog/iceberg-table-health-maintenance" rel="noopener noreferrer"&gt;Table Health and Maintenance&lt;/a&gt; covers every maintenance operation with configuration detail. And &lt;a href="https://lakeops.dev/blog/from-data-swamp-to-modern-iceberg-lakehouse" rel="noopener noreferrer"&gt;From Data Swamp to Modern Lakehouse&lt;/a&gt; covers the full architectural transition. For the platform that closes the loop, explore &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;lakeops.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>data</category>
      <category>dataengineering</category>
      <category>softwareengineering</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Netflix Intelligent Lakehouse Solves Iceberg Maintenance — You Can Easily Too</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Fri, 29 May 2026 13:40:00 +0000</pubDate>
      <link>https://dev.to/jonisar/netflix-intelligent-lakehouse-solves-iceberg-maintenance-you-can-easily-too-5a84</link>
      <guid>https://dev.to/jonisar/netflix-intelligent-lakehouse-solves-iceberg-maintenance-you-can-easily-too-5a84</guid>
      <description>&lt;p&gt;Every production Iceberg data lake eventually hits the same wall: tables that looked fast at 10 GB start crawling at 10 TB. Small files pile up from streaming ingestion, snapshots accumulate because nobody set expiration, orphaned data lingers from failed Spark jobs, and manifest lists grow until planning a simple SELECT takes longer than running it.&lt;/p&gt;

&lt;p&gt;Netflix hit this wall years ago — and their solution shaped how the industry thinks about lakehouse architecture. At AWS re:Invent, their engineers walked through the ecosystem they assembled around Iceberg: &lt;a href="https://github.com/apache/polaris" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt; for catalog management, Autotune for automated compaction, janitors for continuous cleanup, and Metacat for observability. The outcome was a 25% cost reduction and tables that stayed healthy without manual intervention.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm14ehtdabtuz4arqieve.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm14ehtdabtuz4arqieve.png" alt="Netflix intelligent lakehouse architecture overview" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But Netflix had something most teams don't: a dedicated platform organization building custom distributed services backed by CockroachDB, Kafka, and fleets of Spark clusters.&lt;/p&gt;

&lt;p&gt;Today, a &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;lakehouse control plane&lt;/a&gt; — just as good as Netflix's or better — is available for everyone to install on their Iceberg lakehouse.&lt;/p&gt;

&lt;p&gt;The industry's favorite solution is &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This article breaks down what actually makes a lakehouse "intelligent" — component by component — and shows how each piece maps to tooling that exists today.&lt;/p&gt;

&lt;p&gt;Today with LakeOps, every team in the world is 10 minutes away from an intelligent lakehouse. And yes, it includes autonomous snapshot optimization as well as orphan files, metadata, manifests, and more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0obyti0412fxx06h8f83.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0obyti0412fxx06h8f83.png" alt="LakeOps intelligent lakehouse control plane" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30es0w48vvr0nkltxkph.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30es0w48vvr0nkltxkph.png" alt="LakeOps catalog and engine connectivity" width="800" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdhw7epi24y6qk9d0kv6q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdhw7epi24y6qk9d0kv6q.png" alt="LakeOps optimization workflow" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The maintenance gap nobody talks about
&lt;/h2&gt;

&lt;p&gt;Apache Iceberg solved the table format problem. Schema evolution, hidden partitioning, time travel, snapshot isolation — these features are why every major engine from Snowflake to DuckDB now speaks Iceberg natively.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F70kgl9c7jflqi998dbtq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F70kgl9c7jflqi998dbtq.png" alt="Iceberg table format and maintenance gap" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What Iceberg intentionally left unsolved is &lt;em&gt;who runs the maintenance&lt;/em&gt;. The format gives you powerful primitives. Keeping those primitives performing well at scale is your responsibility.&lt;/p&gt;

&lt;p&gt;In practice, this creates a silent degradation cycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Streaming writes produce small files&lt;/strong&gt; — a pipeline appending every 5 minutes to 100 partitions creates 100 new files per commit. After a week, some partitions contain thousands of sub-megabyte Parquet files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshots grow unbounded&lt;/strong&gt; — without explicit expiration, every commit adds a snapshot. A table with hourly writes accumulates 8,760 snapshots per year, each referencing its own manifest list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphan files accumulate&lt;/strong&gt; — aborted Spark jobs, failed compaction runs, and expired snapshots leave behind data files that no snapshot references. These files cost storage but serve nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifests fragment&lt;/strong&gt; — as files are added and removed, the manifest layer becomes a web of small manifest files. Query planning reads every one of them before scanning a single data file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The financial impact compounds from four directions: storage waste (orphans + snapshots), compute waste (scanning small files), metadata overhead (fragmented manifests), and engineering time (maintaining cron scripts that break silently).&lt;/p&gt;

&lt;p&gt;Netflix's insight was that solving these problems one at a time, with isolated scripts, doesn't scale. You need an integrated system — a control plane that sees the full picture and acts on it continuously.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six components of an intelligent lakehouse
&lt;/h2&gt;

&lt;p&gt;Looking across Netflix's published architecture and &lt;a href="https://blog.dataengineerthings.org/i-spent-4-hours-learning-how-netflix-operates-apache-iceberg-at-scale-c93a9f94c539" rel="noopener noreferrer"&gt;detailed breakdowns of their Iceberg ecosystem&lt;/a&gt;, six capabilities separate an intelligent lakehouse from a collection of Iceberg tables with maintenance scripts taped to the side:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Universal catalog connectivity
&lt;/h3&gt;

&lt;p&gt;Netflix built Polaris to replace the Hive Metastore with a catalog purpose-built for Iceberg — scalable, CockroachDB-backed, and supporting the &lt;a href="https://iceberg.apache.org/concepts/catalog/" rel="noopener noreferrer"&gt;Iceberg REST catalog specification&lt;/a&gt; for multi-engine access.&lt;/p&gt;

&lt;p&gt;Most teams aren't replacing their catalog. They're running AWS Glue, or they adopted a REST catalog like Nessie or Lakekeeper early on, or they have tables spread across multiple catalogs in different regions.&lt;/p&gt;

&lt;p&gt;An intelligent lakehouse connects to &lt;em&gt;existing&lt;/em&gt; catalogs — Glue, DynamoDB, REST (Polaris, Nessie, Lakekeeper, Gravitino), S3 Tables, or custom implementations — discovers every namespace and table, and normalizes metadata into a single operational view. No catalog migration required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; does exactly this: point it at your catalog credentials, and within minutes it inventories every table and starts collecting metadata signals.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Intelligent and efficient compaction that actually works
&lt;/h3&gt;

&lt;p&gt;Netflix's Autotune watches for table write events through SQS and spins up Spark jobs to compact small files in the background. It's the core of their self-maintaining architecture.&lt;/p&gt;

&lt;p&gt;The Spark-based approach works but carries significant overhead. You need provisioned compute clusters, JVM tuning, IAM roles for each cluster, and someone on-call for job failures. Spark compaction typically costs around $50 per TB processed.&lt;/p&gt;

&lt;p&gt;A Rust-based alternative changes the economics entirely. LakeOps runs compaction with a native engine built on &lt;a href="https://datafusion.apache.org/" rel="noopener noreferrer"&gt;Apache DataFusion&lt;/a&gt; — no JVM, no cluster provisioning, no shuffle stages.&lt;/p&gt;

&lt;p&gt;It reads Iceberg metadata, plans optimal merges, and writes compacted Parquet directly to your storage. Production benchmarks show roughly &lt;strong&gt;$5/TB&lt;/strong&gt; — a 10x cost reduction over Spark.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9uo2mbygm01vfcwjk231.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9uo2mbygm01vfcwjk231.png" alt="Rust-powered Iceberg compaction engine" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On top, &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; runs compaction based on actual query patterns — so the way your files are organized is optimized to minimize I/O, cutting CPU costs as well as storage by up to 80% compared to Spark or S3 Tables.&lt;/p&gt;

&lt;p&gt;It also coordinates all operations and events with Adaptive Maintenance to maximize results and cut time and costs. The sequence matters, and event- or trigger-driven ops are much smarter and more efficient than cron-based jobs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmu522ktbo3hx6h1hxsmd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmu522ktbo3hx6h1hxsmd.png" alt="Query-aware compaction and optimization" width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1yb91nbrzqlucxmrasi5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1yb91nbrzqlucxmrasi5.png" alt="Adaptive maintenance coordination" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two strategies cover every workload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binpack&lt;/strong&gt; — combines small files targeting optimal file sizes (~512 MB). Handles most tables well with minimal configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort&lt;/strong&gt; — reorders data by query-relevant columns so engines skip irrelevant row groups through predicate pushdown. Dramatic speedups for tables with clear access patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each table can be run manually first (configure → Execute → review results) and then switched to automated scheduling with a cron expression. No all-or-nothing commitment.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Metadata lifecycle automation
&lt;/h3&gt;

&lt;p&gt;Netflix runs dedicated "janitor" services for orphan cleanup and snapshot expiration. Without them, their exabyte-scale lake would drown in stale metadata and unreferenced files.&lt;/p&gt;

&lt;p&gt;The same operations — snapshot retention, orphan removal, manifest consolidation — need to run continuously on any production Iceberg lake. LakeOps provides all four as per-table operations with independent configuration:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snapshot retention&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Expires snapshots beyond a retention period, respecting min counts&lt;/td&gt;
&lt;td&gt;Reclaims metadata, enables cleanup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Orphan file cleanup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Removes files unreferenced by any snapshot (with age threshold)&lt;/td&gt;
&lt;td&gt;Recovers wasted storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Manifest optimization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consolidates fragmented manifests&lt;/td&gt;
&lt;td&gt;Speeds up query planning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File compaction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Merges small files (Binpack or Sort)&lt;/td&gt;
&lt;td&gt;Reduces scan overhead and S3 API costs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The execution order matters: expire first, then clean orphans, then compact, then consolidate manifests. Running them out of sequence wastes compute or risks removing files still in use.&lt;/p&gt;

&lt;p&gt;When you want all four automated together, &lt;strong&gt;Adaptive Maintenance&lt;/strong&gt; bundles them into a single data-driven policy that reacts to table activity — the closest equivalent to Netflix's integrated approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Full-stack observability without building a pipeline
&lt;/h3&gt;

&lt;p&gt;Netflix's Metacat provides unified metadata access across all datasets, backed by Kafka event streams for real-time operational visibility. Building this took years and a dedicated team.&lt;/p&gt;

&lt;p&gt;Out-of-the-box observability should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Table health classification&lt;/strong&gt; — every table scored as Healthy, Warning, or Critical based on file counts, size distributions, snapshot accumulation, and metadata fragmentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-generated insights&lt;/strong&gt; — ranked recommendations that flag small-file hotspots, excessive snapshots, and missing retention before they become incidents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event audit trail&lt;/strong&gt; — every maintenance operation recorded with before/after metrics, timestamps, and status — per-table or lake-wide, filterable by catalog and operation type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt; — total operations, query speed gains, cost savings, and resource reduction in a single view&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi4104rljn59aiw6g2699.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi4104rljn59aiw6g2699.png" alt="LakeOps operations dashboard" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And table health dashboards:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ybhe4nhlj96tw0ziny2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ybhe4nhlj96tw0ziny2.png" alt="LakeOps table health monitoring" width="799" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The difference from building it yourself is time-to-value: connect a catalog and immediately see what's degraded, what's wasting money, and what to fix first.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Policy-driven governance that scales with the lake
&lt;/h3&gt;

&lt;p&gt;Configuring maintenance table-by-table stops working somewhere between 50 and 100 tables. Netflix needed organization-wide rules; so does every team that's past the proof-of-concept stage.&lt;/p&gt;

&lt;p&gt;A policy engine lets you define maintenance rules at the catalog or namespace level — snapshot retention every hour, orphan cleanup daily, compaction at 2 AM — and every table in scope inherits them automatically. New tables that appear in a governed catalog get the right configuration without anyone touching them.&lt;/p&gt;

&lt;p&gt;Two policy categories cover the ground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance policies&lt;/strong&gt; — schedule and configure any operation (or all of them via Adaptive Maintenance) across a scope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration policies&lt;/strong&gt; — enforce table settings like Iceberg format version, file format, and write distribution mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8lm759daecu30k0ncui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8lm759daecu30k0ncui.png" alt="LakeOps policy engine" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Per-table overrides always take precedence, so you set sensible defaults broadly and customize only where needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Multi-engine query routing
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F50w1yoc3limmexvhfn85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F50w1yoc3limmexvhfn85.png" alt="Multi-engine lakehouse architecture" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Netflix connects engines through the REST catalog endpoint, but routing decisions — which engine handles which query — remain manual architecture choices in most organizations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxnd9gjnt85uzt1f838z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxnd9gjnt85uzt1f838z.png" alt="Query routing across engines" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An intelligent routing layer dispatches queries to the best engine based on the workload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost-optimized&lt;/strong&gt; — sends queries to the cheapest engine that meets your latency SLA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency-optimized&lt;/strong&gt; — picks the fastest engine for the query shape&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput-optimized&lt;/strong&gt; — distributes load for maximum concurrency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Applications connect to a single SQL endpoint (Postgres wire, MySQL wire, or Arrow Flight). When an engine goes down, failover reroutes automatically. When you add or remove engines, application code doesn't change.&lt;/p&gt;

&lt;p&gt;LakeOps handles this through &lt;a href="https://github.com/lakeops-org/queryflux" rel="noopener noreferrer"&gt;QueryFlux&lt;/a&gt;, an open-source Rust SQL proxy that translates SQL dialects with sqlglot and adds ~0.35ms of overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this adds up to
&lt;/h2&gt;

&lt;p&gt;Each component is useful independently. Together, they compound:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage costs&lt;/strong&gt; drop 40–55% from continuous orphan removal, bounded snapshots, and compaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute costs&lt;/strong&gt; drop up to 75% from Rust-native compaction replacing Spark clusters, sort-order optimization reducing scan volume, and routing hitting the cheapest viable engine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query latency&lt;/strong&gt; improves up to 12x through optimized file sizes, sorted layouts, consolidated manifests, and Puffin column statistics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineering hours&lt;/strong&gt; shift from maintaining scripts and debugging overnight failures to building data products&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foohbi6d5fe5b1ix1ovw0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foohbi6d5fe5b1ix1ovw0.png" alt="Cost reduction with LakeOps" width="798" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond human users: AI agent access
&lt;/h2&gt;

&lt;p&gt;The next layer of intelligence is enabling AI agents to interact with lakehouse data programmatically. LakeOps provides an &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP (Model Context Protocol)&lt;/a&gt; interface that gives agents structured access — table discovery, SQL execution through the routing layer, column statistics without scanning, and maintenance triggers — all within configurable guardrails.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8csmopufyoq9khal3pq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8csmopufyoq9khal3pq.png" alt="AI agent access to the lakehouse" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can enforce read-only access, row limits, PII masking, cost caps, and human approval per agent. As agent usage grows, their query telemetry feeds back into compaction decisions — tables agents query most get optimized first, with sort orders aligned to the predicates agents actually use. The lake self-optimizes as AI adoption scales.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/irRsF9VYP20"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Netflix took years to build their intelligent lakehouse with dedicated teams.&lt;/p&gt;

&lt;p&gt;The same architecture is now accessible in about ten minutes.&lt;/p&gt;

&lt;p&gt;Visit &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;lakeops.dev&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connect your catalog&lt;/strong&gt; — Glue, DynamoDB, REST, S3 Tables, or Custom. Every table is discovered automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize a few tables&lt;/strong&gt; — run compaction or snapshot expiration manually, review results, then flip to automated scheduling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale with policies&lt;/strong&gt; — define rules at the catalog or namespace level. New tables inherit everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor&lt;/strong&gt; — the dashboard shows real-time impact, insights flag what needs attention, events provide the audit trail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your data never leaves your account. No agents to install, no pipelines to change, no infrastructure to provision.&lt;/p&gt;

&lt;p&gt;The intelligent lakehouse is no longer reserved for companies that can build Netflix-scale infrastructure. The building blocks are here. The question is whether your tables are maintained — or quietly degrading while you read this.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>devops</category>
      <category>opensource</category>
      <category>aws</category>
    </item>
    <item>
      <title>Managed Iceberg: Optimizing a Modern Lakehouse</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Sun, 10 May 2026 13:52:17 +0000</pubDate>
      <link>https://dev.to/jonisar/managed-iceberg-optimizing-a-modern-lakehouse-1jld</link>
      <guid>https://dev.to/jonisar/managed-iceberg-optimizing-a-modern-lakehouse-1jld</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F95ljo39oiaqfcchin1ee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F95ljo39oiaqfcchin1ee.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A modern lakehouse looks simple from the outside.&lt;/p&gt;

&lt;p&gt;Data lands in object storage. Apache Iceberg gives you tables, snapshots, schema evolution, time travel, and multi-engine access. Spark writes. Trino queries. Flink streams. Snowflake or Athena may read the same data. Everyone is happy.&lt;/p&gt;

&lt;p&gt;Then the lakehouse starts growing.&lt;/p&gt;

&lt;p&gt;Small files pile up. Snapshots never expire. Manifest metadata gets heavier. Delete files slow down reads. Failed jobs leave orphan files behind. Query planning becomes slower. Storage cost grows in places nobody is watching. Every engine has its own behavior, its own tuning, and its own operational gaps.&lt;/p&gt;

&lt;p&gt;This is the part that gets underestimated.&lt;/p&gt;

&lt;p&gt;Iceberg solves the table format problem. It does not magically solve lakehouse operations.&lt;/p&gt;

&lt;p&gt;The same pattern already happened in compute infrastructure: once systems grew large enough, manual tuning stopped scaling and platforms like &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; became useful because they continuously optimized resources instead of relying on people to chase every inefficiency.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/solutions/agentic-ai-data" rel="noopener noreferrer"&gt;Iceberg lakehouses&lt;/a&gt; need the same shift. Not more scripts. Not more periodic cleanup jobs. A real control plane.&lt;/p&gt;

&lt;p&gt;That is the idea behind &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt;: autonomous lakehouse management for Apache Iceberg. It sits above your lake, watches table and engine behavior, and continuously manages the operational work that keeps Iceberg fast, clean, and cost-efficient.&lt;/p&gt;

&lt;p&gt;This article is a practical guide to what that means.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real job of running an Iceberg lakehouse
&lt;/h2&gt;

&lt;p&gt;When teams first adopt Iceberg, the focus is usually on features.&lt;/p&gt;

&lt;p&gt;ACID transactions on object storage.&lt;/p&gt;

&lt;p&gt;Time travel.&lt;/p&gt;

&lt;p&gt;Schema evolution.&lt;/p&gt;

&lt;p&gt;Partition evolution.&lt;/p&gt;

&lt;p&gt;Hidden partitioning.&lt;/p&gt;

&lt;p&gt;Multiple engines reading the same tables.&lt;/p&gt;

&lt;p&gt;Those are important. They are why Iceberg became popular.&lt;/p&gt;

&lt;p&gt;But after the first production workloads move in, the job changes. You are no longer just “using Iceberg.” You are operating a lakehouse.&lt;/p&gt;

&lt;p&gt;That means you are responsible for table layout, file size, metadata growth, snapshot retention, stale data, query planning, engine behavior, storage waste, and workload safety.&lt;/p&gt;

&lt;p&gt;A healthy lakehouse needs constant maintenance.&lt;/p&gt;

&lt;p&gt;A production Iceberg table is not static. Every ingest, append, merge, delete, compaction, schema change, or streaming write changes its physical and metadata shape. Even when the logical table looks clean, the underlying table may be drifting.&lt;/p&gt;

&lt;p&gt;That drift is the problem.&lt;/p&gt;

&lt;p&gt;A table can still return correct results while becoming slower and more expensive every week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where lakehouse maintenance gets painful
&lt;/h2&gt;

&lt;p&gt;The pain usually appears in a few predictable places.&lt;/p&gt;

&lt;p&gt;Small files are the first one. Streaming jobs, CDC pipelines, frequent appends, and micro-batches create many small files. Query engines then spend too much time opening files, planning splits, and scanning inefficiently. A table that should be read in seconds starts feeling heavy.&lt;/p&gt;

&lt;p&gt;Snapshots are the second. Iceberg creates snapshots so readers can see consistent table versions and users can time travel or roll back. That is useful, but old snapshots accumulate unless someone expires them. Over time, they keep metadata and old data references alive.&lt;/p&gt;

&lt;p&gt;Manifests are the third. Iceberg tracks data files through metadata files. That is what makes Iceberg reliable and engine-independent, but metadata also needs maintenance. If manifests grow or fragment, query planning slows down before the engine even starts scanning data.&lt;/p&gt;

&lt;p&gt;Orphan files are the fourth. Failed writes, aborted jobs, migrations, dropped tables, and imperfect cleanup flows can leave files in object storage that are no longer referenced by Iceberg metadata. Queries do not read them, but storage still bills for them.&lt;/p&gt;

&lt;p&gt;Delete files are another common issue, especially with merge-on-read workloads. If they accumulate, every query may pay the cost of applying deletes at read time.&lt;/p&gt;

&lt;p&gt;Then there is the engine layer. Spark, Trino, Flink, Athena, Snowflake, Databricks, DuckDB, and other engines do not behave the same way. They have different cost models, latency profiles, concurrency limits, and operational strengths. Managing Iceberg across engines is not the same as managing one Spark pipeline.&lt;/p&gt;

&lt;p&gt;This is why “we have Iceberg” is not the same as “we have a managed lakehouse.”&lt;/p&gt;

&lt;h2&gt;
  
  
  The manual way most teams start
&lt;/h2&gt;

&lt;p&gt;Most teams start with scripts.&lt;/p&gt;

&lt;p&gt;A Spark job for compaction.&lt;/p&gt;

&lt;p&gt;A scheduled job for snapshot expiration.&lt;/p&gt;

&lt;p&gt;A cleanup script for orphan files.&lt;/p&gt;

&lt;p&gt;A few dashboards.&lt;/p&gt;

&lt;p&gt;Some alerts.&lt;/p&gt;

&lt;p&gt;A runbook in a wiki.&lt;/p&gt;

&lt;p&gt;A Slack channel where someone asks, “Why is this table slow again?”&lt;/p&gt;

&lt;p&gt;There is nothing wrong with this as a starting point. It is how most platforms mature.&lt;/p&gt;

&lt;p&gt;The problem is that static maintenance does not understand table behavior.&lt;/p&gt;

&lt;p&gt;A daily compaction job does not know whether a table had a quiet day or a massive ingestion spike.&lt;/p&gt;

&lt;p&gt;A weekly snapshot cleanup job does not know whether the table has long-running readers, branch retention requirements, or compliance rules.&lt;/p&gt;

&lt;p&gt;A manifest rewrite schedule does not know which tables are suffering from planning latency.&lt;/p&gt;

&lt;p&gt;A generic orphan cleanup script may be too conservative to reclaim meaningful storage or too aggressive to be safe.&lt;/p&gt;

&lt;p&gt;And none of this naturally connects to cost, query performance, engine behavior, or table-level business importance.&lt;/p&gt;

&lt;p&gt;Manual maintenance works when you have a small number of tables and a small number of workloads. At lakehouse scale, it becomes operational debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What managed Iceberg means
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/blog/managed-iceberg-2026" rel="noopener noreferrer"&gt;Managed Iceberg&lt;/a&gt; means the maintenance loop becomes part of the platform.&lt;/p&gt;

&lt;p&gt;Not a one-off script.&lt;/p&gt;

&lt;p&gt;Not a quarterly cleanup project.&lt;/p&gt;

&lt;p&gt;Not a few jobs someone hopes are still running.&lt;/p&gt;

&lt;p&gt;A managed Iceberg layer continuously observes the lakehouse, decides what needs attention, runs the right operation, and records the result.&lt;/p&gt;

&lt;p&gt;It should manage the core lifecycle of Iceberg tables:&lt;/p&gt;

&lt;p&gt;Compaction.&lt;/p&gt;

&lt;p&gt;Snapshot expiration.&lt;/p&gt;

&lt;p&gt;Manifest optimization.&lt;/p&gt;

&lt;p&gt;Orphan file cleanup.&lt;/p&gt;

&lt;p&gt;Delete file handling.&lt;/p&gt;

&lt;p&gt;Statistics and metadata optimization.&lt;/p&gt;

&lt;p&gt;Table health monitoring.&lt;/p&gt;

&lt;p&gt;Policy enforcement.&lt;/p&gt;

&lt;p&gt;Engine visibility.&lt;/p&gt;

&lt;p&gt;Cost and performance tracking.&lt;/p&gt;

&lt;p&gt;The key point is that management should be table-aware and workload-aware.&lt;/p&gt;

&lt;p&gt;A hot BI table is not the same as a streaming staging table. A CDC table is not the same as a cold archive table. A table queried by Trino all day is not the same as a table used by a nightly Spark job. A table &lt;a href="http://localhost:3000/solutions/agentic-ai-data" rel="noopener noreferrer"&gt;exposed to AI agents has different risk&lt;/a&gt; and cost patterns than an internal batch table.&lt;/p&gt;

&lt;p&gt;A managed lakehouse should understand those differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  The control plane model
&lt;/h2&gt;

&lt;p&gt;A lakehouse control plane is the layer that coordinates operations across storage, Iceberg metadata, catalogs, engines, policies, and observability.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj7rygqzol7un6ahltyh0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj7rygqzol7un6ahltyh0.png" alt=" " width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftfq582wofg8mugvp803c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftfq582wofg8mugvp803c.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhn5oavgi12ju96yfzkta.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhn5oavgi12ju96yfzkta.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It does not replace Iceberg.&lt;/p&gt;

&lt;p&gt;It does not replace your object storage.&lt;/p&gt;

&lt;p&gt;It does not force all teams into one query engine.&lt;/p&gt;

&lt;p&gt;It gives you one operating layer for the lakehouse.&lt;/p&gt;

&lt;p&gt;LakeOps describes this as a &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;control plane for your data lake&lt;/a&gt;: end-to-end optimization for tables and metadata across storage and query engines, with telemetry-driven orchestration and visibility in one place.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;The goal is not to make Iceberg proprietary. The goal is to make open lakehouse operations manageable.&lt;/p&gt;

&lt;p&gt;A good control plane should answer questions like:&lt;/p&gt;

&lt;p&gt;Which tables are unhealthy?&lt;/p&gt;

&lt;p&gt;Which tables are wasting the most storage?&lt;/p&gt;

&lt;p&gt;Which tables have the worst small-file problem?&lt;/p&gt;

&lt;p&gt;Which tables have metadata planning issues?&lt;/p&gt;

&lt;p&gt;Which tables should be compacted now?&lt;/p&gt;

&lt;p&gt;Which tables should not be touched because active workloads are running?&lt;/p&gt;

&lt;p&gt;Which compaction strategy should be used?&lt;/p&gt;

&lt;p&gt;Which snapshots can safely expire?&lt;/p&gt;

&lt;p&gt;Which files are safe to delete?&lt;/p&gt;

&lt;p&gt;Which engine is best for this workload?&lt;/p&gt;

&lt;p&gt;Did the optimization actually improve cost or performance?&lt;/p&gt;

&lt;p&gt;If you cannot answer these questions quickly, the lakehouse is being managed manually, even if it has automation scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solving the small-file problem
&lt;/h2&gt;

&lt;p&gt;Small files are the most visible Iceberg maintenance issue.&lt;/p&gt;

&lt;p&gt;They usually come from streaming ingestion, frequent appends, CDC, micro-batches, partition skew, and multi-writer workloads. The result is predictable: more files, more metadata, more object-store requests, more planning work, and slower queries.&lt;/p&gt;

&lt;p&gt;The normal fix is compaction.&lt;/p&gt;

&lt;p&gt;But not all compaction is equal.&lt;/p&gt;

&lt;p&gt;The simple version is bin-packing: combine many small files into fewer larger files. This is often the right first step because it quickly reduces file count and improves scan efficiency.&lt;/p&gt;

&lt;p&gt;The more advanced version is sort-based compaction: rewrite files according to the columns that queries filter or join on most often. This can improve data skipping and reduce scanned data, but it is more workload-sensitive. Sorting everything blindly can waste compute.&lt;/p&gt;

&lt;p&gt;This is where autonomous management becomes useful.&lt;/p&gt;

&lt;p&gt;LakeOps includes &lt;a href="https://lakeops.dev/docs/compaction" rel="noopener noreferrer"&gt;compaction for Apache Iceberg&lt;/a&gt; that uses table metadata and query patterns to decide which files to rewrite and how. The useful part is not only that it compacts. The useful part is that compaction becomes part of a continuous feedback loop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67i87rinfjvkt2e51sfe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67i87rinfjvkt2e51sfe.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A practical operating model looks like this:&lt;/p&gt;

&lt;p&gt;Start with bin-pack compaction on tables with severe small-file pressure.&lt;/p&gt;

&lt;p&gt;Use query-aware sort compaction only where query patterns justify it.&lt;/p&gt;

&lt;p&gt;Avoid compacting cold tables just because a schedule says so.&lt;/p&gt;

&lt;p&gt;Prioritize tables where compaction will reduce real query cost or latency.&lt;/p&gt;

&lt;p&gt;Track before-and-after impact: file count, data scanned, planning time, runtime, and cost.&lt;/p&gt;

&lt;p&gt;That is the difference between maintenance and optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing snapshots safely
&lt;/h2&gt;

&lt;p&gt;Snapshots are one of the best things about Iceberg.&lt;/p&gt;

&lt;p&gt;They enable time travel, rollback, auditability, and consistent reads. But they also create retention work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F80tnxidfo4w9hvhi7fpe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F80tnxidfo4w9hvhi7fpe.png" alt=" " width="800" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every write creates a new table version. If snapshots are never expired, metadata grows and old data can remain retained longer than needed. On busy tables, this becomes a real cost and performance issue.&lt;/p&gt;

&lt;p&gt;The hard part is not running &lt;code&gt;expire_snapshots&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The hard part is knowing the right policy.&lt;/p&gt;

&lt;p&gt;Some tables need long time-travel windows because they support audits, debugging, or recovery. Some tables only need short retention. Some tables may have branches or tags that must be protected. Some workloads may have long-running readers. Some environments need different rules for production, staging, and development.&lt;/p&gt;

&lt;p&gt;A managed layer should make this explicit.&lt;/p&gt;

&lt;p&gt;LakeOps provides &lt;a href="https://lakeops.dev/docs/snapshots" rel="noopener noreferrer"&gt;snapshot management&lt;/a&gt; as part of table optimization, so retention can be controlled through policies rather than remembered manually per table.&lt;/p&gt;

&lt;p&gt;For platform teams, this is a major shift.&lt;/p&gt;

&lt;p&gt;Instead of asking, “Did someone remember to clean up snapshots on this table?”&lt;/p&gt;

&lt;p&gt;You define retention behavior once, apply it at the right scope, and let the platform enforce it continuously.&lt;/p&gt;

&lt;p&gt;A good default might be:&lt;/p&gt;

&lt;p&gt;Keep enough snapshots for rollback and debugging.&lt;/p&gt;

&lt;p&gt;Retain a minimum number of recent snapshots.&lt;/p&gt;

&lt;p&gt;Use longer retention for critical regulated tables.&lt;/p&gt;

&lt;p&gt;Use shorter retention for temporary or staging data.&lt;/p&gt;

&lt;p&gt;Monitor how much storage is blocked by old snapshots.&lt;/p&gt;

&lt;p&gt;Run expiration before orphan cleanup.&lt;/p&gt;

&lt;p&gt;The exact values depend on the organization. The important thing is that snapshot retention becomes intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping metadata lean with manifest optimization
&lt;/h2&gt;

&lt;p&gt;Iceberg query performance is not only about data files.&lt;/p&gt;

&lt;p&gt;It is also about planning.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb05geve4nz42ykbieo5h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb05geve4nz42ykbieo5h.png" alt=" " width="800" height="675"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before an engine scans data, it reads Iceberg metadata to understand which files belong to the snapshot and which files can be skipped. Manifest files are part of this metadata layer. They are essential, but they can also become fragmented over time.&lt;/p&gt;

&lt;p&gt;When manifests grow poorly, planning time grows. Users experience this as “the query is slow,” but the engine may be spending too much time before meaningful scanning even begins.&lt;/p&gt;

&lt;p&gt;This is easy to miss if you only look at execution time.&lt;/p&gt;

&lt;p&gt;A managed Iceberg system should monitor metadata health directly.&lt;/p&gt;

&lt;p&gt;LakeOps includes &lt;a href="https://lakeops.dev/docs/manifests" rel="noopener noreferrer"&gt;manifest optimization&lt;/a&gt; so teams can consolidate and optimize metadata as part of the same table health loop.&lt;/p&gt;

&lt;p&gt;The principle is simple: metadata is part of performance.&lt;/p&gt;

&lt;p&gt;If you only compact data files but ignore manifests, you are only managing half the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning orphan files without breaking things
&lt;/h2&gt;

&lt;p&gt;Orphan files are a storage leak.&lt;/p&gt;

&lt;p&gt;They sit in object storage but are not referenced by Iceberg metadata. Queries do not use them, but the cloud provider still charges for them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F447b4jfhwd59h2x6p8ye.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F447b4jfhwd59h2x6p8ye.png" alt=" " width="799" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They can appear after failed jobs, aborted commits, manual migrations, dropped tables, incorrect cleanup flows, or maintenance operations that leave old data behind.&lt;/p&gt;

&lt;p&gt;The dangerous part is cleanup.&lt;/p&gt;

&lt;p&gt;Deleting files from a data lake is easy. Deleting the right files safely is hard.&lt;/p&gt;

&lt;p&gt;A safe orphan cleanup process must compare files in storage against Iceberg metadata, apply a conservative age threshold, avoid active write windows, and usually run after snapshot expiration. The age threshold matters because a file that looks unreferenced during an in-progress write may still be committed later.&lt;/p&gt;

&lt;p&gt;LakeOps documents &lt;a href="https://lakeops.dev/docs/orphan-cleanup" rel="noopener noreferrer"&gt;orphan file cleanup&lt;/a&gt; as a managed operation with metadata awareness and safety controls.&lt;/p&gt;

&lt;p&gt;In practice, this is one of the strongest arguments for a &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;control plane&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Nobody wants platform engineers manually reviewing millions of object-store paths. Nobody wants an unsafe script deleting files from production. And nobody wants to keep paying for dead data because cleanup feels risky.&lt;/p&gt;

&lt;p&gt;Managed orphan cleanup should be boring, visible, and conservative.&lt;/p&gt;

&lt;p&gt;Run a dry run.&lt;/p&gt;

&lt;p&gt;Show candidates.&lt;/p&gt;

&lt;p&gt;Apply retention thresholds.&lt;/p&gt;

&lt;p&gt;Delete only when safe.&lt;/p&gt;

&lt;p&gt;Record what was removed.&lt;/p&gt;

&lt;p&gt;Measure storage reclaimed.&lt;/p&gt;

&lt;p&gt;That is how cleanup becomes an operational capability instead of a dangerous maintenance task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policies are what make this scale
&lt;/h2&gt;

&lt;p&gt;Manual tuning does not scale across hundreds or thousands of tables.&lt;/p&gt;

&lt;p&gt;You need policies.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7p832wiuj4uhkx59xdo4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7p832wiuj4uhkx59xdo4.png" alt=" " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Policies let you define how table maintenance should behave at different scopes: organization, catalog, namespace, table, environment, or workload class.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Production BI tables get frequent compaction and manifest optimization.&lt;/p&gt;

&lt;p&gt;Streaming tables get aggressive small-file management.&lt;/p&gt;

&lt;p&gt;CDC tables get delete-file-aware compaction.&lt;/p&gt;

&lt;p&gt;Staging tables get short snapshot retention.&lt;/p&gt;

&lt;p&gt;Archive tables get minimal compute-heavy optimization but regular storage cleanup.&lt;/p&gt;

&lt;p&gt;Critical tables require approvals or simulations before major rewrites.&lt;/p&gt;

&lt;p&gt;Development tables get cheaper, more aggressive cleanup.&lt;/p&gt;

&lt;p&gt;LakeOps supports &lt;a href="https://lakeops.dev/docs/policies" rel="noopener noreferrer"&gt;policies&lt;/a&gt; for maintenance automation, allowing teams to define behavior for compaction, snapshots, manifests, orphan cleanup, and governance across the lakehouse.&lt;/p&gt;

&lt;p&gt;This is important because the platform team should not be in the business of hand-tuning every table forever.&lt;/p&gt;

&lt;p&gt;Good policies give teams defaults, guardrails, and exceptions.&lt;/p&gt;

&lt;p&gt;That is how Iceberg operations become manageable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability turns maintenance into engineering
&lt;/h2&gt;

&lt;p&gt;If maintenance runs but nobody can measure the effect, it is not really managed.&lt;/p&gt;

&lt;p&gt;A lakehouse control plane needs observability at the table, engine, and operation level.&lt;/p&gt;

&lt;p&gt;You need to see:&lt;/p&gt;

&lt;p&gt;Table health.&lt;/p&gt;

&lt;p&gt;File count.&lt;/p&gt;

&lt;p&gt;Average file size.&lt;/p&gt;

&lt;p&gt;Small-file pressure.&lt;/p&gt;

&lt;p&gt;Snapshot count.&lt;/p&gt;

&lt;p&gt;Manifest count.&lt;/p&gt;

&lt;p&gt;Delete file pressure.&lt;/p&gt;

&lt;p&gt;Storage waste.&lt;/p&gt;

&lt;p&gt;Query latency.&lt;/p&gt;

&lt;p&gt;Planning time.&lt;/p&gt;

&lt;p&gt;Data scanned.&lt;/p&gt;

&lt;p&gt;Engine cost.&lt;/p&gt;

&lt;p&gt;Operation history.&lt;/p&gt;

&lt;p&gt;Before-and-after optimization impact.&lt;/p&gt;

&lt;p&gt;Failed or skipped operations.&lt;/p&gt;

&lt;p&gt;Policy coverage.&lt;/p&gt;

&lt;p&gt;LakeOps includes &lt;a href="https://lakeops.dev/docs/observability" rel="noopener noreferrer"&gt;lakehouse observability&lt;/a&gt; so platform teams can see table health, engine metrics, cross-system telemetry, and maintenance history from one place.&lt;/p&gt;

&lt;p&gt;This changes how you operate.&lt;/p&gt;

&lt;p&gt;Instead of waiting for users to complain that dashboards are slow, you can see which tables are drifting.&lt;/p&gt;

&lt;p&gt;Instead of guessing whether compaction helped, you can measure the before and after.&lt;/p&gt;

&lt;p&gt;Instead of discovering storage waste in a cloud bill, you can identify stale data and orphan files directly.&lt;/p&gt;

&lt;p&gt;Good observability turns Iceberg maintenance from reactive firefighting into normal platform engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-engine lakehouses need engine-aware management
&lt;/h2&gt;

&lt;p&gt;The whole point of Iceberg is that many engines can work over the same tables.&lt;/p&gt;

&lt;p&gt;That is also what makes operations harder.&lt;/p&gt;

&lt;p&gt;Spark may be good for heavy rewrites.&lt;/p&gt;

&lt;p&gt;Trino may be better for interactive analytics.&lt;/p&gt;

&lt;p&gt;Athena may be useful for serverless access.&lt;/p&gt;

&lt;p&gt;Snowflake may serve BI workloads.&lt;/p&gt;

&lt;p&gt;Flink may write continuously.&lt;/p&gt;

&lt;p&gt;DuckDB may support local or embedded analytics.&lt;/p&gt;

&lt;p&gt;Each engine has a different performance model. Each workload has a different latency and cost profile.&lt;/p&gt;

&lt;p&gt;A managed lakehouse should not pretend all engines are the same.&lt;/p&gt;

&lt;p&gt;LakeOps supports &lt;a href="https://lakeops.dev/docs/engines" rel="noopener noreferrer"&gt;engine management&lt;/a&gt; and &lt;a href="https://lakeops.dev/docs/query-routing" rel="noopener noreferrer"&gt;query routing&lt;/a&gt;, giving teams a unified view of engine health, cost, usage, and routing behavior.&lt;/p&gt;

&lt;p&gt;This matters because optimization is not only about the table. It is also about where and &lt;a href="https://lakeops.dev/blog/iceberg-cost-optimization-2026" rel="noopener noreferrer"&gt;how workloads run&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sometimes the best optimization is a better file layout.&lt;/p&gt;

&lt;p&gt;Sometimes it is a better engine choice.&lt;/p&gt;

&lt;p&gt;Sometimes it is avoiding an expensive engine for simple queries.&lt;/p&gt;

&lt;p&gt;Sometimes it is routing a workload away from an unhealthy engine.&lt;/p&gt;

&lt;p&gt;A modern lakehouse control plane should see the full system, not just the storage layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why continuous optimization beats scheduled maintenance
&lt;/h2&gt;

&lt;p&gt;The old model is schedule-based.&lt;/p&gt;

&lt;p&gt;Run compaction every night.&lt;/p&gt;

&lt;p&gt;Expire snapshots every Sunday.&lt;/p&gt;

&lt;p&gt;Clean orphan files once a month.&lt;/p&gt;

&lt;p&gt;Rewrite manifests when someone remembers.&lt;/p&gt;

&lt;p&gt;That is better than nothing, but it is not how real workloads behave.&lt;/p&gt;

&lt;p&gt;A high-volume table may need attention multiple times a day.&lt;/p&gt;

&lt;p&gt;A cold table may not need compaction for months.&lt;/p&gt;

&lt;p&gt;A table may become hot because a new dashboard launched.&lt;/p&gt;

&lt;p&gt;A backfill may create temporary file pressure.&lt;/p&gt;

&lt;p&gt;A failed ingestion job may create orphan files.&lt;/p&gt;

&lt;p&gt;A new AI agent may generate many new query patterns.&lt;/p&gt;

&lt;p&gt;A static schedule cannot react to that.&lt;/p&gt;

&lt;p&gt;Continuous optimization uses telemetry to decide what should happen next.&lt;/p&gt;

&lt;p&gt;That is the core value of autonomous lakehouse management.&lt;/p&gt;

&lt;p&gt;The lakehouse is not optimized because a cron job ran. It is optimized because the platform understands table state, workload behavior, and &lt;a href="https://lakeops.dev/solutions/iceberg-cost-optimization" rel="noopener noreferrer"&gt;cost impact&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is where LakeOps is useful in practice. It continuously analyzes the lakehouse, recommends or runs the right operation, and keeps optimizing as workloads change.&lt;/p&gt;

&lt;p&gt;For a platform team, this reduces the amount of manual judgment required for routine operations.&lt;/p&gt;

&lt;p&gt;You still set policies.&lt;/p&gt;

&lt;p&gt;You still define guardrails.&lt;/p&gt;

&lt;p&gt;You still decide what level of autonomy is acceptable.&lt;/p&gt;

&lt;p&gt;But you are no longer manually chasing every unhealthy table.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical rollout plan
&lt;/h2&gt;

&lt;p&gt;The best way to adopt managed Iceberg is not to turn everything on everywhere.&lt;/p&gt;

&lt;p&gt;Start with visibility.&lt;/p&gt;

&lt;p&gt;Connect the catalogs and engines. Let the platform observe table health, file layout, metadata, snapshots, and query behavior. Identify the worst tables by cost, latency, file count, metadata weight, and storage waste.&lt;/p&gt;

&lt;p&gt;Then start with a small set of high-impact tables.&lt;/p&gt;

&lt;p&gt;Good candidates are usually:&lt;/p&gt;

&lt;p&gt;Large tables with many small files.&lt;/p&gt;

&lt;p&gt;Hot BI tables with growing latency.&lt;/p&gt;

&lt;p&gt;Streaming or CDC tables with constant writes.&lt;/p&gt;

&lt;p&gt;Tables with high object-store request cost.&lt;/p&gt;

&lt;p&gt;Tables with many snapshots.&lt;/p&gt;

&lt;p&gt;Tables where users already complain about performance.&lt;/p&gt;

&lt;p&gt;Apply conservative policies first.&lt;/p&gt;

&lt;p&gt;Use bin-pack compaction before sort compaction.&lt;/p&gt;

&lt;p&gt;Use dry runs for cleanup operations.&lt;/p&gt;

&lt;p&gt;Set safe snapshot retention.&lt;/p&gt;

&lt;p&gt;Run orphan cleanup with conservative age thresholds.&lt;/p&gt;

&lt;p&gt;Measure everything.&lt;/p&gt;

&lt;p&gt;Only after you see stable improvements should you expand to more tables, more aggressive compaction, sort optimization, and autonomous mode.&lt;/p&gt;

&lt;p&gt;The point is not to give control away. The point is to move from manual table-by-table work to policy-driven operations with visibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  What LakeOps solves, problem by problem
&lt;/h2&gt;

&lt;p&gt;If you maintain Iceberg yourself, you eventually build pieces of a control plane internally.&lt;/p&gt;

&lt;p&gt;You build table health checks.&lt;/p&gt;

&lt;p&gt;You build compaction jobs.&lt;/p&gt;

&lt;p&gt;You build snapshot cleanup.&lt;/p&gt;

&lt;p&gt;You build orphan cleanup.&lt;/p&gt;

&lt;p&gt;You build dashboards.&lt;/p&gt;

&lt;p&gt;You build job orchestration.&lt;/p&gt;

&lt;p&gt;You build policy conventions.&lt;/p&gt;

&lt;p&gt;You build alerts.&lt;/p&gt;

&lt;p&gt;You build runbooks.&lt;/p&gt;

&lt;p&gt;You build engine-specific scripts.&lt;/p&gt;

&lt;p&gt;You build cost reports.&lt;/p&gt;

&lt;p&gt;Then you maintain all of that.&lt;/p&gt;

&lt;p&gt;LakeOps packages that operating layer into one platform.&lt;/p&gt;

&lt;p&gt;For small files, it provides autonomous compaction and layout optimization.&lt;/p&gt;

&lt;p&gt;For slow queries, it optimizes file sizes, sort layout, manifests, and routing decisions.&lt;/p&gt;

&lt;p&gt;For snapshot bloat, it manages retention policies.&lt;/p&gt;

&lt;p&gt;For orphan files, it performs safe metadata-aware cleanup.&lt;/p&gt;

&lt;p&gt;For fragmented metadata, it rewrites and optimizes manifests.&lt;/p&gt;

&lt;p&gt;For multi-engine complexity, it gives one view of engines and can route workloads based on cost, latency, or throughput.&lt;/p&gt;

&lt;p&gt;For operational visibility, it surfaces table health, engine metrics, events, recommendations, and optimization history.&lt;/p&gt;

&lt;p&gt;For governance, it gives policies, auditability, and controlled automation.&lt;/p&gt;

&lt;p&gt;For adoption risk, it works with the existing lakehouse stack instead of requiring pipeline rewrites or data movement.&lt;/p&gt;

&lt;p&gt;That last point is important.&lt;/p&gt;

&lt;p&gt;A control plane should reduce operational burden without becoming a migration project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to keep managing yourself
&lt;/h2&gt;

&lt;p&gt;Autonomous management does not mean the platform team disappears.&lt;/p&gt;

&lt;p&gt;You still own architecture.&lt;/p&gt;

&lt;p&gt;You still own data modeling.&lt;/p&gt;

&lt;p&gt;You still decide retention requirements.&lt;/p&gt;

&lt;p&gt;You still define governance boundaries.&lt;/p&gt;

&lt;p&gt;You still choose which engines belong in the platform.&lt;/p&gt;

&lt;p&gt;You still control policies and exceptions.&lt;/p&gt;

&lt;p&gt;You still review critical workloads.&lt;/p&gt;

&lt;p&gt;The difference is where your time goes.&lt;/p&gt;

&lt;p&gt;Instead of manually compacting tables, you define compaction policies.&lt;/p&gt;

&lt;p&gt;Instead of hunting stale files, you monitor cleanup impact.&lt;/p&gt;

&lt;p&gt;Instead of guessing why queries slowed down, you inspect table and engine telemetry.&lt;/p&gt;

&lt;p&gt;Instead of writing one-off Spark jobs, you operate the lakehouse as a managed platform.&lt;/p&gt;

&lt;p&gt;That is a better use of senior data platform engineering time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benefits beyond cost and performance
&lt;/h2&gt;

&lt;p&gt;Cost and performance are the obvious wins.&lt;/p&gt;

&lt;p&gt;Fewer small files means less scan overhead.&lt;/p&gt;

&lt;p&gt;Cleaner metadata means faster planning.&lt;/p&gt;

&lt;p&gt;Expired snapshots and orphan cleanup reduce storage waste.&lt;/p&gt;

&lt;p&gt;Better layout reduces data scanned.&lt;/p&gt;

&lt;p&gt;Better routing reduces unnecessary compute.&lt;/p&gt;

&lt;p&gt;But there are other benefits that matter just as much.&lt;/p&gt;

&lt;p&gt;Reliability improves because maintenance is consistent instead of ad hoc.&lt;/p&gt;

&lt;p&gt;Governance improves because policies are explicit.&lt;/p&gt;

&lt;p&gt;Debugging improves because every operation is visible.&lt;/p&gt;

&lt;p&gt;Onboarding improves because new tables inherit sane defaults.&lt;/p&gt;

&lt;p&gt;Security improves when access and actions are auditable.&lt;/p&gt;

&lt;p&gt;Capacity planning improves because table growth and engine behavior are observable.&lt;/p&gt;

&lt;p&gt;AI readiness improves because agents query cleaner, faster, better-governed tables.&lt;/p&gt;

&lt;p&gt;Team focus improves because engineers stop spending so much time on repetitive maintenance.&lt;/p&gt;

&lt;p&gt;These benefits compound.&lt;/p&gt;

&lt;p&gt;A lakehouse that is continuously maintained becomes easier to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes when managing Iceberg manually
&lt;/h2&gt;

&lt;p&gt;The first mistake is treating compaction as the whole problem. Compaction is important, but it does not replace snapshot expiration, manifest optimization, orphan cleanup, delete-file handling, or observability.&lt;/p&gt;

&lt;p&gt;The second mistake is applying the same policy to every table. A staging table and a production revenue table should not have the same retention and optimization strategy.&lt;/p&gt;

&lt;p&gt;The third mistake is running cleanup without a safety model. Orphan cleanup especially needs conservative thresholds and visibility.&lt;/p&gt;

&lt;p&gt;The fourth mistake is ignoring metadata. Data files get attention because they are visible, but manifests and snapshots often explain planning latency and storage drift.&lt;/p&gt;

&lt;p&gt;The fifth mistake is optimizing for one engine while the table is used by many engines.&lt;/p&gt;

&lt;p&gt;The sixth mistake is not measuring impact. If you cannot show what changed after maintenance, you cannot tune the lakehouse intelligently.&lt;/p&gt;

&lt;p&gt;The seventh mistake is waiting for incidents. Iceberg degradation is often gradual. By the time users complain, the table may have been unhealthy for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The target operating model
&lt;/h2&gt;

&lt;p&gt;A modern Iceberg lakehouse should operate like this:&lt;/p&gt;

&lt;p&gt;Tables are continuously monitored.&lt;/p&gt;

&lt;p&gt;Health is measured at the file, metadata, snapshot, storage, and query level.&lt;/p&gt;

&lt;p&gt;Policies define maintenance behavior.&lt;/p&gt;

&lt;p&gt;Compaction runs when it has measurable value.&lt;/p&gt;

&lt;p&gt;Snapshot expiration follows retention rules.&lt;/p&gt;

&lt;p&gt;Orphan cleanup is safe and auditable.&lt;/p&gt;

&lt;p&gt;Manifest optimization keeps planning fast.&lt;/p&gt;

&lt;p&gt;Engine behavior is visible.&lt;/p&gt;

&lt;p&gt;Query routing can account for cost and latency.&lt;/p&gt;

&lt;p&gt;Optimization history is recorded.&lt;/p&gt;

&lt;p&gt;Engineers can override, approve, or inspect operations.&lt;/p&gt;

&lt;p&gt;The system improves continuously.&lt;/p&gt;

&lt;p&gt;That is managed Iceberg.&lt;/p&gt;

&lt;p&gt;Not a hosted table format.&lt;/p&gt;

&lt;p&gt;Not a black box.&lt;/p&gt;

&lt;p&gt;Not a replacement for engineering judgment.&lt;/p&gt;

&lt;p&gt;A control plane that takes the repetitive, error-prone, high-volume operational work and turns it into policy-driven automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Iceberg is a strong foundation for the modern lakehouse, but it is not the whole platform.&lt;/p&gt;

&lt;p&gt;Once Iceberg becomes production infrastructure, the work shifts from “how do we create tables?” to “how do we keep hundreds or thousands of tables healthy while many engines and workloads use them?”&lt;/p&gt;

&lt;p&gt;That is where many teams feel the pain.&lt;/p&gt;

&lt;p&gt;Manual maintenance is fine at the beginning. Scripts are fine at the beginning. But as the lakehouse grows, entropy wins unless something is continuously managing the system.&lt;/p&gt;

&lt;p&gt;Managed Iceberg is the next layer.&lt;/p&gt;

&lt;p&gt;It means compaction, snapshots, manifests, orphan files, engines, policies, observability, and cost optimization are handled as one operating system for the lakehouse.&lt;/p&gt;

&lt;p&gt;LakeOps is built around that idea: autonomous lakehouse management for Apache Iceberg, running on top of the stack teams already use.&lt;/p&gt;

&lt;p&gt;For data platform engineers, the value is simple.&lt;/p&gt;

&lt;p&gt;You keep Iceberg open.&lt;/p&gt;

&lt;p&gt;You keep your storage.&lt;/p&gt;

&lt;p&gt;You keep your engines.&lt;/p&gt;

&lt;p&gt;You keep control.&lt;/p&gt;

&lt;p&gt;But you stop managing the lakehouse one table, one script, and one incident at a time. Thanks for reading! :)&lt;/p&gt;

</description>
      <category>data</category>
      <category>dataengineering</category>
      <category>devops</category>
      <category>backend</category>
    </item>
    <item>
      <title>Managed Iceberg Data Lakes: A Guide</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Thu, 07 May 2026 16:47:06 +0000</pubDate>
      <link>https://dev.to/jonisar/managed-iceberg-data-lakes-a-guide-3hk7</link>
      <guid>https://dev.to/jonisar/managed-iceberg-data-lakes-a-guide-3hk7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5k19wxxwup59bm9msm4y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5k19wxxwup59bm9msm4y.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Apache Iceberg has become the default table format for open data lakes. The 2025 State of the Apache Iceberg Ecosystem survey found 96.4% Spark adoption, 60.7% Trino, and growing DuckDB and Flink usage. Ryft's 2026 enterprise study reports that 58% of organizations now use Iceberg for business-critical analytics, and 79% plan to move their remaining data to it within 12 months.&lt;/p&gt;

&lt;p&gt;Adoption is no longer the question. The question is: who maintains all of this?&lt;/p&gt;

&lt;p&gt;Iceberg gives you snapshot isolation, schema evolution, hidden partitioning, and time travel. It does not give you someone to compact your files, expire your snapshots, clean up orphans, rewrite your manifests, or tell you which of your 800 tables is about to make your morning dashboards unusable. That is your job — and at scale, it is a job that breaks.&lt;/p&gt;

&lt;p&gt;This guide covers what it actually takes to run an Iceberg data lake in production: the maintenance operations, the failure modes, and how &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; — an autonomous control plane for Apache Iceberg — addresses each of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "managed" means in the Iceberg context
&lt;/h2&gt;

&lt;p&gt;The word "managed" gets overloaded. In the Iceberg world, it refers to the ongoing operational work required to keep tables healthy after data is written. Iceberg handles transactional correctness — atomic commits, snapshot isolation, optimistic concurrency control. But it intentionally leaves maintenance to the operator. The format's spec defines &lt;em&gt;what&lt;/em&gt; snapshots, manifests, and data files are. It does not define &lt;em&gt;when&lt;/em&gt; they should be cleaned up, &lt;em&gt;how&lt;/em&gt; files should be reorganized, or &lt;em&gt;which&lt;/em&gt; tables need attention first.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://lakeops.dev/solutions/managed-iceberg" rel="noopener noreferrer"&gt;managed Iceberg data lake&lt;/a&gt; is one where these operations are handled continuously, automatically, and with awareness of the broader system — not just individual tables.&lt;/p&gt;

&lt;p&gt;There are roughly six categories of maintenance work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compaction&lt;/strong&gt; — merging small files into optimally-sized ones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot lifecycle management&lt;/strong&gt; — expiring old snapshots and reclaiming storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifest maintenance&lt;/strong&gt; — rewriting manifests, consolidating position deletes, refreshing statistics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphan file cleanup&lt;/strong&gt; — removing data files that are no longer referenced by any snapshot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — knowing which tables are healthy, which are degrading, and why&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy and governance&lt;/strong&gt; — enforcing consistent maintenance rules across catalogs, namespaces, and teams&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these is straightforward in isolation. The difficulty is doing all of them, for every table, at the right frequency, in the right order, without breaking anything. Most teams start with custom Spark scripts scheduled in Airflow. That works at 10 tables. At 200 tables across multiple catalogs and engines, the scripts become the problem — brittle, uncoordinated, and blind to the interactions between operations.&lt;/p&gt;

&lt;p&gt;LakeOps treats these six categories as a single coordinated system. You connect your catalog and storage — typically in under 10 minutes, with no agents installed, no data movement, and no pipeline changes — and the platform continuously manages all six across your entire fleet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small file problem and why compaction matters
&lt;/h2&gt;

&lt;p&gt;Every Iceberg write produces new data files. Streaming pipelines that commit every few seconds can generate thousands of files per hour. Batch jobs with high partition cardinality scatter data across many small files. Even well-designed pipelines accumulate file fragmentation over time as tables evolve.&lt;/p&gt;

&lt;p&gt;The consequences compound:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query planning overhead&lt;/strong&gt;: the query engine must open and parse every manifest entry to build a scan plan. More files means more manifest entries means slower planning. A table with 100,000+ files can push planning time from milliseconds to tens of seconds before a single byte of data is read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I/O amplification&lt;/strong&gt;: each file requires a separate object storage GET request. Object stores are optimized for throughput on large sequential reads, not for opening thousands of small files. A query that should scan 10 GB across 20 files instead scans the same 10 GB across 2,000 files — same data volume, dramatically worse latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata bloat&lt;/strong&gt;: more files means larger manifests. Larger manifests means more data for the coordinator to load into memory. At extreme scale, this can cause OOM failures during query planning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The standard solution is compaction: periodically rewriting groups of small files into fewer, larger files targeting 256–512 MB each. Iceberg supports this natively through the &lt;code&gt;RewriteDataFiles&lt;/code&gt; action in Spark, or through engine-specific SQL commands. But &lt;em&gt;how&lt;/em&gt; you compact — the engine, the strategy, the awareness of actual query patterns — makes a dramatic difference.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/irRsF9VYP20"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  How LakeOps handles compaction
&lt;/h3&gt;

&lt;p&gt;LakeOps replaces Spark-based compaction with a purpose-built Rust engine built on Apache Arrow and DataFusion. The difference is architectural: no JVM, no garbage collection pauses, bounded memory, vectorized execution. In &lt;a href="https://lakeops.dev/blog/benchmarking-lakeops-compaction" rel="noopener noreferrer"&gt;production benchmarks on a 5.5 TB dataset across 10 tables&lt;/a&gt;, the engine achieved up to 99.8% file reduction and peak throughput of 2,522 MB/s — completing jobs that caused Spark OOM on identical hardware.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpi7tg18hlxbc4ui8qcia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpi7tg18hlxbc4ui8qcia.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But raw speed is only part of it. LakeOps compaction is &lt;strong&gt;query-aware&lt;/strong&gt;: it analyzes actual query patterns against each table to determine which file groups to prioritize and which sort orders will produce the best scan pruning. This means compaction cycles directly reduce I/O for the queries your team actually runs, rather than blindly reorganizing data no one reads.&lt;/p&gt;

&lt;p&gt;Two strategies are available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binpack&lt;/strong&gt; consolidates small files into optimally-sized files (~512 MB) without changing sort order. It is the default, and it resolves the small file problem with minimal compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort compaction&lt;/strong&gt; rewrites data in a column order derived from query filter and join patterns — enabling Iceberg's min/max metadata pruning to skip entire files. On TPC-H benchmarks, sorted layouts reduced bytes scanned by 51% with an additional ~9% compression improvement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fov9crn1o8d1o20ezpox1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fov9crn1o8d1o20ezpox1.png" alt=" " width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LakeOps also supports &lt;strong&gt;branch-based simulations&lt;/strong&gt;: you can test a layout change on an Iceberg branch, see the projected impact on scan volume and query latency, and promote only if the results justify it — without touching the production snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snapshot lifecycle management
&lt;/h2&gt;

&lt;p&gt;Every Iceberg commit creates a new snapshot. Snapshots enable time travel, but they are not free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each snapshot references a manifest list, which references manifests, which reference data files. Old snapshots keep references alive to data files that may have been logically deleted or replaced by compaction.&lt;/li&gt;
&lt;li&gt;A table with 120 days of hourly commits accumulates ~2,880 snapshots. Each snapshot's metadata must be tracked, and the data files it references cannot be garbage collected until the snapshot is expired.&lt;/li&gt;
&lt;li&gt;In production, unexpired snapshots are one of the largest sources of storage waste. Teams routinely discover that 30–50% of their object storage bill is data referenced only by old snapshots that should have been expired weeks ago.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzd62d1ll9iina0pcbwop.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzd62d1ll9iina0pcbwop.png" alt=" " width="800" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Snapshot expiration removes old snapshots and makes their exclusively-referenced data files eligible for deletion. The parameters are straightforward — retention window, minimum snapshot count, schedule — but the execution at fleet scale is where things break down. Different tables need different retention windows. Streaming tables commit far more frequently than batch tables. And expiration must happen &lt;em&gt;before&lt;/em&gt; orphan cleanup to avoid deleting files still referenced by unexpired snapshots.&lt;/p&gt;

&lt;p&gt;LakeOps automates snapshot lifecycle per table, respecting configurable retention policies at every scope level. You set the rules once — the platform handles scheduling, sequencing, and cleanup continuously. It also provides a snapshot explorer in the UI: browse snapshots, compare states, tag versions, and roll back — with full visibility into how much storage each retention window is costing you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manifest maintenance
&lt;/h2&gt;

&lt;p&gt;Manifests are Iceberg's indexing layer. Each manifest file tracks a subset of data files along with their partition values, column-level min/max statistics, and file sizes. Over time, they accumulate problems that silently degrade query performance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbr809svmc8ftul0an3v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbr809svmc8ftul0an3v.png" alt=" " width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manifest fragmentation&lt;/strong&gt;: as tables evolve through many small commits, manifests proliferate. A table with 500+ manifests forces the query planner to open and parse each one during scan planning. Rewriting manifests consolidates them — reducing the number of files the planner must read and cutting planning latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Position delete files&lt;/strong&gt;: Iceberg v2 supports row-level deletes via position delete files (merge-on-read). At query time, the engine reads both data files and their associated delete files, filtering out deleted rows. As delete files accumulate, read amplification grows. Rewriting position deletes physically merges them into the data files, converting merge-on-read to copy-on-write and eliminating per-query overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Puffin statistics&lt;/strong&gt;: Puffin files store column-level statistics (NDV, histograms, null counts) that enable cost-based query optimization. Stale Puffin stats lead to suboptimal join ordering and filter selectivity estimates. They need to be refreshed after significant data changes.&lt;/p&gt;

&lt;p&gt;These operations interact with each other and with compaction. Manifest rewrites should happen after compaction (since compaction changes the file layout). Position delete merges should be coordinated with compaction to avoid redundant rewrites. Puffin refresh should follow any significant layout change.&lt;/p&gt;

&lt;p&gt;LakeOps treats all of these as coordinated operations within a single &lt;a href="https://lakeops.dev/blog/autonomous-iceberg-table-maintenance" rel="noopener noreferrer"&gt;autonomous maintenance loop&lt;/a&gt;. The platform understands the dependency graph — expire snapshots first, then clean orphans, then compact, then rewrite manifests and refresh statistics — and executes the full sequence at the right cadence for each table based on its ingestion rate and query patterns. No manual scheduling, no sequencing bugs, no forgotten tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Orphan file cleanup
&lt;/h2&gt;

&lt;p&gt;Orphan files are data files that exist in storage but are not referenced by any current snapshot or metadata file. They accumulate from failed writes, completed compaction (old files remain until explicitly deleted), snapshot expiration (references removed, files left behind), and table drops or partition deletes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqcpnq69bdjuv6m1xfnrp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqcpnq69bdjuv6m1xfnrp.png" alt=" " width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Left unchecked, orphan files consume enormous storage. Production lakes regularly accumulate hundreds of terabytes of dead data — files that cost money every month but serve no purpose. One team discovered ~200 TB of orphan files across their lake, costing roughly $4,000/month in pure storage waste.&lt;/p&gt;

&lt;p&gt;Cleanup involves listing all files in the table's storage location, comparing against all file references in current metadata, and deleting files that are unreferenced and older than a configurable age threshold. The age threshold is critical — set it shorter than your longest-running write job and you risk deleting files that are part of an in-progress transaction.&lt;/p&gt;

&lt;p&gt;LakeOps runs orphan cleanup as part of its coordinated maintenance sequence, after snapshot expiration has properly released references. The age threshold is configurable per policy (default 7 days), and the platform ensures cleanup never runs ahead of expiration. For teams that have never cleaned orphans, the first run often reclaims a surprising amount of storage — one documented case saw a &lt;a href="https://lakeops.dev/blog/from-350tb-to-230tb-in-10-minutes" rel="noopener noreferrer"&gt;350 TB lake shrink to 230 TB in 10 minutes&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability: knowing what is broken before users notice
&lt;/h2&gt;

&lt;p&gt;The most dangerous state for a data lake is one where tables are degrading but no one knows. Queries get slower by 5% per week. Storage costs creep up. A table that used to plan in 200ms now takes 4 seconds. No single commit is the cause — it is the accumulation of hundreds of small writes without maintenance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fffewfuhxd85rnbt7h2fz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fffewfuhxd85rnbt7h2fz.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Effective Iceberg observability requires monitoring at multiple levels:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table-level health metrics&lt;/strong&gt;: file count, average file size, manifest count, snapshot count and age, position delete file count, orphan file volume. A table with 50,000 files averaging 2 MB is in trouble. A table with 500+ manifests has a planning bottleneck. These signals are available via Iceberg metadata tables (&lt;code&gt;$files&lt;/code&gt;, &lt;code&gt;$snapshots&lt;/code&gt;, &lt;code&gt;$manifests&lt;/code&gt;), but querying them across hundreds of tables and correlating the results into actionable classifications is a significant engineering effort on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxrmlt2pnjw21k03teng.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxrmlt2pnjw21k03teng.png" alt=" " width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query-level signals&lt;/strong&gt;: planning time vs. execution time (if planning exceeds 30% of total query time, metadata or file count is the bottleneck), data scanned vs. data returned (high ratios mean poor file layout), and engine-specific metrics like Trino split counts or Spark task counts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frk52pcxzpdyk0nsolqna.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frk52pcxzpdyk0nsolqna.png" alt=" " width="799" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fleet-level views&lt;/strong&gt;: you need to see across all tables in all catalogs to identify systemic patterns — which namespaces have the most critical tables, whether streaming tables degrade faster than batch tables, and where storage cost is concentrating.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyx1x9fh2k4r3j47zwq4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyx1x9fh2k4r3j47zwq4.png" alt=" " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LakeOps provides this observability out of the box. Every table gets a health classification (critical, warning, healthy) based on file count, file size distribution, manifest fragmentation, snapshot depth, and orphan accumulation. The &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;platform dashboard&lt;/a&gt; shows fleet-wide health, engine metrics, cost signals, and query performance — correlated across engines so you see the full picture rather than stitching together engine-specific monitoring. Insights surface specific issues (e.g., "12 tables have small-file hotspots," "3 tables have snapshot bloat") with severity and recommended action, so you know exactly where your lake needs attention and what to do about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policy and governance at fleet scale
&lt;/h2&gt;

&lt;p&gt;When you have 10 tables, you configure maintenance manually. When you have 800 tables across 5 catalogs, you need policies.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffv7guvy79eop8j1qez7k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffv7guvy79eop8j1qez7k.png" alt=" " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A policy defines what maintenance operations should run, on which scope, at what schedule, with what parameters. Policies should cascade — an organization-wide default applies everywhere, but a namespace-level policy can override it for streaming tables that need more aggressive compaction, and a table-level policy can override that for a specific high-priority table.&lt;/p&gt;

&lt;p&gt;Common policy dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compaction&lt;/strong&gt;: target file size, strategy (binpack/sort), schedule, concurrency limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot expiration&lt;/strong&gt;: retention period, minimum snapshots to retain, schedule&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifest rewrite&lt;/strong&gt;: schedule, trigger threshold (e.g., rewrite when manifest count exceeds 50)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphan cleanup&lt;/strong&gt;: age threshold, schedule, scope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table configuration standards&lt;/strong&gt;: enforce Iceberg v2, default Parquet compression, write distribution modes, commit retry settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LakeOps implements a hierarchical policy engine with four scopes: table → namespace → catalog → organization. More specific policies override broader ones. You define the rules once, and the platform enforces them continuously across your entire fleet. Every action is logged, auditable, and reversible — which matters when you are explaining to compliance why a table's data was reorganized at 2 AM.&lt;/p&gt;

&lt;p&gt;For reference, a reasonable manual scheduling baseline looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Schedule&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot expiration&lt;/td&gt;
&lt;td&gt;Hourly&lt;/td&gt;
&lt;td&gt;High-frequency commits need frequent cleanup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction&lt;/td&gt;
&lt;td&gt;Daily, 2 AM&lt;/td&gt;
&lt;td&gt;Off-peak, after expiration has freed references&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orphan cleanup&lt;/td&gt;
&lt;td&gt;Daily, 3 AM&lt;/td&gt;
&lt;td&gt;After expiration, before compaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest rewrite&lt;/td&gt;
&lt;td&gt;Daily, 4 AM&lt;/td&gt;
&lt;td&gt;After compaction produces new file layout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With LakeOps, you do not need to manage these schedules yourself — the platform's autonomous loop handles sequencing and adapts frequency to each table's ingestion rate — but the table above is useful for understanding the dependency order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-engine considerations
&lt;/h2&gt;

&lt;p&gt;One of Iceberg's core value propositions is engine independence. A single table can be read and written by Spark, queried by Trino, accessed by Snowflake's external tables, and analyzed by DuckDB — all through the same catalog.&lt;/p&gt;

&lt;p&gt;This creates two challenges: maintenance operations must be compatible with all engines accessing the table, and routing decisions affect both cost and performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catalog as the coordination point
&lt;/h3&gt;

&lt;p&gt;The Iceberg REST Catalog API has emerged as the standard for multi-engine access. LakeOps connects to your existing catalog — AWS Glue, Apache Polaris, Gravitino, Nessie, Lakekeeper, or REST-compatible catalogs — and operates through it. All maintenance commits go through the same catalog path your query engines use, ensuring atomic commits and proper concurrency handling. No separate catalog, no sidecar processes, no data movement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query routing
&lt;/h3&gt;

&lt;p&gt;With multiple engines available, the question becomes: which engine should run which query? A small lookup query costs $0.01 on DuckDB and $0.08 on Snowflake. A complex analytical join runs in 2 seconds on Trino and 45 seconds on DuckDB. Sending every query to the same engine wastes money or wastes time.&lt;/p&gt;

&lt;p&gt;LakeOps includes a multi-engine query routing layer that classifies queries and routes them to the optimal engine based on cost, latency, or throughput targets. Routing groups present a single SQL endpoint to downstream consumers (via PostgreSQL, MySQL, or Arrow Flight wire protocols) while the platform handles backend engine selection. Applications, BI tools, and agents connect to one endpoint — the routing layer optimizes which engine actually executes each query, and adapts as table health and engine load change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic AI and the data lake
&lt;/h2&gt;

&lt;p&gt;A newer challenge: AI agents that issue SQL queries autonomously. LLM-powered agents, feature store pipelines, and autonomous data workflows increasingly query Iceberg tables directly — often at unpredictable volumes, with unpredictable query shapes, and without the human judgment that would avoid scanning a 500 TB table with &lt;code&gt;SELECT *&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This changes the maintenance equation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency sensitivity increases&lt;/strong&gt;: agents expect sub-second responses. A table with a 4-second planning time because of small file accumulation is unusable for agent workflows, even if it is "fine" for batch analytics. Benchmarks show uncompacted tables impose a 5–10x latency penalty on agent queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query volume increases&lt;/strong&gt;: agents can issue 50 queries per interaction, multiplied across thousands of concurrent agents. Poorly laid-out tables amplify costs multiplicatively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety becomes critical&lt;/strong&gt;: an agent with write access to production tables can issue DDL, trigger full table scans on petabyte tables, or expose PII. Without guardrails, autonomous SQL access is a liability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LakeOps addresses this through a dedicated &lt;a href="https://lakeops.dev/solutions/agentic-ai-data" rel="noopener noreferrer"&gt;agentic AI enablement layer&lt;/a&gt;. Agents connect via Model Context Protocol (MCP) with structured tools for schema discovery, query execution, and statistics retrieval — no raw JDBC connections. A layered guardrail chain enforces read-only access, row limits, cost estimation (via EXPLAIN), PII masking, and optional human approval for high-risk operations. The routing layer classifies agent queries and sends them to the right engine automatically. And critically, the maintenance system monitors agent query patterns and adjusts sort orders, compaction priority, and file sizes accordingly — so the lake self-optimizes for the workloads actually hitting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common anti-patterns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Running compaction without expiring snapshots first.&lt;/strong&gt; Compaction creates new files and commits a new snapshot, but the old files remain referenced by old snapshots. If you compact aggressively without expiring, you &lt;em&gt;increase&lt;/em&gt; storage consumption — you now have both the old files and the new compacted files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting orphan cleanup age threshold too low.&lt;/strong&gt; If your longest-running write job takes 3 hours, and you clean orphans older than 1 hour, you will delete files that are part of an in-progress transaction. Set the threshold to at least 2x your longest write job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compacting everything on the same schedule.&lt;/strong&gt; Not all tables need the same compaction frequency. A streaming table committing every 10 seconds needs hourly compaction. A daily batch table needs weekly compaction. One-size-fits-all scheduling either wastes compute on cold tables or under-maintains hot tables. LakeOps adapts compaction frequency per table based on ingestion rate and health signals, eliminating this tradeoff entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring manifest count.&lt;/strong&gt; Teams focus on file count and miss manifest fragmentation. A table with 500 manifests and well-sized data files will still have slow query planning because the planner must parse every manifest. Manifest rewriting is cheap — do it regularly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sort compaction without query analysis.&lt;/strong&gt; Sorting by arbitrary columns wastes compute and can actually make queries slower if the sort order does not match dominant filter patterns. Always analyze query patterns before choosing sort columns — or let a system that observes actual query traffic make the decision for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No observability before automation.&lt;/strong&gt; Automating maintenance without visibility into table health is flying blind. You will not know if your automation is effective, if it is missing tables, or if it is making things worse. This is why LakeOps starts with observability — classifying every table's health before taking any action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;If you are standing up or improving a managed Iceberg data lake, here is a practical path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit your current state.&lt;/strong&gt; Query the &lt;code&gt;$files&lt;/code&gt;, &lt;code&gt;$snapshots&lt;/code&gt;, and &lt;code&gt;$manifests&lt;/code&gt; metadata tables for your most important tables. How many files? What is the average file size? How many snapshots? This gives you a baseline — and usually a few surprises.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connect LakeOps to your catalog.&lt;/strong&gt; The platform connects to your existing catalog and storage &lt;a href="https://lakeops.dev/solutions/managed-iceberg" rel="noopener noreferrer"&gt;in under 10 minutes&lt;/a&gt; — no agents, no data movement, no pipeline changes. Once connected, it automatically classifies every table as healthy, warning, or critical, giving you immediate visibility into your fleet's health.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Let autonomous maintenance run.&lt;/strong&gt; LakeOps handles snapshot expiration, orphan cleanup, compaction, and manifest maintenance in the correct sequence, at the right frequency for each table. Start in observation mode if you want to review actions before they execute, then switch to full autopilot once you are confident.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Define policies for your fleet.&lt;/strong&gt; Set organization-wide defaults for retention windows, compaction targets, and cleanup thresholds. Override at the namespace or table level where specific workloads need different treatment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable query routing.&lt;/strong&gt; If you run multiple engines, configure routing groups to optimize cost and latency across your engine mix. Present a single endpoint to consumers and let the routing layer handle engine selection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Evaluate sort compaction selectively.&lt;/strong&gt; For your most-queried tables, use LakeOps simulations to test sort strategies against actual query patterns. Promote layouts that show significant scan reduction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan for agents.&lt;/strong&gt; If AI agents will query your lake, enable the MCP interface and guardrail chain. Monitor agent workload patterns and let the platform optimize table layouts accordingly.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The work of managing an Iceberg data lake is not glamorous. It is compaction schedules, orphan cleanup thresholds, manifest fragmentation, and snapshot retention policies. It is the kind of work that, when done well, is invisible — queries are fast, storage is efficient, and engineers are not firefighting.&lt;/p&gt;

&lt;p&gt;When done poorly — or not done at all — it is the reason your dashboards are slow, your cloud bill is unexplainable, and your platform team spends their time writing one-off maintenance scripts instead of building the things that actually matter.&lt;/p&gt;

&lt;p&gt;Iceberg gives you a powerful foundation. &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; is what turns that foundation into a production-grade data platform — autonomously managed, fully observable, and ready for whatever workloads come next.&lt;/p&gt;

</description>
      <category>data</category>
      <category>database</category>
      <category>dataengineering</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Introducing QueryFlux: Open-Source Universal Multi-Engine Query Router and SQL Proxy</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Mon, 06 Apr 2026 09:07:13 +0000</pubDate>
      <link>https://dev.to/jonisar/introducing-queryflux-multi-engine-query-router-and-universal-sql-proxy-19e9</link>
      <guid>https://dev.to/jonisar/introducing-queryflux-multi-engine-query-router-and-universal-sql-proxy-19e9</guid>
      <description>&lt;p&gt;Efficiently routing multiple query engines is a critical challenge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://queryflux.dev/" rel="noopener noreferrer"&gt;QueryFlux&lt;/a&gt; is a universal SQL proxy and multi-engine query router written in Rust. It sits between clients and query engines. Clients connect to QueryFlux using a protocol they already know. QueryFlux routes each query to the right backend, translates SQL dialects when needed, enforces concurrency limits, and gives you a unified observability surface.&lt;/p&gt;

&lt;p&gt;Open table formats unified the data. QueryFlux unifies the access.&lt;/p&gt;

&lt;p&gt;If you already run more than one query engine, you know the problem is not only where data lives. The harder part is how query access works in practice.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which engine should this run on?&lt;/li&gt;
&lt;li&gt;Which client should connect where?&lt;/li&gt;
&lt;li&gt;How do you protect low-latency traffic from batch workloads?&lt;/li&gt;
&lt;li&gt;What happens when one cluster is saturated?&lt;/li&gt;
&lt;li&gt;How much routing logic ends up hardcoded across the stack?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the problem &lt;a href="https://queryflux.dev/" rel="noopener noreferrer"&gt;QueryFlux&lt;/a&gt; is built to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why QueryFlux exists
&lt;/h2&gt;

&lt;p&gt;Modern data platforms are multi-engine by design.&lt;/p&gt;

&lt;p&gt;A team may use Trino for federated queries, DuckDB for embedded analytics, StarRocks for low-latency serving, and Athena for pay-per-scan workloads on cold data. That mix is not a sign of architectural drift. In many cases, it is the right shape of the system.&lt;/p&gt;

&lt;p&gt;Open table formats made this possible. With Apache Iceberg, Delta Lake, or Hudi, multiple engines can read the same data in object storage without duplicating it. That solved storage interoperability.&lt;/p&gt;

&lt;p&gt;What it did not solve is compute access.&lt;/p&gt;

&lt;p&gt;Each engine still comes with its own protocol, its own SQL dialect, its own connection handling, and its own operational behavior. Clients still need to know where to connect. Routing logic still leaks into notebooks, applications, dashboards, and team conventions. Capacity management is still fragmented across backends.&lt;/p&gt;

&lt;p&gt;QueryFlux adds the missing layer above the table format: one access layer in front of the engine fleet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What QueryFlux does
&lt;/h2&gt;

&lt;p&gt;At a high level, QueryFlux handles three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;protocol ingestion&lt;/li&gt;
&lt;li&gt;routing&lt;/li&gt;
&lt;li&gt;dispatch and dialect translation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clients connect using protocols they already speak. QueryFlux supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trino HTTP&lt;/li&gt;
&lt;li&gt;PostgreSQL wire&lt;/li&gt;
&lt;li&gt;MySQL wire&lt;/li&gt;
&lt;li&gt;Arrow Flight SQL&lt;/li&gt;
&lt;li&gt;Admin REST API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the backend side, it already supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trino&lt;/li&gt;
&lt;li&gt;DuckDB&lt;/li&gt;
&lt;li&gt;StarRocks&lt;/li&gt;
&lt;li&gt;Athena&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives it a very specific place in the stack. It is not trying to replace engines, and it is not introducing a custom client model. It is making a heterogeneous engine fleet look coherent from the access layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a query flows through the system
&lt;/h2&gt;

&lt;p&gt;A client connects to QueryFlux using a native protocol.&lt;/p&gt;

&lt;p&gt;The query is evaluated against an ordered routing chain.&lt;/p&gt;

&lt;p&gt;The first matching rule selects the cluster group that should handle the query.&lt;/p&gt;

&lt;p&gt;From there, QueryFlux selects a healthy cluster in that group, optionally rewrites the SQL into the target dialect using sqlglot, and dispatches the query.&lt;/p&gt;

&lt;p&gt;If the group is already at its concurrency limit, the query can queue at the proxy instead of failing immediately.&lt;/p&gt;

&lt;p&gt;That is the important design move. QueryFlux is not just a forwarder. It is the runtime layer where access, routing, translation, and capacity handling meet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client (psql / Trino CLI / mysql / BI tool)
    │
    │ native protocol
    ▼
┌─────────────────────────────────────────────┐
│                 QueryFlux                   │
│                                             │
│  Frontend ──► Router ──► Dialect translation│
│                    │                        │
│              Cluster group                  │
│         (concurrency limit + queue)         │
└──────────────────┬──────────────────────────┘
                   │
      ┌────────────┼────────────┐
      ▼            ▼            ▼
   Trino       StarRocks      Athena
      └────────────┴────────────┘
          Apache Iceberg / Delta / Hudi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The architecture is simple enough to understand quickly, but deep enough to be useful in real environments.&lt;/p&gt;

&lt;p&gt;The simplicity is at the edge. Clients keep using the protocols they already know.&lt;/p&gt;

&lt;p&gt;The depth is inside the routing and dispatch path, where QueryFlux can apply routing policy, translation, concurrency limits, queueing, health-aware selection, and load balancing without pushing that complexity back into every client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing is where the value becomes obvious
&lt;/h2&gt;

&lt;p&gt;QueryFlux evaluates each query against an ordered router chain.&lt;/p&gt;

&lt;p&gt;Routing can be based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;protocol&lt;/li&gt;
&lt;li&gt;HTTP headers&lt;/li&gt;
&lt;li&gt;SQL text using regex&lt;/li&gt;
&lt;li&gt;client tags&lt;/li&gt;
&lt;li&gt;Python script logic&lt;/li&gt;
&lt;li&gt;compound rules&lt;/li&gt;
&lt;li&gt;fallback routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matters because real routing logic is rarely a single condition. In practice, you may want to steer PostgreSQL wire traffic to a low-latency group, send ETL-tagged traffic to a batch-oriented cluster, and use query patterns to catch common fast-path cases.&lt;/p&gt;

&lt;p&gt;A simple example looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fast_queries&lt;/span&gt;
    &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;query_regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;LIMIT&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;d+"&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;duckdb_group&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dashboard_queries&lt;/span&gt;
    &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;starrocks_group&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;heavy_analytics&lt;/span&gt;
    &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;query_regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JOIN|GROUP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;BY|WINDOW"&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trino_group&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fallback&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;athena_group&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is not that every deployment should use this exact policy. The point is that the policy becomes explicit, traceable, and shared.&lt;/p&gt;

&lt;p&gt;That alone removes a surprising amount of hidden operational drag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cluster groups make routing operational
&lt;/h2&gt;

&lt;p&gt;Once a route resolves to a cluster group, QueryFlux handles execution there.&lt;/p&gt;

&lt;p&gt;It supports these load-balancing strategies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;roundRobin&lt;/li&gt;
&lt;li&gt;leastLoaded&lt;/li&gt;
&lt;li&gt;failover&lt;/li&gt;
&lt;li&gt;engineAffinity&lt;/li&gt;
&lt;li&gt;weighted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;per-group concurrency limits&lt;/li&gt;
&lt;li&gt;proxy-side queueing when groups are full&lt;/li&gt;
&lt;li&gt;health-aware cluster selection&lt;/li&gt;
&lt;li&gt;background health checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where QueryFlux starts to feel deeper than a typical proxy.&lt;/p&gt;

&lt;p&gt;It is not only deciding where a query should go. It is also giving operators a place to control how traffic behaves when systems are under load, how overflow is absorbed, and how healthy capacity is chosen.&lt;/p&gt;

&lt;p&gt;That is the part that makes the system practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQL translation is built into the path
&lt;/h2&gt;

&lt;p&gt;Multi-engine routing is much more useful when SQL dialect differences do not immediately get in the way.&lt;/p&gt;

&lt;p&gt;QueryFlux integrates dialect-only translation through sqlglot. When needed, it can rewrite SQL into the target engine’s dialect during dispatch.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clients can keep speaking the SQL they naturally emit&lt;/li&gt;
&lt;li&gt;QueryFlux can normalize for the backend that will actually execute the query&lt;/li&gt;
&lt;li&gt;teams do not need to maintain multiple versions of the same query only because engines differ&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current design is disciplined here. What is implemented today is dialect-only translation. Schema-aware translation is explicitly on the roadmap.&lt;/p&gt;

&lt;p&gt;That is a good balance: the system is already useful now, and the path to deeper translation is clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability is part of the product, not an add-on
&lt;/h2&gt;

&lt;p&gt;A routing layer only works if operators can see what it is doing.&lt;/p&gt;

&lt;p&gt;QueryFlux includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prometheus metrics&lt;/li&gt;
&lt;li&gt;Grafana dashboard&lt;/li&gt;
&lt;li&gt;Admin REST API&lt;/li&gt;
&lt;li&gt;QueryFlux Studio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current observability surface covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;query counts&lt;/li&gt;
&lt;li&gt;query duration&lt;/li&gt;
&lt;li&gt;translation metrics&lt;/li&gt;
&lt;li&gt;running queries&lt;/li&gt;
&lt;li&gt;queued queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also supports routing traces, which matters in practice. When you introduce a routing layer, one of the first questions engineers ask is: why did this query land there? QueryFlux has a real answer to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this becomes useful quickly
&lt;/h2&gt;

&lt;p&gt;The value of QueryFlux is easier to see in real scenarios than in abstract feature lists.&lt;/p&gt;

&lt;h3&gt;
  
  
  A multi-engine platform
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;BI tools connect through one access layer&lt;/li&gt;
&lt;li&gt;different workloads are routed to the engines they fit best&lt;/li&gt;
&lt;li&gt;backend topology becomes configuration instead of client code&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dashboard SLA protection
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;low-latency groups can be protected with concurrency limits&lt;/li&gt;
&lt;li&gt;overflow can queue or spill instead of degrading the serving path&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Incremental engine migration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;weighted routing makes gradual traffic shifts possible&lt;/li&gt;
&lt;li&gt;clients do not need to change while the migration happens&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mixed workloads on shared data
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;batch, interactive, and exploratory traffic can be separated by policy&lt;/li&gt;
&lt;li&gt;routing intent lives in one place instead of being spread across the stack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are practical benefits. They show up immediately once a platform becomes multi-engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started is intentionally simple
&lt;/h2&gt;

&lt;p&gt;One of the nice things about the project is that the first run experience is straightforward.&lt;/p&gt;

&lt;p&gt;A minimal setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lakeops-org/queryflux.git
&lt;span class="nb"&gt;cd &lt;/span&gt;queryflux/examples/minimal-trino
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;QueryFlux on &lt;code&gt;http://localhost:8080&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Trino direct on &lt;code&gt;http://localhost:8081&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Admin API on &lt;code&gt;http://localhost:9000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Studio on &lt;code&gt;http://localhost:3000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Postgres on &lt;code&gt;localhost:5433&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can then send a simple query through the Trino HTTP frontend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/v1/statement &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Trino-User: dev"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"SELECT 42"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are also examples for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a minimal in-memory setup&lt;/li&gt;
&lt;li&gt;a Prometheus + Grafana stack&lt;/li&gt;
&lt;li&gt;a full stack with Trino, StarRocks, and Iceberg-related services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination is important. The system is conceptually ambitious, but the on-ramp is short.&lt;/p&gt;

&lt;p&gt;It feels like deep infrastructure without feeling heavy to try.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is already shipped
&lt;/h2&gt;

&lt;p&gt;QueryFlux already includes a substantial set of capabilities on the main branch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trino HTTP frontend&lt;/li&gt;
&lt;li&gt;PostgreSQL wire frontend&lt;/li&gt;
&lt;li&gt;MySQL wire frontend&lt;/li&gt;
&lt;li&gt;Arrow Flight SQL frontend&lt;/li&gt;
&lt;li&gt;Admin REST API&lt;/li&gt;
&lt;li&gt;Trino backend&lt;/li&gt;
&lt;li&gt;DuckDB backend&lt;/li&gt;
&lt;li&gt;StarRocks backend&lt;/li&gt;
&lt;li&gt;Athena backend&lt;/li&gt;
&lt;li&gt;ordered router chains and routing fallback&lt;/li&gt;
&lt;li&gt;route tracing support&lt;/li&gt;
&lt;li&gt;per-group concurrency limits&lt;/li&gt;
&lt;li&gt;proxy-side queueing&lt;/li&gt;
&lt;li&gt;multiple load-balancing strategies&lt;/li&gt;
&lt;li&gt;health-aware cluster selection&lt;/li&gt;
&lt;li&gt;dialect-only translation through sqlglot&lt;/li&gt;
&lt;li&gt;in-memory persistence&lt;/li&gt;
&lt;li&gt;PostgreSQL persistence&lt;/li&gt;
&lt;li&gt;authentication providers including none, static, OIDC, and LDAP&lt;/li&gt;
&lt;li&gt;authorization modes including allow-all, simple policy, and OpenFGA&lt;/li&gt;
&lt;li&gt;Prometheus metrics&lt;/li&gt;
&lt;li&gt;Grafana dashboard&lt;/li&gt;
&lt;li&gt;QueryFlux Studio&lt;/li&gt;
&lt;li&gt;dynamic config reload from Postgres&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because the project already feels like infrastructure, not just an idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;The roadmap extends the same core design.&lt;/p&gt;

&lt;p&gt;Near-term work includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;schema-aware SQL translation&lt;/li&gt;
&lt;li&gt;ClickHouse backend and HTTP frontend&lt;/li&gt;
&lt;li&gt;richer routing telemetry in Studio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Medium-term work includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cost- and performance-aware routing&lt;/li&gt;
&lt;li&gt;Snowflake backend&lt;/li&gt;
&lt;li&gt;BigQuery backend&lt;/li&gt;
&lt;li&gt;Redis persistence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That roadmap makes sense. It deepens the same access layer instead of changing the project’s center of gravity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solving the data ccess side
&lt;/h2&gt;

&lt;p&gt;The most interesting thing about QueryFlux is not that it is a proxy.&lt;/p&gt;

&lt;p&gt;It is that it is a carefully placed layer in a part of the modern data stack that is still surprisingly underbuilt.&lt;/p&gt;

&lt;p&gt;Open table formats solved the data side.&lt;/p&gt;

&lt;p&gt;QueryFlux is solving the access side.&lt;/p&gt;

&lt;p&gt;That creates an appealing combination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;conceptually clean architecture&lt;/li&gt;
&lt;li&gt;obvious operational benefits&lt;/li&gt;
&lt;li&gt;room for sophisticated policy and routing logic&lt;/li&gt;
&lt;li&gt;low-friction adoption path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It feels like the kind of infrastructure that becomes more valuable as the rest of the stack becomes more heterogeneous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Once a data platform becomes multi-engine, the missing piece is usually not another engine.&lt;br&gt;&lt;br&gt;
It is the access layer.&lt;br&gt;&lt;br&gt;
Clients still need to know where to connect. Routing still leaks into tools and applications. SQL dialect differences still show up at the edges. Capacity handling is still fragmented.&lt;br&gt;&lt;br&gt;
QueryFlux gives that layer a shape.&lt;br&gt;&lt;br&gt;
It makes multi-engine access easier to reason about, easier to operate, and easier to evolve.&lt;br&gt;&lt;br&gt;
That is why it is a compelling project: the idea is deep, the benefits are immediate, and the first experience is simple.&lt;br&gt;&lt;br&gt;
To try it out visit: &lt;a href="https://queryflux.dev/" rel="noopener noreferrer"&gt;https://queryflux.dev/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>dataengineering</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>11 Compaction Optimizations for Iceberg Data Lakes</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Mon, 16 Feb 2026 12:55:54 +0000</pubDate>
      <link>https://dev.to/jonisar/11-compaction-optimizations-for-iceberg-data-lakes-52h2</link>
      <guid>https://dev.to/jonisar/11-compaction-optimizations-for-iceberg-data-lakes-52h2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F742rq0qi27n0m42m1rka.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F742rq0qi27n0m42m1rka.png" alt="Iceberg Coontrol Plan provides automated optimzied compaction" width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Compaction should provide an easy solution to a very difficult problem: controlling file count, minimizing the cost of delete operations over read costs, and keeping metadata growth from turning every query plan into a deep, time-consuming walk through snapshots and manifests.&lt;/p&gt;

&lt;p&gt;The data layer has compaction as its mechanism for solving these issues, but only if compaction is run using some defined set of rules governing the scope of compactions, the thresholds above which compactions are run, and the synchronization of compaction runs with the timing of snapshot expirations and the maintenance of manifests.&lt;/p&gt;

&lt;p&gt;Compaction can be run manually via scripts and schedules, or automatically by a control plane.&lt;/p&gt;

&lt;p&gt;When manual scripts are used to run compaction, they can effectively manage compaction for a small number of tables and one engine. However, as soon as there are many tables, and/or multiple engines, the manual process becomes guesswork; scripts may rewrite too much, may run too infrequently, may interfere with ongoing ingestions, and will cause churn in both the snapshots and manifests.&lt;/p&gt;

&lt;p&gt;A control plane flips this model completely around.&lt;/p&gt;

&lt;p&gt;Instead of rewriting everything all the time, a control plane continuously monitors the health and workload characteristics of tables. Then, only when necessary, a control plane spends rewrite budget on the parts of the tables that actually change performance or cost, while also managing the entire lifecycle of maintaining the table.&lt;/p&gt;

&lt;p&gt;This article will teach you how to run compaction like the production lakes do it: how to choose your base line strategy (bin-packing vs sorting) for compaction, how to prevent rewrites of healthy partitioning, how to limit the scope of each compaction so maintenance remains invisible, how to focus on hot and delete-heavy areas first, how to prevent the continuous commit cadence of streaming data from creating a factory of snapshot partitions, and how to synchronize compaction with the metadata cleanup that is needed to maintain stable query planning.&lt;/p&gt;

&lt;p&gt;For further reading on other aspects of optimizing and maintaining Iceberg, please also refer to:&lt;/p&gt;

&lt;p&gt;For further reading on other aspects of optimizing and maintaining Iceberg, please also refer to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://overcast.blog/7-best-compaction-engines-for-apache-iceberg/" rel="noopener noreferrer"&gt;7 Best Compaction Engines for Apache Iceberg&lt;/a&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://overcast.blog/11-iceberg-performance-optimizations-you-should-know/" rel="noopener noreferrer"&gt;11 Iceberg Performance Optimizations You Should Know&lt;/a&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://overcast.blog/9-apache-iceberg-table-maintenance-tools-you-should-know/" rel="noopener noreferrer"&gt;9 Apache Iceberg Table Maintenance Tools You Should Know&lt;/a&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's move on to the compaction strategies that actually work in real production lakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Add a Control Plane for 20x Faster Compaction and Optimized Table Maintenance
&lt;/h2&gt;

&lt;p&gt;If you add a control plane to your lake, LakeOps comes with the most powerful and intelligent compaction engine that exists today, and will also manage and optimize table maintenance and lake operations for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwo5b6j2npuehmuta4r3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwo5b6j2npuehmuta4r3.png" alt=" " width="799" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Snowflake-like experience for Iceberg with 10x performance (source: lakeops.dev)&lt;/p&gt;

&lt;p&gt;Instead of being confined to fixed schedules, LakeOps operates as a control plane for Iceberg tables and can know when and how to compact what. It treats compaction as a continuous operational problem rather than a periodic batch job, and optimizes it in real time.&lt;/p&gt;

&lt;p&gt;It analyzes telemetry data from query engines and Iceberg catalogs and uses that data to decide when compaction is actually needed, what to compact, and how. It takes actual usage patterns into account as well.&lt;/p&gt;

&lt;p&gt;Under the hood, LakeOps uses a dedicated Rust-based compaction engine that is designed specifically for Iceberg layouts and metadata behavior. Compaction is coordinated with snapshot expiration, manifest rewrites, orphan cleanup, and statistics maintenance so these operations reinforce each other instead of fighting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcmtgryzwbhhygpmnmn9s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcmtgryzwbhhygpmnmn9s.png" alt=" " width="800" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The results are ~20x faster compaction, ~15x faster queries, and ~80% CPU/Storage cost saving.&lt;/p&gt;

&lt;p&gt;🚢 Apache Iceberg compaction is not “background maintenance.” It’s a time-critical optimization problem that directly impacts query latency, metadata growth, and infrastructure cost.&lt;/p&gt;

&lt;p&gt;Learn more about it here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.linkedin.com/posts/amit-gilad_apache-iceberg-compaction-time-critical-optimization-activity/" rel="noopener noreferrer"&gt;Apache Iceberg Compaction: Time-Critical Optimization | Amit Gilad&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addition to compaction LakeOps gives you control with manual and autopilot modes for all maintenance operations in youe tables and coordinates them with compaction. That includes expiring snapshots, manifest rewrites, orphan file cleanups and more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0rrirvr0hsr1al47v0r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0rrirvr0hsr1al47v0r.png" alt=" " width="799" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Real-Time comoaction and maintenance optimization with a control plane (source: lakeops.dev/)&lt;/p&gt;

&lt;p&gt;You can choose between manual mode and auto-pilot per table or for groups of tables to control compaction and maintenance proccesses.&lt;/p&gt;

&lt;p&gt;LakeOps also lets you define policies across the lake to enforce your standards, and provides you with dashboards to see and manage all compaction and maintenance processes per table and for the entire lake.&lt;/p&gt;

&lt;p&gt;Learn more: &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;https://lakeops.dev&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Use bin-pack as the baseline correction
&lt;/h2&gt;

&lt;p&gt;Most iceberg tables do not require complex layout schemes. However, they all suffer from file fragmentation.&lt;/p&gt;

&lt;p&gt;Before attempting to fix the problem using sort-based layouts, clustering, or partitioning, take a closer look at the most obvious source of file fragmentation: the writing process itself. In almost all cases, the initial performance decline caused by file fragmentation is due to the streaming ingestion of very small batches; micro-batch commits occur very frequently; and backfill data will always come in very unevenly-sized chunks.&lt;/p&gt;

&lt;p&gt;As a result of the write process, many small Parquet files exist within each partition. None of these files are "broken," and queries will still continue to provide accurate answers. However, as more and more small Parquet files exist within each partition, planning time will increase, the overhead associated with task scheduling will grow, and the number of object store calls will grow.&lt;/p&gt;

&lt;p&gt;This is not a layout issue; it is a file count issue.&lt;/p&gt;

&lt;p&gt;The easiest and most reliable method to solve the file count issue is to use bin-pack compaction. Bin-pack compaction combines small data files into a smaller number of larger, properly sized files. Bin-pack compaction does not alter the existing file sort order, nor does it re-cluster data; it merely normalizes the file size and decreases the metadata overhead associated with having a high number of files.&lt;/p&gt;

&lt;p&gt;In practice, this is usually sufficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Approach to Compaction Really Does Improve Performance
&lt;/h3&gt;

&lt;p&gt;Iceberg engines operate at the file level. The more files you add to an engine, the more the engine must plan:&lt;/p&gt;

&lt;p&gt;More manifest entries must be read&lt;br&gt;&lt;br&gt;
More file footers must be inspected&lt;br&gt;&lt;br&gt;
More scan tasks must be scheduled&lt;br&gt;&lt;br&gt;
More file references to delete must be tracked&lt;/p&gt;

&lt;p&gt;As the number of files grows exponentially, so too does the planning time. Bin-pack compaction eliminates the number of files physically on disk, while maintaining the existing logical layout. Therefore, there are fewer planning reads, and fewer tasks to schedule without requiring additional shuffling.&lt;/p&gt;

&lt;p&gt;A good rule of thumb for most production tables is to target file sizes ranging from 128 MB to 512 MB. The specific size range will depend on the engine, and the workload. What is important is consistency.&lt;/p&gt;
&lt;h3&gt;
  
  
  Start with the Default Rewriting Method
&lt;/h3&gt;

&lt;p&gt;Unless you specify otherwise, Iceberg uses the bin-pack rewriting method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the effectiveness of the rewrite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_size_in_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_size_gb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want to see fewer files and the same total size. If the total size is substantially changed, something other than file fragmentation is occurring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Specify Your Target File Size
&lt;/h3&gt;

&lt;p&gt;If file fragmentation continues, specify a target file size at the table level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="s1"&gt;'write.target-file-size-bytes'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'536870912'&lt;/span&gt; &lt;span class="c1"&gt;-- 512MB&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then perform the rewrite with the same target file size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
    &lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'536870912'&lt;/span&gt; 
  &lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without specifying a target file size, the engine and writer will create files of varying sizes, which will require subsequent compactions to restore the target file size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define Thresholds to Prevent Unnecessary Rewrites
&lt;/h3&gt;

&lt;p&gt;At scale, performing rewrites on healthy data wastes compute resources. Define the following thresholds to prevent unnecessary rewrites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
    &lt;span class="s1"&gt;'min-input-files'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s1"&gt;'min-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'134217728'&lt;/span&gt; &lt;span class="c1"&gt;-- 128MB &lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will ensure that only those partitions with a minimum of five input files are rewritten. Partitions with one or two files of reasonable sizes are ignored.&lt;/p&gt;

&lt;p&gt;These thresholds can be made tighter by defining multiple conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
    &lt;span class="s1"&gt;'min-input-files'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s1"&gt;'min-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'134217728'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- 128MB &lt;/span&gt;
    &lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'536870912'&lt;/span&gt; &lt;span class="c1"&gt;-- 512MB &lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Therefore, compaction will now only run under the following conditions:&lt;/p&gt;

&lt;p&gt;There are enough small files to warrant consolidation&lt;br&gt;&lt;br&gt;
Files are currently below a reasonable size threshold&lt;br&gt;&lt;br&gt;
There is a clear target to normalize to&lt;/p&gt;

&lt;p&gt;This transforms compaction from an automated rewrite operation to a targeted repair operation.&lt;/p&gt;

&lt;p&gt;Prior to executing a compaction operation, review the distribution of files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_size_in_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_mb&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt; 
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt; 
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; 
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a partition contains four files averaging 480 MB and your target file size is 512 MB, rewriting the partition will not significantly affect either planning time or scan time.&lt;/p&gt;

&lt;p&gt;However, if another partition contains 180 files averaging 25 MB, that partition clearly requires compaction.&lt;/p&gt;

&lt;p&gt;Decisions regarding compaction operations should be based on this type of signal. Not a schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Conditional Compaction
&lt;/h2&gt;

&lt;p&gt;Another way to waste compute cycles in an Iceberg lake is to blindly compact data on a regular basis.&lt;/p&gt;

&lt;p&gt;It usually begins innocently. A periodic rewrite job is created to "keep things tidy." For a period of time, it appears to help. Eventually, however, it begins to rewrite partitions that were previously healthy. Each rewrite generates new files, new snapshots, and updates the manifest list. Although none of the files are "broken," the system is spending compute cycles on processing data that does not improve performance.&lt;/p&gt;

&lt;p&gt;Compaction should be used as a corrective measure, not as a routine activity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Unconditional Rewrites Cause Churn
&lt;/h3&gt;

&lt;p&gt;Each time you rewrite data files, Iceberg:&lt;/p&gt;

&lt;p&gt;Creates new data files&lt;br&gt;&lt;br&gt;
Generates a new snapshot&lt;br&gt;&lt;br&gt;
Updates manifest lists&lt;br&gt;&lt;br&gt;
Increases metadata history&lt;/p&gt;

&lt;p&gt;If the files being rewritten are already close to their target size, you are essentially cycling the data through the system. The added churn causes increased metadata depth and longer planning times over time.&lt;/p&gt;

&lt;p&gt;At scale, this overhead becomes noticeable.&lt;/p&gt;

&lt;p&gt;Your objective is not to compact frequently. It is to compact when the layout is measurably unhealthy.&lt;/p&gt;
&lt;h3&gt;
  
  
  Implement Gateways to Control Rewrites
&lt;/h3&gt;

&lt;p&gt;Iceberg provides a rewrite_data_files procedure that allows you to implement gateway conditions. The most effective condition is min_input_files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
    &lt;span class="s1"&gt;'min-input-files'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'3'&lt;/span&gt; 
  &lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using this condition, Iceberg will only rewrite file groups that contain at least three files. Partitions that already have one or two files that are of reasonable size are excluded.&lt;/p&gt;

&lt;p&gt;This is a relatively small change to make, but in large lakes, it will significantly reduce unnecessary compaction.&lt;/p&gt;

&lt;p&gt;You can implement additional conditions to make this gateway even tighter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
    &lt;span class="s1"&gt;'min-input-files'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s1"&gt;'min-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'134217728'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- 128MB &lt;/span&gt;
    &lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'536870912'&lt;/span&gt; &lt;span class="c1"&gt;-- 512MB &lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compaction will now only run when the following conditions are met:&lt;/p&gt;

&lt;p&gt;There are sufficient small files to warrant consolidation&lt;br&gt;&lt;br&gt;
Files are currently below a reasonable size threshold&lt;br&gt;&lt;br&gt;
There is a valid target to normalize to&lt;/p&gt;

&lt;p&gt;This converts compaction from an automatic rewrite into a targeted repair operation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Let the Table State Drive the Decision
&lt;/h3&gt;

&lt;p&gt;Prior to initiating a compaction operation, review the distribution of files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_size_in_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_mb&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt; 
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt; 
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; 
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a partition has four files averaging 480 MB and your target file size is 512 MB, rewriting the partition will not materialy affect either planning or scanning time.&lt;/p&gt;

&lt;p&gt;However, if another partition has 180 files averaging 25 MB, that partition is a prime candidate for compaction.&lt;/p&gt;

&lt;p&gt;Compaction decisions should be based on signals such as this. Not a schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Limit Rewrite Scope Per Run
&lt;/h2&gt;

&lt;p&gt;Big compaction jobs appear great on paper. In reality, they are among the easiest ways to create instability in a production lake.&lt;/p&gt;

&lt;p&gt;Backfills, partition evolutions, or long stretches of time without maintenance can quickly turn terabytes of data into rewrite candidates. If you don't specify any boundaries, Iceberg will rewrite everything that meets its criteria. The outcome is well understood: long-running jobs, significant shuffles, large amounts of object store I/O, significant increases in snapshot sizes, and sometimes even cluster contention with users' queries.&lt;/p&gt;

&lt;p&gt;Compaction operates at its best when it is incremental.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why "Rewrite Everything" Is A Risk
&lt;/h3&gt;

&lt;p&gt;When you rewrite a large section of a table in a single pass, you are executing a number of costly operations simultaneously:&lt;/p&gt;

&lt;p&gt;Reading numerous data files&lt;br&gt;&lt;br&gt;
Shuffle and rewrite them&lt;br&gt;&lt;br&gt;
Create a lot of new files&lt;br&gt;&lt;br&gt;
Create a new large snapshot&lt;br&gt;&lt;br&gt;
Possibly rewrite manifests&lt;/p&gt;

&lt;p&gt;Regardless of whether it is successful, you have produced a major maintenance event. Even if it fails in the middle of its execution, you will lose some resources and extend the length of your maintenance period.&lt;/p&gt;

&lt;p&gt;Operationally, smaller, and more frequent corrections are safer than infrequent large-scale rewrites.&lt;/p&gt;
&lt;h3&gt;
  
  
  Restrict rewrite size explicitly
&lt;/h3&gt;

&lt;p&gt;Rewrite_data_files is a method that allows Iceberg to provide parameters that can help limit the amount of effort that is put into a single run of rewriting data.&lt;/p&gt;

&lt;p&gt;To illustrate, you may desire to restrict the maximum number of file group rewrites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'max-file-group-rewrites'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'20'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This restricts the number of rewrite groups executed in a single pass of rewrite_data_files. Instead of rewriting hundreds of partitions in one pass, you will execute a controlled sequence of batch passes.&lt;/p&gt;

&lt;p&gt;You can also utilize these with eligibility thresholds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'min-input-files'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'max-file-group-rewrites'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'20'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'536870912'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compaction now becomes predictable. Each pass of compaction corrects a finite amount of drift.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disperse correction over cycles
&lt;/h3&gt;

&lt;p&gt;There is rarely a good reason to try to "Fix everything tonight" if a table contains months of small files due to a large number of small writes.&lt;/p&gt;

&lt;p&gt;Instead, a more sustainable and steady state approach would be:&lt;/p&gt;

&lt;p&gt;Compact data with limited rewrite scope.&lt;br&gt;&lt;br&gt;
Permit normal operation of user workloads.&lt;br&gt;&lt;br&gt;
Repeat on the subsequent maintenance cycle.&lt;/p&gt;

&lt;p&gt;Within a couple of cycles, fragmentation will decrease significantly, without generating a maintenance peak.&lt;/p&gt;

&lt;p&gt;This also produces less "snapshot shock." Instead of a large, single-pass rewrite snapshot replacing nearly half of the table, you generate a series of smaller, incremental snapshots.&lt;/p&gt;
&lt;h3&gt;
  
  
  Prioritize rather than rewriting randomly
&lt;/h3&gt;

&lt;p&gt;Limiting rewrite scope, prioritizing becomes essential.&lt;/p&gt;

&lt;p&gt;Practically speaking, you want to rewrite:&lt;/p&gt;

&lt;p&gt;Partition groups with the greatest number of files.&lt;br&gt;&lt;br&gt;
Partition groups with the least average file size.&lt;br&gt;&lt;br&gt;
Partition groups with the largest numbers of deletions.&lt;br&gt;&lt;br&gt;
Partition groups with the most user queries.&lt;/p&gt;

&lt;p&gt;You can find the worst offending partition groups using the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_size_in_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_mb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then either target the offending partition groups directly using a WHERE statement or permit your orchestration layer to determine the highest impact groups to rewrite first.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'event_date &amp;gt;= DATE &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;2026-01-01&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'max-file-group-rewrites'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'10'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This limits both the logical scope (only recent partitions) and the physical rewrite volume.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maintain Maintenance Invisible To Users
&lt;/h3&gt;

&lt;p&gt;The ultimate objective of limiting rewrite scope is not merely cluster stability. It is predictability.&lt;/p&gt;

&lt;p&gt;When compaction is done in a manner such that each run is relatively small and bounded:&lt;/p&gt;

&lt;p&gt;Maintenance windows are brief.&lt;br&gt;&lt;br&gt;
Resource spikes are under control.&lt;br&gt;&lt;br&gt;
Snapshots grow gradually.&lt;br&gt;&lt;br&gt;
Query performance improves incrementally, rather than suddenly.&lt;/p&gt;

&lt;p&gt;In production lakes, stability is usually more important than the rate of correction. Incremental correction is generally preferred to dramatic restructuring.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Focus On Hot Partition Groups
&lt;/h2&gt;

&lt;p&gt;In the majority of Iceberg tables, compaction impact is not uniformly distributed.&lt;/p&gt;

&lt;p&gt;A small segment of partitions is responsible for most of the pain: They receive the most writes (thus they tend to fragment the quickest) and they receive the most reads (thus every additional file appears as both planning + scan overhead). If you rewrite the partitions that are "hot", you will typically gain 80% of the benefits with only a fraction of the rewrite volume.&lt;/p&gt;

&lt;p&gt;The simplest approach to achieve this is to treat compaction as a rolling window problem.&lt;/p&gt;
&lt;h3&gt;
  
  
  "Hot" Generally Means Two Things
&lt;/h3&gt;

&lt;p&gt;Hot partitions generally represent partitions that are still active:&lt;/p&gt;

&lt;p&gt;They are still receiving new files from streaming or micro-batch systems&lt;br&gt;&lt;br&gt;
They are the partitions that your analysts / dashboards / downstream jobs are accessing constantly.&lt;/p&gt;

&lt;p&gt;This results in two operational principles:&lt;/p&gt;

&lt;p&gt;Compact relatively recent partitions regularly, since they will accumulate the most small files.&lt;br&gt;&lt;br&gt;
Do not compact the actively written partitions unless you know you can tolerate collisions with writers.&lt;/p&gt;

&lt;p&gt;AWS describes this for Iceberg compaction: Use a where predicate to exclude actively written partitions, so that you do not encounter data conflicts with writers and leave only metadata conflicts that Iceberg can normally resolve.&lt;/p&gt;
&lt;h3&gt;
  
  
  Identify Your Rolling Window With Where
&lt;/h3&gt;

&lt;p&gt;Iceberg's Spark procedure provides a where predicate for filtering which files (and hence which partitions) qualify for rewriting.&lt;/p&gt;

&lt;p&gt;An extremely common use case is "Compact everything older than the current ingest window":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'event_date &amp;lt; DATE &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;2026-02-10&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'536870912'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'min-input-files'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'5'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This maintains compaction away from the partition(s) that are currently being mutated, while continually cleaning up yesterday's and previous data.&lt;/p&gt;

&lt;p&gt;If your table is partitioned hourly, perform the same concept at the hour granularity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'event_hour &amp;lt; TIMESTAMP &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;2026-02-11 12:00:00&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'536870912'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'min-input-files'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'5'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main principle here is not the specific cut-off. It is maintaining a buffer so that compaction does not conflict with ingestion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Locate the Worst Partition Groups First
&lt;/h3&gt;

&lt;p&gt;Even within the "hot-ish" window, not all partition groups are equally bad. You can typically find the worst offender simply by examining the file count and average size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_size_in_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_mb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;file_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you plan to compact only a limited portion of the data per pass (as you probably should), this query will indicate where you will obtain the largest initial benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  If You Must Compact Partition Groups That Are Still Receiving Late Data
&lt;/h3&gt;

&lt;p&gt;There are certain types of workloads that receive late-arriving events, updates, or merges that keep older partition groups "active." If you compact them regardless, you may periodically collide with writers.&lt;/p&gt;

&lt;p&gt;Iceberg includes a partial progress mode that commits compaction in smaller portions, rather than committing a large block, which reduces the collision risk associated with retries when conflicts occur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'event_date &amp;gt;= DATE &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;2026-02-01&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; AND event_date &amp;lt; DATE &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;2026-02-10&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'partial-progress.enabled'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'partial-progress.max-commits'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'10'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'max-concurrent-file-group-rewrites'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'10'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are making a tradeoff of "one clean commit" versus "multiple smaller commits that fail at lower expense." In actual production lakes with continuous write activity, that trade is typically worthwhile.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where a Control Plane Helps
&lt;/h3&gt;

&lt;p&gt;Once you have numerous tables, "hot partition groups" ceases to be something you determine through experience. You require a loop that continuously determines the "hot partition groups" based on the actual read/write usage of your system and then applies the rolling window concept to those partition groups.&lt;/p&gt;

&lt;p&gt;That is the point at which a control plane such as LakeOps becomes useful: It is not adding a new compaction algorithm as much as it is determining where to expend your rewrite budget based on real workload telemetry and applying that determination in a consistent manner across hundreds of tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Sort or Z-Order When Scan Efficiency Is The Bottleneck
&lt;/h2&gt;

&lt;p&gt;Bin-packing compaction decreases the number of files. However, it does not affect how the data is organized internally within those files.&lt;/p&gt;

&lt;p&gt;If partitions are appropriately sized and file counts are reasonable, but selectivity-based queries are scanning a disproportionately larger number of data files than anticipated, the cause is likely clustering. You will commonly observe the following pattern: Planning times are consistent, partition pruning is functioning, but filtered queries are reading a large percent of files within a given partition. This is the point where sorting-based compaction becomes relevant.&lt;/p&gt;

&lt;p&gt;Data engines depend on file-based statistical information (such as min and max values) to determine whether a file can be excluded from processing. When data is written in a random fashion, the value ranges between files overlap greatly. Therefore, even selective predicates typically cannot exclude a large number of files. Sorting changes this. When data is ordered by a frequently used filter column, each file will generally contain a narrower value range than previously existed, thus allowing more files to be skipped based upon the predicate and reducing the number of bytes to be scanned.&lt;/p&gt;

&lt;p&gt;If there is one column that dominates your predicates (e.g., event_time within a date-partitioned table), a simple sort-based rewrite is typically sufficient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sort'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sort_order&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'event_time ASC NULLS LAST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'536870912'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'min-input-files'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'5'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rearranges the rows of data within files so that time-based predicates can remove more files early in the process. The effect is evident not only in terms of runtime, but also in the reduction in scanned bytes and the number of splits generated.&lt;/p&gt;

&lt;p&gt;If your workload filters on multiple columns (e.g., user_id, event_type, and occasionally device_type), Z-order is typically a superior choice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sort'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sort_order&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'zorder(user_id, event_type)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'536870912'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'min-input-files'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'5'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Z-ordering enhances locality across multiple dimensions. While Z-ordering will never perfectly optimize any individual column, it typically minimizes overall scan expansion when filter patterns vary.&lt;/p&gt;

&lt;p&gt;Important Note: Defining a sort order at the table level does not rewrite historical data. It only affects newly-written data. All existing files will remain unmodified until a rewrite occurs.&lt;/p&gt;

&lt;p&gt;It is typical to define a sort order and then expect improvements, only to discover that no changes occurred to the physical layout of the data.&lt;/p&gt;

&lt;p&gt;After performing a sort-based compaction, verify it correctly. Examine the number of files that are scanned for common predicates. Compare the total bytes scanned before and after. Runtime can be difficult to quantify in shared environments; however, file count and total bytes scanned are more reliable metrics.&lt;/p&gt;

&lt;p&gt;Sorting is more resource-intensive than bin-packing. Sorting involves additional shuffle and CPU overhead during compaction. If you were to blindly apply sorting to all partitions, the costs of maintenance could potentially exceed the query performance improvements. In general, sorting works best when applied selectively: Target high-traffic partitions; Align the sort with real filter patterns; Apply rewrites incrementally.&lt;/p&gt;

&lt;p&gt;When scan efficiency is the primary bottleneck rather than file count, sorting or Z-order is one of the few techniques that will reliably enhance pruning. The key is to apply sorting or Z-order in a manner that aligns with the characteristics of your workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Compact delete-heavy partitions deliberately
&lt;/h2&gt;

&lt;p&gt;You can view a table, note that file sizes are healthy, and believe that compaction is being handled - yet the queries are slowing down.&lt;/p&gt;

&lt;p&gt;A common cause is delete files.&lt;/p&gt;

&lt;p&gt;Iceberg does not immediately write out data files when rows are modified or deleted. Rather, it will store the row position deletes or equality deletes with the data. At read time, the engine will combine the data files with their associated delete files. Although this provides an efficient mechanism for writing, as delete files become numerous, every query will incur additional cost.&lt;/p&gt;

&lt;p&gt;The affect is subtle. File sizes appear fine. Bin-pack has already normalized fragmentation. However, scan CPU increases, and partitions that are update-heavy begin to perform poorer than append-only partitions.&lt;/p&gt;

&lt;p&gt;You may usually verify this by examining the delete file distribution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;delete_file_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete_files&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;delete_file_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If certain partitions contain a high concentration of delete files, that is an indication that reads are performing more work than necessary.&lt;/p&gt;

&lt;p&gt;Iceberg supports delete aware compaction. Instead of rewriting files solely based upon their size, you may specify a threshold for the ratio of deleted rows and have Iceberg rewrite data files that meet this criteria. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_data_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'delete-ratio-threshold'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'0.3'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'remove-dangling-deletes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'target-file-size-bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'536870912'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, Iceberg will rewrite data files that are severely impacted by deletes and will remove the physical representation of deleted rows as well as the associated delete files.&lt;/p&gt;

&lt;p&gt;The practical result is that queries will no longer require the merging of as many delete files at runtime; CPU decreases; scan cost stabilizes; and planning becomes easier since there are fewer auxiliary files to track.&lt;/p&gt;

&lt;p&gt;This has a significant impact primarily on tables that have been subjected to upserts, CDC pipelines, and/or frequent merges. Event tables that are used exclusively for appending data do not typically exhibit this behavior. Similarly, dimension tables and slowly changing datasets do.&lt;/p&gt;

&lt;p&gt;Just like with any other compaction strategy, maintain focus. Use the combination of delete thresholds, partition filters, and rewrite limits to optimize the compaction strategy. It is unnecessary to rewrite the entire table simply because a handful of partitions contain a high amount of delete activity.&lt;/p&gt;

&lt;p&gt;Healthy file size does not ensure healthy performance. If delete files comprise the majority of a partition, the compaction strategy must specifically target these delete files - otherwise, read cost will continue to increase regardless of whether the layout appears to be "correct".&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Rewrite position delete files individually when needed
&lt;/h2&gt;

&lt;p&gt;Rewriting data files does not necessarily resolve all delete related issues.&lt;/p&gt;

&lt;p&gt;In many update-intensive workloads, position delete files accumulate more quickly than data files are rewritten. Even if you execute a delete aware compaction strategy, you can still find yourself with a large number of position delete files attached to otherwise healthy data files.&lt;/p&gt;

&lt;p&gt;Even after executing a compaction strategy that reduces the number of data files, the engine still has to open and apply the delete files during a read operation. Therefore, as delete files accumulate, the scan overhead will remain greater than it should be.&lt;/p&gt;

&lt;p&gt;This is particularly prevalent in tables that receive regular upserts or merges. Tables that are subject to append-only inserts do not typically exhibit this type of behavior. However, CDC pipelines and dimension tables do.&lt;/p&gt;

&lt;p&gt;Iceberg permits the explicit rewriting of position delete files as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_position_delete_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rewrite smaller delete files into fewer, larger ones and attempt to eliminate obsolete entries wherever possible. The objective of this approach is not merely to reduce the total number of files, but rather to minimize the number of files that the engine has to open during a read operation and thus improve performance.&lt;/p&gt;

&lt;p&gt;You may also limit the scope of this rewrite operation, similar to how you limit the scope of data file rewrites, by specifying the partition(s) to be rewrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_position_delete_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'event_date &amp;gt;= DATE &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;2026-02-01&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have already rewritten the data files and the performance of your application has not improved, inspect the distribution of delete files. In some cases, rewriting data files will reduce fragmentation, but will leave a heavy delete layer behind. In such cases, the separate rewriting of delete files will be required.&lt;/p&gt;

&lt;p&gt;Similar to all of the strategies described throughout this guide, keep the rewriting of delete files focused. There is little to be gained by rewriting delete files across the entire table if only a limited number of partitions are subject to upserts.&lt;/p&gt;

&lt;p&gt;In addition, treating the maintenance of delete files as a separate entity maintains the predictability of read cost. Otherwise, even though the data files themselves are sized appropriately, the accumulated overhead that is generated by the engine processing the delete files can slow over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Lower the commit frequency for streaming workloads
&lt;/h2&gt;

&lt;p&gt;When writing to Iceberg from streaming or micro-batch applications, the commit frequency is one of the largest factors contributing to the overall cost multiplier in the system.&lt;/p&gt;

&lt;p&gt;Each commit generates a new snapshot and produces new metadata work. This includes updating the manifest and creating new, small data files. As you commit every few seconds, you don't simply create small files; you create a long chain of snapshots and a continuous flow of metadata churn. While nothing "breaks," the planning is slowed and maintenance must continually struggle to keep pace with the increasing overhead.&lt;/p&gt;

&lt;p&gt;The frustrating aspect is that teams typically attempt to resolve this issue by applying more compaction, while the true solution lies upstream: stop committing as often.&lt;/p&gt;

&lt;h3&gt;
  
  
  The benefits of modifying the commit frequency
&lt;/h3&gt;

&lt;p&gt;When you increase the interval between commits, you generally gain three tangible benefits at once.&lt;/p&gt;

&lt;p&gt;First, you generate fewer snapshots, resulting in fewer pieces of metadata that the engine has to evaluate during planning.&lt;/p&gt;

&lt;p&gt;Second, you generate fewer manifests / manifest updates overall.&lt;/p&gt;

&lt;p&gt;Third, each commit contains more data, resulting in larger files (or fewer small files) and therefore reduced compaction pressure.&lt;/p&gt;

&lt;p&gt;You're essentially lowering entropy at the source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured Streaming using Spark: Set a valid trigger interval
&lt;/h3&gt;

&lt;p&gt;A common antipattern is to configure structured streaming to run "as fast as possible" or with a very short trigger. If you are writing to Iceberg tables, avoid this practice unless you have a true requirement for sub-minute freshness.&lt;/p&gt;

&lt;p&gt;The following shows the configuration for setting a reasonable commit interval in PySpark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeStream&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iceberg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outputMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;append&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkpointLocation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3://prod-checkpoints/events/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processingTime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1 minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod.db.events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your service level agreement (SLA) allows it, increase the commit interval to 2–5 minutes. In most analytics lakes, this tradeoff is worthwhile: slightly increased data freshness lag in exchange for significantly decreased metadata churn and less maintenance overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flink: Commit frequency follows checkpointing
&lt;/h3&gt;

&lt;p&gt;For Flink, Iceberg commits typically follow the checkpoint intervals. If you checkpoint every 30 seconds, you are essentially committing every 30 seconds. That's a lot.&lt;/p&gt;

&lt;p&gt;A more reasonable interval would be minutes, not seconds, unless you are operating a low-latency serving pipeline.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 5 minutes&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableCheckpointing&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300_000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ultimately, the best value will depend on recovery requirements and end-to-end latency needs. However, the underlying premise is the same: do not checkpoint so frequently that you turn your Iceberg table into a snapshot factory.&lt;/p&gt;

&lt;h3&gt;
  
  
  A simple method to select the interval
&lt;/h3&gt;

&lt;p&gt;Do not overcomplicate things. Ask yourself: what is the longest delay that your downstream consumers can tolerate for data freshness?&lt;/p&gt;

&lt;p&gt;If the response is "near real-time", you may still be fine at 1 minute. If the response is "a few minutes", take advantage of the situation and commit every few minutes.&lt;/p&gt;

&lt;p&gt;If the response is "we run dashboards hourly", then committing every 10 seconds is just self-imposed suffering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sanity check
&lt;/h3&gt;

&lt;p&gt;If you observe thousands of snapshots being created daily for a single table, this is typically an indication that your commit cadence is too aggressive for an analytics lake. You can certainly use Iceberg as a means of generating data in this manner - it is designed to be correct - but you will pay for it in terms of planning overhead and ongoing maintenance.&lt;/p&gt;

&lt;p&gt;Lowering the commit frequency is one of the few optimization techniques that will decrease costs and improve stability, independent of whether you adjust the compaction strategy. Fixing this earlier is beneficial because once you have multiple dozen or hundred of streaming-written tables, this behavior will dictate your operational overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Stop the repair loop; fix the write path
&lt;/h2&gt;

&lt;p&gt;Write paths that produce too many small files or heavily skewed partitions will make compaction a never-ending battle. As long as you continue to re-write the same issues, the lake will always drift back into an unhealthy condition.&lt;/p&gt;

&lt;p&gt;The majority of "we need more compaction" situations are actually "our write path is poorly configured."&lt;/p&gt;

&lt;h3&gt;
  
  
  Begin with Distribution Mode
&lt;/h3&gt;

&lt;p&gt;Small file generation is a common result of poor data distribution during the write process. A common scenario is when one writer (task) has the majority of the data for a given partition, it emits a couple of large files, while the remaining writers (tasks) emit a large number of smaller files. Worse, if the data distribution is unstable between batches, you will experience fragmentation regardless of how often you compact.&lt;/p&gt;

&lt;p&gt;Iceberg allows you to configure the way data is written across multiple writers. A good baseline configuration for many workloads is hash, as it generally spreads rows out more evenly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'write.distribution-mode'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'hash'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not completely remove the necessity for compaction, but it helps slow down how quickly fragmentation occurs again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Establish a Target File Size for Writers at the Table Level
&lt;/h3&gt;

&lt;p&gt;When writers do not have a target file size, you will see variability in the file sizes produced by writers across the engine and job. Some will produce 16MB files, some will produce 1GB files, etc., and compaction will continually attempt to normalize the mess.&lt;/p&gt;

&lt;p&gt;Create a target file size for writers at the table level and maintain it consistent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'write.target-file-size-bytes'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'536870912'&lt;/span&gt; &lt;span class="c1"&gt;-- 512MB&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have established a target file size for writers at the table level, compaction will transition from "fix everything" to "fix the outliers."&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimize the Writer and Not Just the Table
&lt;/h3&gt;

&lt;p&gt;Another common reason writers produce small files is due to the number of tasks that are utilized when writing versus the amount of data in each micro-batch or partition. The easiest method to optimize this is to adjust the degree of parallelism at the point of write.&lt;/p&gt;

&lt;p&gt;If you are experiencing hundreds of files per partition per batch, consider reducing the number of output partitions prior to writing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;df&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;pick&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;corresponds&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod.db.events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't have to find the optimal number. All you need to do is stop creating 1000 small files because your job happened to run with 1000 tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Not Create "Hot Partitions" by Design
&lt;/h3&gt;

&lt;p&gt;Some datasets inherently skew towards certain items, such as one customer producing 70% of the events, or a specific date receiving a massive backfill. When a partitioning scheme directs a large amount of data into a single partition, you will continually be fighting it with compaction.&lt;/p&gt;

&lt;p&gt;This is one of the few instances where adjusting the partitioning scheme to provide less skewness can greatly reduce compaction load. A common strategy is to add another dimension to the partitioning scheme (or create a derived shard key) to ensure that a single logical partition does not become a physical hotspot.&lt;/p&gt;

&lt;p&gt;You do not need to re-design the entire table. One additional dimension may be sufficient to prevent the worst skewness.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Maintain Metadata With Compaction
&lt;/h2&gt;

&lt;p&gt;You can obtain the desired file sizes, reduce the number of files, and yet still end up with a table whose performance and cost characteristics degrade over time. This is typically a metadata issue and not a physical layout issue.&lt;/p&gt;

&lt;p&gt;Every time you run compaction, you create a new snapshot. Every snapshot adds to the table's history. Each manifestation accumulates. The old metadata remains until something removes the history. If nothing removes the history, the table becomes deeper and more expensive to reason through, even if the physical data files appear to be clean.&lt;/p&gt;

&lt;p&gt;This is the most common trap: Teams focus on rewrite_data_files and neglect what happens to the snapshots and manifestations after the fact.&lt;/p&gt;

&lt;p&gt;In general, compaction should be run immediately followed by snapshot expiration. If you keep thousands of historical snapshots around "just in case," the engine still has to traverse the lineage when performing planning. Over time, this shows up as slower metadata reads and longer planning times.&lt;/p&gt;

&lt;p&gt;Typically, a snapshot expiration would look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expire_snapshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;retain_last&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual number of retained snapshots will vary based on your rollback and time-travel policies, however, it is critical that you establish a clear retention policy. Retention policies greater than infinite are rarely what you truly need.&lt;/p&gt;

&lt;p&gt;After expiring snapshots, it is also beneficial to perform manifest consolidation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite_manifests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if your file sizes are acceptable, fragmented manifests will require the planner to open and evaluate numerous small metadata files. Manifest consolidation will reduce the fan-out and stabilize the planner costs.&lt;/p&gt;

&lt;p&gt;Then there is the removal of orphans. Failed jobs, speculative tasks, and partial re-writes will leave files in object storage that are no longer referenced by the table. Over months, this is a lot of money. Removing these orphans will help to predictably manage the lake.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove_orphan_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;older_than&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-02-10 00:00:00'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The older_than guardrail is essential. You cannot afford to be racing with active writers in a production environment. Safety is more important than aggression.&lt;/p&gt;

&lt;p&gt;What complicates this is that the above actions are not independent. How you retain snapshots impacts what you can remove. How frequently the table is updated determines how frequently you need to rewrite manifests. Removing orphans is related to when commits occur and streaming jobs.&lt;/p&gt;

&lt;p&gt;Therefore, compaction is not simply a single maintenance action. It is part of a life cycle. Physical data files, snapshots, manifests, and physical storage move in tandem.&lt;/p&gt;

&lt;p&gt;At small scales, you can run these actions manually and get away with it. At larger scales, you need to be able to enforce consistency. Tables drift in various ways at varying rates. Without coordinated metadata maintenance, you will continue to repair file layout, while the metadata layer quietly continues to grow.&lt;/p&gt;

&lt;p&gt;Your goal is not simply to minimize the number of small files. Your goal is to maintain a table whose performance and cost characteristics remain stable over time. Compaction addresses the data layer. The three above actions address the metadata layer to prevent it from becoming the next bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap and Conclusion
&lt;/h2&gt;

&lt;p&gt;Compaction in Iceberg is not about scheduling rewrite_data_files. It is about maintaining the alignment between the layout, deletes, and metadata of a table with its actual usage.&lt;/p&gt;

&lt;p&gt;Compaction plus table maintenance is now a coordination problem. Therefore, having a control plane to continuously assess the health of tables, prioritize the top partitions requiring compaction, and coordinate compaction with metadata maintenance rather than treat these as separate jobs is a home run on the first step:&lt;/p&gt;

&lt;p&gt;Manual work and scripting are typically the alternatives.&lt;/p&gt;

&lt;p&gt;We previously reviewed the practical aspects of this:&lt;/p&gt;

&lt;p&gt;Use bin-pack to control the file count&lt;br&gt;&lt;br&gt;
Escalate to sorting only when the scan efficiency is the limiting factor&lt;br&gt;&lt;br&gt;
Use gates to restrict the re-writing of healthy data&lt;br&gt;&lt;br&gt;
Limit the scope to make the maintenance predictable&lt;br&gt;&lt;br&gt;
Proactively resolve delete-heavy partitions&lt;br&gt;&lt;br&gt;
Reduce the commit entropy in streaming jobs&lt;br&gt;&lt;br&gt;
Correct the write path to prevent constant repair of the same issues&lt;br&gt;&lt;br&gt;
Connect compaction to snapshot expiration, manifest rewrites, and orphan removal&lt;/p&gt;

&lt;p&gt;Most importantly, we connected compaction to snapshot expiration, manifest rewrites, and orphan removal - because the physical data layout and the metadata health are interdependent.&lt;/p&gt;

&lt;p&gt;Your goal is not to simply minimize the number of small files. Your goal is to maintain a lake that remains predictable - in terms of performance, cost, and operational overhead - as it grows.&lt;/p&gt;

&lt;p&gt;If you are operating Iceberg in production, I would appreciate your feedback regarding what has worked (and failed) for you. Real world patterns are always more interesting than theoretical ones.&lt;/p&gt;

&lt;p&gt;Thank you for reading 🍺&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>iceberg</category>
      <category>snowflake</category>
    </item>
    <item>
      <title>Iceberg Rewrite Manifest Files: A Guide</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Sun, 08 Feb 2026 15:31:17 +0000</pubDate>
      <link>https://dev.to/jonisar/iceberg-rewrite-manifest-files-a-guide-m5f</link>
      <guid>https://dev.to/jonisar/iceberg-rewrite-manifest-files-a-guide-m5f</guid>
      <description>&lt;h3&gt;
  
  
  Iceberg Rewrite Manifest Files: A&amp;nbsp;Guide
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8m6hfp8trhca4vpg19m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8m6hfp8trhca4vpg19m.png" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A data engineer happily runing into the quicksand&lt;/p&gt;

&lt;p&gt;Manifest rewrites are a critical ongoing operation in Iceberg table maintenance.&lt;/p&gt;

&lt;p&gt;Data keeps landing, queries stay correct, and nothing looks obviously wrong. But over time, query planning takes longer, metadata reads increase, and latency creeps up even though the amount of data scanned hasn’t really changed. In most production systems, the root cause is not data layout — it’s &lt;strong&gt;metadata&lt;/strong&gt;, and specifically how manifest files accumulate and degrade over time.&lt;/p&gt;

&lt;p&gt;Manifest files are central to how Iceberg works. They’re what allow engines to plan efficiently without listing object storage. But with frequent commits, streaming writes, deletes, and long snapshot histories, manifests naturally fragment. Iceberg doesn’t reorganize them automatically, so planning cost quietly grows until it starts to matter.&lt;/p&gt;

&lt;p&gt;This guide focuses on &lt;strong&gt;rewrite manifests&lt;/strong&gt;: what they actually do, when they help, and how to run them correctly in production. You’ll learn how to detect when manifest rewrites are needed, how they interact with snapshot expiration and compaction, and why running them in isolation often delivers disappointing results.&lt;/p&gt;

&lt;p&gt;We’ll also contrast two operational models: managing all of this manually with scripts and schedules, and handling it continuously through a &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;&lt;strong&gt;Control&lt;/strong&gt; &lt;strong&gt;Plane&lt;/strong&gt; like LakeOps&lt;/a&gt;, which optimizes table maintenance based on real workload behavior instead of fixed timers.&lt;/p&gt;

&lt;p&gt;The rest of the article guides you through mannual optimization with practical examples. No spec theory, no generic advice — just what actually works when Iceberg tables grow, change, and age in real systems.&lt;/p&gt;

&lt;p&gt;Let’s begin then 🙂&lt;/p&gt;

&lt;h3&gt;
  
  
  Smart Automation vs Manual&amp;nbsp;Scripts
&lt;/h3&gt;

&lt;p&gt;Before getting into mechanics, it’s important to understand the two main ways teams approach manifest management: Automated maintenance optimization with a Control Plane like &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;&lt;strong&gt;LakeOps&lt;/strong&gt;&lt;/a&gt;, and doing this operation manually and ongoingly by hand, and using generic scripts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Continuous Optimization with a Control&amp;nbsp;Plane
&lt;/h4&gt;

&lt;p&gt;A &lt;strong&gt;control plane&lt;/strong&gt; is a layer that sits above your data lake, catalogs, and query engines and takes responsibility for &lt;em&gt;operating and optimizing&lt;/em&gt; tables over time. Iceberg defines table structure and guarantees correctness, but it intentionally does not decide &lt;strong&gt;when&lt;/strong&gt;, &lt;strong&gt;where&lt;/strong&gt;, or &lt;strong&gt;how aggressively&lt;/strong&gt; maintenance should run. That operational and optimization gap is exactly what a control plane fills.&lt;/p&gt;

&lt;p&gt;Instead of running maintenance because a schedule says it’s time, a control plane continuously &lt;strong&gt;optimizes&lt;/strong&gt; tables based on what is actually happening in the system. Operations run only when and where they are needed, or according to explicit policies you define, rather than blindly across all tables.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;&lt;strong&gt;LakeOps&lt;/strong&gt;&lt;/a&gt; acts as a control plane for Iceberg by continuously analyzing telemetry from Iceberg catalogs and query engines. Using this data, LakeOps builds a live understanding of how each table behaves in practice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqi5oes035kbuki5fgus.png" width="799" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cotinious Optimization and Maintenance with an Iceberg Control Plane (source: lakeops.dev)&lt;/p&gt;

&lt;p&gt;From that telemetry, LakeOps continuously optimizes table maintenance. Manifest rewrites are triggered only when metadata fragmentation begins to impact planning or cost. Snapshot expiration runs only when retained history no longer provides real value. Compaction is optimized continuously to reduce small files before they create downstream metadata pressure. Orphan cleanup runs when metadata and data files are no longer referenced and can safely be removed.&lt;/p&gt;

&lt;p&gt;Coordination is central to optimization. Rewrite manifests, snapshot expiration, compaction, and cleanup are not independent jobs. They are executed as part of a single, continuous optimization loop that ensures only the required operations run, only on the tables that need them, and only at the point where they actually improve performance or cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F79itrha3wsuk3c3n8zhw.png" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Automating Smart Rewrite manfiest operations (source: lakeops.dev)&lt;/p&gt;

&lt;p&gt;Engineers don’t tune per-table schedules or chase drifting thresholds. They decide &lt;em&gt;what&lt;/em&gt; should be optimized and &lt;em&gt;within what constraints&lt;/em&gt;, and the control plane decides &lt;em&gt;when and where&lt;/em&gt; to run each operation. The result is stable metadata, predictable performance, and far less work.&lt;/p&gt;

&lt;p&gt;Learn more:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://overcast.blog/9-apache-iceberg-table-maintenance-tools-you-should-know-df864ed7a6d5" rel="noopener noreferrer"&gt;&lt;strong&gt;9 Apache Iceberg Table Maintenance Tools You Should Know&lt;/strong&gt;&lt;/a&gt;&lt;a href="https://overcast.blog/9-apache-iceberg-table-maintenance-tools-you-should-know-df864ed7a6d5" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Start at the beginning: What Manifest Files&amp;nbsp;Are
&lt;/h3&gt;

&lt;p&gt;Manifest files are the core metadata units Iceberg uses to describe &lt;em&gt;which data files exist&lt;/em&gt; and &lt;em&gt;what is inside them&lt;/em&gt;. They sit between snapshots and actual data files and are the reason Iceberg can plan queries efficiently without scanning directories or listing objects in storage.&lt;/p&gt;

&lt;p&gt;Each manifest file is essentially a list of data file entries. For every data file, the manifest records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  the partition values for that file&lt;/li&gt;
&lt;li&gt;  record count&lt;/li&gt;
&lt;li&gt;  per-column statistics such as min and max values&lt;/li&gt;
&lt;li&gt;  file size and other low-level attributes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a query runs, the engine does &lt;strong&gt;not&lt;/strong&gt; discover data files by walking object storage. Instead, it reads manifests referenced by the current snapshot and uses the stored statistics to decide which data files can be skipped entirely. This is how Iceberg enables predicate and partition pruning at planning time.&lt;/p&gt;

&lt;p&gt;Manifests are immutable. Every commit creates new metadata. When a write happens, Iceberg typically creates one or more new manifest files describing the files added or removed by that commit. Over time, a snapshot references many manifests, some created recently and some carried forward from older snapshots.&lt;/p&gt;

&lt;p&gt;This design is powerful, but it has predictable operational consequences.&lt;/p&gt;

&lt;p&gt;Frequent small commits, especially from streaming or micro-batch ingestion, tend to produce many small manifest files. For example, a streaming job that commits every minute may generate hundreds or thousands of manifests per day, each describing only a handful of data files. From Iceberg’s point of view this is correct, but for the query engine it means more metadata to read and evaluate during planning.&lt;/p&gt;

&lt;p&gt;Another issue is &lt;strong&gt;manifest clustering&lt;/strong&gt;. Manifests are not automatically reorganized around how tables are queried. If files are appended over time with mixed partitions or evolving data distributions, manifests may contain entries that are poorly aligned with common filters. The engine still prunes correctly, but it has to examine more metadata to do so.&lt;/p&gt;

&lt;p&gt;Snapshots make this worse if they are not expired. Each snapshot retains references to the manifests that describe its table state. Even if newer snapshots supersede old ones, the metadata remains live as long as those snapshots are kept. This means manifests that are no longer useful for active queries still participate in metadata reads and storage costs.&lt;/p&gt;

&lt;p&gt;The net effect is subtle but significant. Query planning time increases even though data size stays flat. Metadata I/O grows quietly. Storage costs creep up due to retained metadata. None of this breaks correctness, which is why it often goes unnoticed until performance degrades.&lt;/p&gt;

&lt;p&gt;Manifest rewrites exist specifically to address these issues. They allow Iceberg to reorganize and consolidate manifests so that the metadata layer reflects the &lt;em&gt;current&lt;/em&gt; table state and access patterns, rather than the historical accident of how data arrived over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Rewrite Manifests Does
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;rewrite manifests&lt;/strong&gt; operation restructures the metadata layer of an Iceberg table without touching the data itself.&lt;/p&gt;

&lt;p&gt;At a high level, Iceberg takes the manifest files referenced by the &lt;em&gt;current snapshot&lt;/em&gt;, reads the data-file entries inside them, and writes a new set of manifest files that describe the &lt;strong&gt;exact same live data files&lt;/strong&gt;, just in a layout that’s cheaper for engines to plan against. The commit updates table metadata to point the current snapshot at the new manifests. The old manifests become obsolete once nothing references them anymore (usually after snapshot expiration and cleanup).&lt;/p&gt;

&lt;p&gt;This is a metadata rewrite, not a data rewrite. No Parquet/ORC/Avro files are rewritten.&lt;/p&gt;

&lt;h4&gt;
  
  
  What actually&amp;nbsp;improves
&lt;/h4&gt;

&lt;p&gt;Rewrite manifests helps in three very concrete ways.&lt;/p&gt;

&lt;p&gt;It reduces manifest fan-out. When you have many small commits (streaming, micro-batch), you often end up with lots of tiny manifest files. Each query has to open and evaluate those manifests during planning. Rewriting consolidates many small manifests into fewer, larger ones, which reduces metadata I/O and planning latency.&lt;/p&gt;

&lt;p&gt;It aligns manifest layout with partitioning. Iceberg sorts data-file entries in manifests by fields in the partition spec. In practice, this tends to make partition pruning cheaper because related entries are adjacent and engines do less work to decide what to skip.&lt;/p&gt;

&lt;p&gt;It removes “historical write shape” from the current snapshot. Without rewrites, manifests reflect how data arrived over time, not how it’s queried. Rewriting reorganizes metadata around the current state, which is usually what you actually care about for planning.&lt;/p&gt;

&lt;h4&gt;
  
  
  What rewrite manifests does not&amp;nbsp;do
&lt;/h4&gt;

&lt;p&gt;It does not compact data files. Tiny data files stay tiny. It does not change partitioning or rewrite records.&lt;/p&gt;

&lt;p&gt;It does not delete old manifests by itself. If old snapshots still reference them, they’ll remain. Cleanup is a separate step.&lt;/p&gt;

&lt;h4&gt;
  
  
  Practical code&amp;nbsp;examples
&lt;/h4&gt;

&lt;p&gt;Below are a few examples that are actually useful in day-to-day operations, not just “hello world”.&lt;/p&gt;

&lt;p&gt;1) Measure the problem before you touch anything&lt;/p&gt;

&lt;p&gt;Start by inspecting the metadata table that lists manifests. Don’t assume column names — Iceberg versions and engines can differ — so first look at the schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="c1"&gt;-- Spark: inspect the manifests metadata table schema  &lt;/span&gt;
&lt;span class="k"&gt;DESCRIBE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;EXTENDED&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then get a baseline count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="c1"&gt;-- How many manifests does the current snapshot reference?  &lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_count&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this number grows steadily week over week while the table isn’t exploding in size, planning overhead is usually creeping up.&lt;/p&gt;

&lt;p&gt;2) Rewrite manifests via Spark SQL procedure (the most common operational path)&lt;/p&gt;

&lt;p&gt;This runs the rewrite in parallel using Spark:&lt;/p&gt;

&lt;p&gt;CALL prod.system.rewrite_manifests('db.my_table');&lt;/p&gt;

&lt;p&gt;In Spark, this returns a small result set with counters (how many manifests were rewritten, how many were added). In practice, you run the call, note the counters, and then re-check &lt;code&gt;my_table.manifests&lt;/code&gt; to see the manifest count drop.&lt;/p&gt;

&lt;p&gt;3) Rewrite manifests for a specific partition spec (when you’ve done partition evolution)&lt;/p&gt;

&lt;p&gt;If your table has evolved partition specs over time, you may want to rewrite manifests for a particular spec id:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_manifests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
  &lt;span class="k"&gt;table&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.my&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;table'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when an older spec still contributes a lot of manifest fragmentation and you want to target it instead of doing everything blindly.&lt;/p&gt;

&lt;p&gt;4) Disable Spark caching if executors get memory pressure during rewrites&lt;/p&gt;

&lt;p&gt;Some environments prefer to avoid caching during maintenance to reduce executor memory footprint:&lt;/p&gt;

&lt;p&gt;CALL prod.system.rewrite_manifests('db.my_table', false);&lt;/p&gt;

&lt;p&gt;If you’ve ever seen maintenance jobs destabilize executor memory, this is one of the first knobs to reach for.&lt;/p&gt;

&lt;p&gt;5) Validate the effect (simple but important)&lt;/p&gt;

&lt;p&gt;After the rewrite, validate that you actually improved the metadata shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_count&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_after&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the count doesn’t drop (or drops only slightly), the usual causes are that snapshots weren’t expired (old manifests still referenced), or the table’s write pattern keeps producing fragmentation faster than your maintenance cadence.&lt;/p&gt;

&lt;p&gt;That’s the point where you either tighten the full maintenance loop (expire snapshots, rewrite manifests, remove orphans, and revisit compaction) or stop doing this manually and let a control plane keep it stable continuously.&lt;/p&gt;

&lt;h3&gt;
  
  
  When You Should Rewrite Manifests
&lt;/h3&gt;

&lt;p&gt;Manifest rewrites are not something you run on a fixed schedule “just in case”. They are most effective when there is a clear signal that metadata, not data, is becoming the bottleneck.&lt;/p&gt;

&lt;p&gt;The most common trigger is &lt;strong&gt;planning getting slower while data size stays flat&lt;/strong&gt;. If query runtimes increase but the amount of data scanned is roughly the same, the extra time is often spent in planning and metadata evaluation. This is especially visible in engines that log planning or analysis time separately.&lt;/p&gt;

&lt;p&gt;Another strong signal is &lt;strong&gt;manifest growth that outpaces data growth&lt;/strong&gt;. If storage size grows slowly but the number of manifests keeps climbing, you are accumulating metadata fragmentation. This usually happens in tables with frequent commits, even if each commit is small.&lt;/p&gt;

&lt;p&gt;Tables that receive &lt;strong&gt;streaming or micro-batch writes&lt;/strong&gt; are prime candidates. Frequent commits tend to generate many small manifests. Even if data files are reasonably sized, the metadata layer becomes increasingly expensive to process.&lt;/p&gt;

&lt;p&gt;A very common real-world pattern is a table that “looks healthy” in storage metrics but becomes steadily slower to query over weeks. Nothing is broken, nothing obvious changed, but planning time creeps up. That is almost always a manifest problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing Manifest Rewrites&amp;nbsp;Manually
&lt;/h3&gt;

&lt;p&gt;If you don’t use a &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;&lt;strong&gt;control plane&lt;/strong&gt;&lt;/a&gt;, the following sequence reflects what works well in production if done right and in context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Inspect Metadata&amp;nbsp;Health
&lt;/h3&gt;

&lt;p&gt;Before you decide to rewrite manifests, you need &lt;strong&gt;visibility into the live metadata&lt;/strong&gt; — not guesswork, not periodic dashboards, but concrete numbers that reflect how fragmented the metadata has become.&lt;/p&gt;

&lt;p&gt;Iceberg exposes &lt;strong&gt;metadata tables&lt;/strong&gt; that you can query just like regular tables. These include tables like&amp;nbsp;&lt;code&gt;…$manifests&lt;/code&gt;,&amp;nbsp;&lt;code&gt;…$files&lt;/code&gt;,&amp;nbsp;&lt;code&gt;…$snapshots&lt;/code&gt;, etc. You can use these directly in SQL to inspect current state and spot trouble early.&lt;/p&gt;

&lt;h4&gt;
  
  
  Iceberg stores metadata in&amp;nbsp;layers:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-   A **manifest list** per snapshot points to all manifests for that snapshot.
-   Each **manifest file** lists a subset of data files, partition values, and column statistics (min/max/null counts).
-   Manifests may be reused across snapshots.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Lots of small commits → many small manifests.&lt;/li&gt;
&lt;li&gt;  Old snapshots hold onto old manifests.&lt;/li&gt;
&lt;li&gt;  Query engines read manifests during plan time to prune partitions/files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If manifests are fragmented or numerous, query planning becomes slow because engines read and evaluate many metadata files before they touch actual data.&lt;/p&gt;

&lt;p&gt;This is why &lt;strong&gt;metadata health matters early, not late&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  What to Look&amp;nbsp;At
&lt;/h4&gt;

&lt;p&gt;Here are the core checks you should be doing regularly — ideally automated — to monitor manifest health.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔍 1) Count the Current Manifests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run a live count of manifests referenced by the current snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_manifest&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_count&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sudden jump in this number relative to data size usually correlates with planning overhead.&lt;br&gt;&lt;br&gt;
&amp;nbsp;A steady climb over time, without data volume growth, is a strong indicator your metadata is fragmenting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Look at Files per Manifest&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Iceberg metadata stores statistics such as file counts per manifest. Pull a distribution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;  
  &lt;span class="k"&gt;CASE&lt;/span&gt;  
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;10 rows'&lt;/span&gt;  
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_count&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'10–100 rows'&lt;/span&gt;  
    &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'100+ rows'&lt;/span&gt;  
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_size&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;manifests&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;manifests&lt;/span&gt;  
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;strong&gt;lots of manifests with very few rows/files&lt;/strong&gt;, that means fragmentation. It means many small manifests (from tiny commits) that blow up planning work.&lt;/p&gt;

&lt;p&gt;You can also look at larger manifests: if lots of manifests hold small amounts of data, it’s a sign that maintenance will be valuable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) Compare Manifests to Data Growth&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you track how data size and manifest count change together, you can spot divergence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="c1"&gt;-- number of data files  &lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_file&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_count&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="c1"&gt;-- number of manifests  &lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_count&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;manifest_count&lt;/code&gt; grows faster than &lt;code&gt;data_file_count&lt;/code&gt;, that’s another sign of metadata inefficiency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) Look at Snapshots (Optional but Useful)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Snapshots tell you how many historical versions you’re retaining, which impacts how many manifests persist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;  
  &lt;span class="k"&gt;committed&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;snapshots&lt;/span&gt;  
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;committed&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;  
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Long snapshot histories mean old manifests may still be referenced and not cleaned up until expiration happens.&lt;/p&gt;

&lt;h4&gt;
  
  
  Interpreting the&amp;nbsp;Results
&lt;/h4&gt;

&lt;p&gt;Here are practical heuristics data engineers use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;High manifest count with small average manifest size&lt;/strong&gt; → metadata fragmentation (good candidate for rewrite).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Stable manifest count but slow query planning&lt;/strong&gt; → the problem might be clustering, not count; manifest rewrites can help.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Lots of snapshots older than retention needs&lt;/strong&gt; → metadata is being kept too long; expire them first so rewrites can be effective.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Manifest growth outpacing data file growth&lt;/strong&gt; → metadata is drifting away from the current shape of data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Example Scenario
&lt;/h4&gt;

&lt;p&gt;Imagine a streaming table ingesting updates every minute. You might see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  5,000 data files&lt;/li&gt;
&lt;li&gt;  2,000 manifests&lt;/li&gt;
&lt;li&gt;  70% of manifests contain &amp;lt;10 files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a classic candidate for consolidated manifests: smaller number of larger manifests will cut planning time dramatically, especially if queries filter on partitions that aren’t well clustered yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Expire Snapshots First
&lt;/h3&gt;

&lt;p&gt;Always expire snapshots &lt;strong&gt;before&lt;/strong&gt; rewriting manifests. This is not a best-practice nicety — it directly determines whether a manifest rewrite will actually do anything useful.&lt;/p&gt;

&lt;p&gt;The easiest way to achieve this is using a &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;&lt;strong&gt;Control Plane&lt;/strong&gt;&lt;/a&gt; for automated and optimized maintenance operations that include snapshot expirations in addition to manifest rewrites. Learn more:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/irRsF9VYP20"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fai8r2rsuyf1p4w5ju96k.png" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Automated and optimized snapshot expiration with a Control Plane (source: lakeops.dev)&lt;/p&gt;

&lt;p&gt;Her’es a deep dive into the topic and practical solutions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://overcast.blog/11-apache-iceberg-expired-snapshots-strategiesyou-should-know-ca7b81e87fb5" rel="noopener noreferrer"&gt;&lt;strong&gt;11 Expire Snapshots Optimizations for Apache Iceberg&lt;/strong&gt;&lt;/a&gt;&lt;a href="https://overcast.blog/11-apache-iceberg-expired-snapshots-strategiesyou-should-know-ca7b81e87fb5" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Snapshots are what keep manifests alive. Every snapshot references a specific set of manifest files that describe the table state at that point in time. As long as a snapshot exists, all of its manifests must remain reachable, even if they describe data that is no longer relevant for current queries.&lt;/p&gt;

&lt;p&gt;If you run a manifest rewrite while old snapshots are still retained, Iceberg can only optimize the manifests referenced by the &lt;em&gt;current&lt;/em&gt; snapshot. Older snapshots will continue to reference older manifests, which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  old manifests stay in storage,&lt;/li&gt;
&lt;li&gt;  metadata fan-out remains higher than expected,&lt;/li&gt;
&lt;li&gt;  storage costs don’t drop,&lt;/li&gt;
&lt;li&gt;  and in some engines, planning still touches more metadata than necessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the most common reason teams say “we ran rewrite manifests and it didn’t really help”.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why snapshot expiration comes&amp;nbsp;first
&lt;/h4&gt;

&lt;p&gt;Think of snapshot expiration as &lt;strong&gt;pruning the metadata graph&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Until you expire snapshots, Iceberg is obligated to preserve historical metadata for correctness and time travel. A rewrite cannot remove or consolidate manifests that are still referenced by retained snapshots. Expiring snapshots reduces the metadata surface area first, so the rewrite can actually consolidate what remains.&lt;/p&gt;

&lt;p&gt;In practice, snapshot expiration is what turns a rewrite from “cosmetic” into “effective”.&lt;/p&gt;

&lt;h4&gt;
  
  
  Inspect snapshot history before&amp;nbsp;expiring
&lt;/h4&gt;

&lt;p&gt;Before expiring anything, look at what you’re retaining:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;  
  &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="k"&gt;committed&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="k"&gt;operation&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;snapshots&lt;/span&gt;  
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;committed&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In many production systems, you’ll find snapshots going back weeks or months, even though nobody ever queries historical versions beyond a few days.&lt;/p&gt;

&lt;p&gt;That’s usually accidental, not intentional.&lt;/p&gt;

&lt;h4&gt;
  
  
  Expire snapshots based on real&amp;nbsp;needs
&lt;/h4&gt;

&lt;p&gt;Snapshot retention should reflect &lt;strong&gt;actual recovery and audit requirements&lt;/strong&gt;, not defaults or copy-pasted examples.&lt;/p&gt;

&lt;p&gt;If you only need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  a few days of rollback for operational safety, or&lt;/li&gt;
&lt;li&gt;  short-term auditability,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then retaining dozens or hundreds of snapshots actively hurts metadata efficiency with no upside.&lt;/p&gt;

&lt;p&gt;A common pattern is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  retain snapshots newer than a time threshold, and&lt;/li&gt;
&lt;li&gt;  always keep the last N snapshots as a safety net.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Example: expire old snapshots in&amp;nbsp;Spark
&lt;/h4&gt;

&lt;p&gt;Here’s a practical Spark SQL example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expire&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_snapshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.my&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;table'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="n"&gt;older&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_than&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="n"&gt;retain&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_last&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;  
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes snapshots older than the specified timestamp while keeping the most recent snapshots for safety.&lt;/p&gt;

&lt;p&gt;After this runs, many old manifests will become unreferenced — which is exactly what you want &lt;em&gt;before&lt;/em&gt; rewriting manifests.&lt;/p&gt;

&lt;h4&gt;
  
  
  Validate the&amp;nbsp;effect
&lt;/h4&gt;

&lt;p&gt;After expiring snapshots, re-check your metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_snapshots&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;snapshots&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a much smaller snapshot set. At this point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  old manifests are no longer protected,&lt;/li&gt;
&lt;li&gt;  rewrite manifests can actually consolidate metadata,&lt;/li&gt;
&lt;li&gt;  and orphan cleanup will be able to reclaim storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Run Rewrite Manifests
&lt;/h3&gt;

&lt;p&gt;At this point, the current snapshot references only the metadata that still matters. That gives Iceberg room to consolidate and reorganize manifests instead of carrying forward historical baggage.&lt;/p&gt;

&lt;h4&gt;
  
  
  What this step actually does&amp;nbsp;now
&lt;/h4&gt;

&lt;p&gt;After snapshot expiration, rewrite manifests can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  merge many small manifests into fewer, larger ones,&lt;/li&gt;
&lt;li&gt;  reorganize data-file entries so they’re better clustered by partition and statistics,&lt;/li&gt;
&lt;li&gt;  reduce the amount of metadata the engine has to read during planning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you skip snapshot expiration, most of these benefits are muted. After expiration, they show up immediately in planning time and metadata size.&lt;/p&gt;

&lt;h4&gt;
  
  
  Running rewrite manifests (Spark&amp;nbsp;example)
&lt;/h4&gt;

&lt;p&gt;In Spark-based environments, this is usually done via a system procedure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rewrite&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_manifests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'db.my&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;table'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This executes the rewrite in parallel across the cluster. Spark will read the existing manifests, generate a new optimized set, and commit a new snapshot that references them.&lt;/p&gt;

&lt;p&gt;The command itself is simple. The impact depends entirely on whether you prepared the table correctly in the earlier steps.&lt;/p&gt;

&lt;h4&gt;
  
  
  Validate that it actually&amp;nbsp;worked
&lt;/h4&gt;

&lt;p&gt;After the rewrite, always check the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_count&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_after&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a noticeable drop in manifest count or, at the very least, fewer very small manifests. If nothing changes, the usual reasons are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  snapshots were not expired, so old manifests are still referenced,&lt;/li&gt;
&lt;li&gt;  the table’s write pattern is fragmenting metadata faster than maintenance runs,&lt;/li&gt;
&lt;li&gt;  or the table is already in a reasonably healthy state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why this step is cheap — but not&amp;nbsp;free
&lt;/h4&gt;

&lt;p&gt;Rewrite manifests does not rewrite data files, so it’s much cheaper than compaction. However, it still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  reads all manifests referenced by the current snapshot,&lt;/li&gt;
&lt;li&gt;  writes new manifest files,&lt;/li&gt;
&lt;li&gt;  and commits new metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On large tables with many manifests, this can still consume noticeable CPU, memory, and I/O. That’s why you should not run it blindly across hundreds of tables at once.&lt;/p&gt;

&lt;h4&gt;
  
  
  Practical scheduling guidance
&lt;/h4&gt;

&lt;p&gt;If you’re doing this manually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  avoid peak query hours,&lt;/li&gt;
&lt;li&gt;  stagger rewrites across tables,&lt;/li&gt;
&lt;li&gt;  and gate execution on actual metadata health signals rather than time alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running rewrite manifests selectively, when metadata drift is real, is what keeps it a high-ROI operation instead of background noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Remove Orphan&amp;nbsp;Files
&lt;/h3&gt;

&lt;p&gt;Once snapshots are expired and manifests are rewritten, you need to clean up what is no longer referenced.&lt;/p&gt;

&lt;p&gt;Orphan files are data or metadata files that exist in storage but are no longer referenced by any snapshot. They typically appear after snapshot expiration, manifest rewrites, failed jobs, or aborted commits. Iceberg does not delete these files automatically, because doing so without coordination would risk correctness.&lt;/p&gt;

&lt;p&gt;If you stop after rewriting manifests, those unreferenced files will remain in object storage indefinitely.&lt;/p&gt;

&lt;p&gt;From Iceberg’s point of view, everything is correct after a rewrite. From your cloud bill’s point of view, nothing changed.&lt;/p&gt;

&lt;p&gt;Skipping orphan cleanup is one of the most common reasons teams see storage costs grow even though they “ran all the maintenance jobs.” The metadata graph is clean, but the physical files are still sitting in S3, GCS, or ADLS.&lt;/p&gt;

&lt;p&gt;This step is what turns logical cleanup into actual cost reduction.&lt;/p&gt;

&lt;h4&gt;
  
  
  What orphan cleanup actually&amp;nbsp;removes
&lt;/h4&gt;

&lt;p&gt;Orphan cleanup removes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  old manifest files no longer referenced by any snapshot,&lt;/li&gt;
&lt;li&gt;  metadata files left behind by rewrites and expired snapshots,&lt;/li&gt;
&lt;li&gt;  data files created by failed or rolled-back writes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; remove any file that is reachable from a live snapshot. If a file is still referenced, it stays.&lt;/p&gt;

&lt;h4&gt;
  
  
  Running orphan cleanup (Spark&amp;nbsp;example)
&lt;/h4&gt;

&lt;p&gt;In Spark environments, orphan cleanup is typically done with a system procedure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_orphan&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db.my&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;table'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="n"&gt;older&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_than&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;  
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;older_than&lt;/code&gt; guard is critical. It ensures Iceberg only deletes files older than a safe cutoff, protecting against races with in-flight or recently committed jobs.&lt;/p&gt;

&lt;p&gt;Never run orphan cleanup without a time threshold.&lt;/p&gt;

&lt;h4&gt;
  
  
  Validate the&amp;nbsp;effect
&lt;/h4&gt;

&lt;p&gt;After cleanup, you should see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  a reduction in storage usage over time,&lt;/li&gt;
&lt;li&gt;  fewer unreferenced metadata files,&lt;/li&gt;
&lt;li&gt;  and no change in query results or correctness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Storage metrics won’t always drop instantly due to object store reporting delays, but the trend should flatten instead of creeping upward.&lt;/p&gt;

&lt;h4&gt;
  
  
  The key&amp;nbsp;takeaway
&lt;/h4&gt;

&lt;p&gt;Snapshot expiration and manifest rewrites clean up &lt;strong&gt;logical metadata&lt;/strong&gt;. Orphan cleanup is what turns that into &lt;strong&gt;physical cleanup&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you skip this step, maintenance looks successful on paper but storage costs keep rising. If you include it consistently, metadata maintenance finally translates into real, measurable savings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Coordinate with Compaction
&lt;/h3&gt;

&lt;p&gt;Manifest rewrites optimize &lt;em&gt;how files are described&lt;/em&gt;. Compaction optimizes &lt;em&gt;how many files exist&lt;/em&gt;. If you ignore compaction, manifest rewrites will help briefly — then fragmentation will return.&lt;/p&gt;

&lt;p&gt;Small data files are the main upstream cause of manifest churn. Every time a write job produces many small files, Iceberg must record them in metadata. Even if you rewrite manifests perfectly, frequent small-file writes will recreate fragmentation within days.&lt;/p&gt;

&lt;p&gt;The optimal solution is to use an &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;Iceberg Control Plane&lt;/a&gt;. LakeOps for example, compacts data 95% faster and cheaper than alternatives thanks to a rust based engine and analyzed cross-system data. Compaction is also smarter, so query times go up ans costs go down dramatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;![](https://cdn-images-1.medium.com/max/1600/1*7w1IT-CzuDQRQsVuCionDA.png)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compaction optimizagtion with a dcontrol plane (source:lakeops/dev)&lt;/p&gt;

&lt;p&gt;In LakeOps, compaction processes are also synchronized with maintenance processes like manifest rewrites, so everything runs smoothly and you don’t have to connect and coordinate it yourself. Results are optimized and are usually far better than a home-made solution.&lt;/p&gt;

&lt;h4&gt;
  
  
  Example: why rewrites alone don’t&amp;nbsp;hold
&lt;/h4&gt;

&lt;p&gt;Let’s go back to the core problem for a second, and then see how to manually address it if you don’t use a control plane.&lt;/p&gt;

&lt;p&gt;Consider a table with streaming ingestion committing every few minutes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Each commit writes 20–50 small Parquet files.&lt;/li&gt;
&lt;li&gt;  Each commit creates one or more new manifests.&lt;/li&gt;
&lt;li&gt;  After a week, the table has thousands of data files and hundreds of manifests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You run snapshot expiration and rewrite manifests. Planning time improves.&lt;/p&gt;

&lt;p&gt;Two days later, the table is slow again.&lt;/p&gt;

&lt;p&gt;Nothing is broken. The write pattern simply recreated the same metadata pressure. This is what happens when compaction is missing or misaligned.&lt;/p&gt;

&lt;h4&gt;
  
  
  Use metadata to confirm compaction pressure
&lt;/h4&gt;

&lt;p&gt;Before scheduling more manifest rewrites, check whether small files are the real problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;  
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_size&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_in&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_file&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_mb&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the average file size is far below your engine’s sweet spot, manifest rewrites are treating symptoms, not the cause.&lt;/p&gt;

&lt;p&gt;Another useful signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;  
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;added&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_data&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_files&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_files&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_tracked&lt;/span&gt;  
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If file counts are high and keep growing quickly, metadata pressure will return unless compaction slows it down.&lt;/p&gt;

&lt;h4&gt;
  
  
  How compaction stabilizes manifest&amp;nbsp;rewrites
&lt;/h4&gt;

&lt;p&gt;When compaction is running correctly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  fewer data files are created per write cycle,&lt;/li&gt;
&lt;li&gt;  manifests grow more slowly and stay denser,&lt;/li&gt;
&lt;li&gt;  rewrite manifests becomes an occasional cleanup, not a recurring firefight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In stable tables, teams often find that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  compaction runs frequently (or continuously),&lt;/li&gt;
&lt;li&gt;  manifest rewrites run infrequently,&lt;/li&gt;
&lt;li&gt;  snapshot expiration runs regularly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That balance is what keeps planning predictable.&lt;/p&gt;

&lt;h4&gt;
  
  
  Practical coordination rule
&lt;/h4&gt;

&lt;p&gt;In production, a simple rule holds up well: If manifest rewrites are needed often, compaction is not doing enough.&lt;/p&gt;

&lt;p&gt;If you find yourself rewriting manifests weekly or daily on the same tables, it’s usually a sign that upstream file layout is unstable.&lt;/p&gt;

&lt;h4&gt;
  
  
  Should you add compaction code&amp;nbsp;here?
&lt;/h4&gt;

&lt;p&gt;At this point in the guide, &lt;strong&gt;full compaction code examples are usually not helpful&lt;/strong&gt;. Compaction is engine-specific, workload-specific, and already well-covered elsewhere. What matters here is understanding the dependency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  compaction reduces metadata churn,&lt;/li&gt;
&lt;li&gt;  reduced churn makes manifest rewrites effective,&lt;/li&gt;
&lt;li&gt;  without compaction, rewrites are temporary relief.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That mental model is more valuable than a generic compaction snippet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Add policies and guardrails
&lt;/h3&gt;

&lt;p&gt;If you manage maintenance with scripts or schedulers, automation needs guardrails or it will drift out of alignment with reality.&lt;/p&gt;

&lt;p&gt;The simpest way is to add a &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;&lt;strong&gt;Control Plane&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then you can define policies per table or for your entire lake.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgy7vm5m1c8trzkylvrtb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgy7vm5m1c8trzkylvrtb.png" width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Define maintenance policies with a control plane (source:lakeops.dev/)&lt;/p&gt;

&lt;p&gt;Start by skipping inactive tables. Tables that are rarely queried or written to don’t need aggressive maintenance. Running rewrites on them just burns cluster resources without benefit.&lt;/p&gt;

&lt;p&gt;Avoid peak query hours. Even though manifest rewrites are cheaper than data compaction, they still consume CPU, memory, and I/O. Running them during high query load increases contention and hurts user-facing performance.&lt;/p&gt;

&lt;p&gt;Trigger maintenance based on &lt;strong&gt;observed metadata health&lt;/strong&gt;, not time alone. Manifest count, average files per manifest, snapshot growth, and planning time trends are far better signals than “once a day” or “once a week”.&lt;/p&gt;

&lt;p&gt;Finally, expect thresholds to change. Write patterns evolve, query behavior shifts, and what worked six months ago may be wrong today. Scripts that never get revisited slowly turn into background noise or, worse, a source of instability.&lt;/p&gt;

&lt;p&gt;This is the point where many teams decide that maintaining guardrails manually is more work than it’s worth and move metadata maintenance into a control plane.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical steps to&amp;nbsp;take
&lt;/h3&gt;

&lt;p&gt;Rewrite manifests is one of those Iceberg operations that looks optional until it isn’t. When metadata is healthy, planning is fast and predictable. When it drifts, everything still works — just slower and more expensively. That’s why manifest issues often go unnoticed for a long time.&lt;/p&gt;

&lt;p&gt;In this guide, we walked through what manifest files actually are, why they fragment in real systems, and what rewrite manifests really does under the hood. We covered when rewrites are worth running, why snapshot expiration has to come first, how orphan cleanup turns logical cleanup into real cost savings, and why compaction is the long-term stabilizer that keeps metadata from degrading again.&lt;/p&gt;

&lt;p&gt;If you’re managing this manually, the step-by-step approach will get you there. If you want this handled continuously and optimzied, a &lt;strong&gt;C&lt;/strong&gt;&lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;&lt;strong&gt;ontrol&lt;/strong&gt; &lt;strong&gt;Plane&lt;/strong&gt;&lt;/a&gt; exists to do exactly that — operating and optimizing Iceberg tables based on real workload behavior instead of fixed schedules.&lt;/p&gt;

&lt;p&gt;The big takeaway is that rewrite manifests only works well as part of a &lt;strong&gt;coordinated maintenance loop&lt;/strong&gt;. Run it in isolation and the benefits are usually temporary. Pair it with snapshot expiration, compaction, and cleanup, and it becomes one of the highest-ROI metadata optimizations Iceberg offers.&lt;/p&gt;

&lt;p&gt;If you’ve run into edge cases, different patterns, or lessons learned the hard way, feel free to share them in the comments.&lt;/p&gt;

&lt;p&gt;Thanks for reading, and hope this helps keep your Iceberg tables healthy, fast, predictable, and boring in all the right ways.&lt;/p&gt;

&lt;p&gt;Cheers 🍺&lt;/p&gt;

&lt;h3&gt;
  
  
  Learn more
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://overcast.blog/11-iceberg-performance-optimizations-you-should-know-d9aef7aab235" rel="noopener noreferrer"&gt;&lt;strong&gt;11 Iceberg Performance Optimizations You Should Know&lt;/strong&gt;&lt;/a&gt;&lt;a href="https://overcast.blog/11-iceberg-performance-optimizations-you-should-know-d9aef7aab235" rel="noopener noreferrer"&gt;&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://overcast.blog/13-apache-iceberg-optimizations-you-should-know-85bc25690f00" rel="noopener noreferrer"&gt;&lt;strong&gt;13 Apache Iceberg Optimizations You Should Know&lt;/strong&gt;&lt;/a&gt;&lt;a href="https://overcast.blog/13-apache-iceberg-optimizations-you-should-know-85bc25690f00" rel="noopener noreferrer"&gt;&lt;/a&gt;.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://overcast.blog/11-apache-iceberg-cost-reduction-strategies-you-should-know-8de7acb14151" rel="noopener noreferrer"&gt;&lt;strong&gt;11 Apache Iceberg Cost Reduction Strategies You Should Know&lt;/strong&gt;&lt;/a&gt;&lt;a href="https://overcast.blog/11-apache-iceberg-cost-reduction-strategies-you-should-know-8de7acb14151" rel="noopener noreferrer"&gt;&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://overcast.blog/9-data-lake-cost-optimization-tools-you-should-know-2a5995be8f4b" rel="noopener noreferrer"&gt;&lt;strong&gt;9 Data Lake Cost Optimization Tools You Should Know&lt;/strong&gt;&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://overcast.blog/9-data-lake-cost-optimization-tools-you-should-know-2a5995be8f4b" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>11 Must-Know FrontEnd Trends for 2020</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Sun, 29 Dec 2019 12:30:59 +0000</pubDate>
      <link>https://dev.to/jonisar/11-must-know-frontend-trends-for-2020-13e1</link>
      <guid>https://dev.to/jonisar/11-must-know-frontend-trends-for-2020-13e1</guid>
      <description>&lt;h3&gt;
  
  
  Or- how to sound smart in frontEnd lunch conversations!
&lt;/h3&gt;

&lt;p&gt;Sounding smart at your team's lunch talks is obviously a great reason to stay updated with the latest frontend trends. It might even help you become a better developer, build better technology and better products. Maybe.&lt;/p&gt;

&lt;p&gt;So, please allow me to make this honorable quest easier by pointing you in a few interesting directions. I will not explain every concept A-Z, but will introduce the concept, how it’s useful and direct to further resources.&lt;/p&gt;

&lt;p&gt;For example, we’ll shortly cover an introduction to Micro Fontends, Atmoic Design, Web components TS take-over, ESM CDN and even Design tokens. Feel free to scroll through and mark the topics you’d like to learn more about. For any questions or more suggestions, just drop a comment below. &lt;/p&gt;

&lt;p&gt;Short disclaimer: I'm on the team building Bit. This doesn't make any of the following less true though. Enjoy!&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Micro frontends
&lt;/h2&gt;

&lt;p&gt;Micro Frontends are the buzziest frontend topic for lunch conversations.&lt;br&gt;
Ironically, while frontend development enjoys the modular advantages of components, it is still largely more monolithic than backend microservices.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5RM_mJgL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2ASdrrxeKfuAyDEAKATFNUNg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5RM_mJgL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2ASdrrxeKfuAyDEAKATFNUNg.png" alt=""&gt;&lt;/a&gt;&lt;br&gt;
Micro frontends bring the promise of splitting your frontend architecture into different frontends for different teams working on different parts of your app. Each team can gain autonomy over the end-to-end lifecycle of their micro frontend, which can be developed, versioned, tested, built, rendered, updated and deployed independently (using &lt;a href="https://bit.dev"&gt;tools like Bit&lt;/a&gt; for example).&lt;br&gt;
Instead of explaining the whole concept here, &lt;a href="https://martinfowler.com/articles/micro-frontends.html#InANutshell"&gt;**read this great post&lt;/a&gt;** by &lt;a class="comment-mentioned-user" href="https://dev.to/thecamjackson"&gt;@thecamjackson&lt;/a&gt;
 published at the @martinfowler blog. It’s really good and should cover everything you need to start digging into this concept.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kAQ3L-9K--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AfxACkCp1y_fDwnF-N7bVMQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kAQ3L-9K--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AfxACkCp1y_fDwnF-N7bVMQ.png" alt=""&gt;&lt;/a&gt;&lt;br&gt;
However, there are still certain shortages in today’s ecosystem. Mostly, people are worried by issues like the deployments of separate frontends, bundling, environment differences etc. &lt;a href="https://bit.dev"&gt;Bit&lt;/a&gt; already lets you isolate, version, build, test and update individual frontends/components. For now, this is mainly useful when working with multiple applications (though It’s already commonly used for gradually refactoring parts of existing apps via components).&lt;br&gt;
When Bit will introduce deployments in 2020, independent teams will get the power to develop, compose, version, deploy and update standalone frontends. It will let you compose UI apps together and let teams create simple decoupled codebases with independent continuous deployments and incremental upgrades. The composition of these frontends will end up creating your application. Here's how a UI app composed with Bit looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--j0oWJyZI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/v2nf316tdaw9nkxw7mng.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--j0oWJyZI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/v2nf316tdaw9nkxw7mng.png" alt="Composed UI app"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Learn more:&lt;br&gt;
&lt;a href="https://martinfowler.com/articles/micro-frontends.html"&gt;Micro Frontends - Martin Fowler&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Atomic Design
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://blog.bitsrc.io/atomic-design-and-ui-components-theory-to-practice-f200db337c24"&gt;&lt;br&gt;
&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--usZNK7ni--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/6528/1%2Aq5IW7xZF8AYFj8NZEVi17Q.jpeg"&gt;&lt;br&gt;
&lt;/a&gt; &lt;br&gt;
&lt;a href="https://bradfrost.com/blog/post/atomic-web-design/"&gt;Atomic Design&lt;/a&gt; is yet another super interesting topic for lunch talks, which I like to think about more of as a philosophy than a pure methodology.&lt;br&gt;
Simply put, the theory introduced by &lt;a href="https://dev.toundefined"&gt;Brad Frost&lt;/a&gt; compares the composition of web applications to the natural composition of Atoms, Molecules, Organisms and so on- ending with concrete web pages. Atoms compose molecules (e.g. text-input + button + label atoms = search molecule). Molecules compose an organism. Organisms live in a layout template, which can be concretized into a page delivered to your users.&lt;br&gt;
Here’s a &lt;a href="https://blog.bitsrc.io/atomic-design-and-ui-components-theory-to-practice-f200db337c24?"&gt;*detailed 30-seconds explanation with visual examples&lt;/a&gt;. *It includes very impressive drawings I made with great artistic talent, which you can copy-paste to your office board 😆&lt;br&gt;
The advantages of Atomic components go beyond building modular UI applications through modular and reusable components. This paradigm forces you to think in composition so you better understand the role and API of every component, their hierarchy, and how to abstract the building process of your application in an effective and efficient way. &lt;a href="https://bradfrost.com/blog/post/atomic-web-design/"&gt;Take a look.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Encapsulated Styling and Shadow Dom
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ud3q7udi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2276/1%2ATSOpITlAqbyYC_UYYW7zMg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ud3q7udi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2276/1%2ATSOpITlAqbyYC_UYYW7zMg.png" alt="Source: developer.mozzila.org"&gt;&lt;/a&gt;&lt;em&gt;Source: developer.mozzila.org&lt;/em&gt;&lt;br&gt;
An important aspect of components is encapsulation — being able to keep the markup structure, style, and behavior hidden and separate from other code on the page so that different parts do not clash, and the code can be kept nice and clean. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM"&gt;The Shadow DOM API&lt;/a&gt; is a key part of this, providing a way to attach a hidden separated DOM to an element.&lt;br&gt;
&lt;em&gt;Shadow&lt;/em&gt; DOM is actually used by browsers for a long time now. You &lt;a href="https://bitsofco.de/what-is-the-shadow-dom/"&gt;can think of the shadow DOM &lt;/a&gt;as a “DOM within a DOM”. It is its own isolated DOM tree with its own elements and styles, completely isolated from the original DOM.&lt;br&gt;
It allows hidden DOM trees to be attached to elements in the regular DOM tree — this shadow DOM tree starts with a shadow root, underneath which can be attached to any elements you want, in the same way as the normal DOM. The &lt;a href="https://dev.to/maxart2501/css-for-an-encapsulated-web-7fo"&gt;main implication&lt;/a&gt; of this is that we have &lt;em&gt;no need for a namespace&lt;/em&gt; for our classes, as there’s no risk of name clashing or style spilling. There also additional advantages. It is often referred to as the long-promised solution to a true encapsulation of styles for web components. Learn more:&lt;br&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM"&gt;Using shadow DOM&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  4. The TypeScript take over
&lt;/h2&gt;

&lt;p&gt;So lately every conversation &lt;a href="https://medium.com/@jtomaszewski/why-typescript-is-the-best-way-to-write-front-end-in-2019-feb855f9b164"&gt;makes it sound like TS is taking over&lt;/a&gt; frontend development. It is reported that &lt;a href="https://2018.stateofjs.com/javascript-flavors/typescript/"&gt;**80% of developers admit they would like to use or learn TypeScript in their next project&lt;/a&gt;**.&lt;br&gt;
Although it has it’s shortcomings, TS code is easier to understand, faster to implement, it produces less bugs and requires less boilerplate. Want to refactor your React app to work with TS? Go for it. Want to start gradually? Use tools like &lt;a href="https://github.com/teambit/bit"&gt;Bit&lt;/a&gt; to gradually refactor components in your app to TS and use the &lt;a href="https://bit.dev/bit/envs/compilers/react-typescript"&gt;React-Typescript compiler&lt;/a&gt; to build them independently from your app. This way to can gradually upgrade your code one component at a time.&lt;br&gt;
learn more:&lt;br&gt;
&lt;a href="https://medium.com/@jtomaszewski/why-typescript-is-the-best-way-to-write-front-end-in-2019-feb855f9b164"&gt;Why TypeScript is the best way to write Front-end in 2019And why you should convince everybody to use it.&lt;/a&gt;&lt;br&gt;
&lt;a href="https://eng.lyft.com/typescript-at-lyft-64f0702346ea"&gt;TypeScript at Lyft&lt;/a&gt;&lt;br&gt;
&lt;a href="https://slack.engineering/typescript-at-slack-a81307fa288d"&gt;TypeScript at Slack&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Web components- Stencil, Svelte, Lit &amp;amp; friends!
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tnwswldm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3200/1%2A-zkpV1IfOv-1dux6ZqWBCQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tnwswldm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3200/1%2A-zkpV1IfOv-1dux6ZqWBCQ.png" alt=""&gt;&lt;/a&gt;&lt;br&gt;
So basically, this is the future. Why? because these pure web components are framework agnostic and can work without a framework or with any framework- spelling &lt;strong&gt;standardization&lt;/strong&gt;. Because they are free from JS fatigue and are supported by modern browsers. Because their bundle size and consumption will be optimal, and VDOM rendering is mind-blowing.&lt;br&gt;
These components provide Custom Element, a Javascript API that allows you to define a new kind of html tag, HTML templates to specify layouts, and of course the Shadow DOM which is component-specific by nature.&lt;br&gt;
Prominent tools to know in this space are &lt;a href="https://github.com/Polymer/lit-html"&gt;**Lit-html&lt;/a&gt; &lt;strong&gt;(and &lt;a href="https://lit-element.polymer-project.org/"&gt;Lit-element&lt;/a&gt;), &lt;a href="https://github.com/ionic-team/stencil"&gt;**StencilJS&lt;/a&gt;&lt;/strong&gt;, &lt;a href="https://github.com/sveltejs/svelte"&gt;**SvelteJS&lt;/a&gt; &lt;strong&gt;and of course&lt;/strong&gt; &lt;a href="https://bit.dev/"&gt;Bit&lt;/a&gt;**, for reusable modular components which can be directly shared, consumed and developed anywhere.&lt;br&gt;
When thinking of the future of our UI development, and of how principles of modularity, reusability, encapsulation, and standardization should look like in the era of components, web components are the answer. Learn more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.bitsrc.io/7-tools-for-developing-web-components-in-2019-1d5b7360654d"&gt;7 Tools for Developing Web Components in 2019&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.bitsrc.io/9-web-component-ui-libraries-you-should-know-in-2019-9d4476c3f103"&gt;9 Web Components UI Libraries You Should Know in 2019&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.bitsrc.io/prototyping-with-web-components-build-an-rss-reader-5bb753508d48"&gt;Prototyping with Web Components: Build an RSS Reader&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  6. From component libraries to dynamic collections
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MxwQnLBi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AVmerRS_ufSltgSGYiNHinQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MxwQnLBi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2AVmerRS_ufSltgSGYiNHinQ.png" alt="Organize components in dynamic collections; reuse, compose, stay independent"&gt;&lt;/a&gt;&lt;em&gt;Organize components in dynamic collections; reuse, compose, stay independent&lt;/em&gt;&lt;br&gt;
The emergence of &lt;a href="https://blog.bitsrc.io/a-guide-to-component-driven-development-cdd-69dbd3d07bf0?source=collection_home---4------13-----------------------"&gt;component-driven development&lt;/a&gt; gave birth to a verity of tools. One prominent tool is &lt;a href="https://github.com/teambit/bit"&gt;Bit&lt;/a&gt;, alongside it’s hosting platform &lt;a href="https://bit.dev"&gt;Bit.dev&lt;/a&gt;.&lt;br&gt;
Instead of working hard to build a cumbersome and highly-coupled component-library, use Bit to continuously isolate and export existing components into a dynamically reusable shared-collection.&lt;br&gt;
Using &lt;a href="https://github.com/teambit/bit"&gt;Bit (GitHub)&lt;/a&gt; you can independently isolate, version, build, test and update UI components. It streamlines the process of isolating a component in an existing app, harvesting it to a remote collection, and using it anywhere. Every component can build, test, and render outside of any project. You can update a single component (and it’s dependants) and not the whole app.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ost0MZ7C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2Ac6475ieLqqEzb4htt3T94Q.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ost0MZ7C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2Ac6475ieLqqEzb4htt3T94Q.gif" alt=""&gt;&lt;/a&gt;&lt;br&gt;
In the bit.dev platform (or on your own server) your components can be remotely hosted and organized for different teams, so that every team can control the development of their own components. Every team can share and reuse components but keep their independence and control.&lt;br&gt;
The platform also provides the all-in-one ecosystem for a shared components out-of-the-box: It auto-documents UI components, renders components in an interactive playground, and even provides a built-in registry to install components using npm/yarn. In addition, you can bit import components for modifications in any repository.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qVhfkPYZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2ARZP_jNEEilVtmjGH4O4UHQ.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qVhfkPYZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2ARZP_jNEEilVtmjGH4O4UHQ.gif" alt=""&gt;&lt;/a&gt;&lt;br&gt;
In the short run, this revolutionizes the process of sharing and composing components in a similar way to how Spotify/iTunes changed the process of previously sharing Music through static CD Music Albums. It’s a dynamic and modular solution that lets everyone share and use components together.&lt;br&gt;
In the long run, Bit helps pave the way to micro-frontends. Why? Because it already lets you independently version, test, build and update parts of your UI application. In 2020 it will introduce independent deployments, which will finally allow different teams to own parts of your apps end-to-end: keep decoupled and simple codebases, let teams cautiously and continuously build and deploy incremental UI upgrades, and compose frontends together.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bit.dev"&gt;Share reusable code components as a team&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bit.dev/collections"&gt;UI Component design systems&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  7. State management: Bye Bye Redux? (Not….)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--00YNB73y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2290/1%2A6oeKSYnPG2pbg8vdaiteYg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--00YNB73y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2290/1%2A6oeKSYnPG2pbg8vdaiteYg.png" alt=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://blog.bitsrc.io/state-of-react-state-management-in-2019-779647206bbc"&gt;Redux is a hard beast to kill&lt;/a&gt;. While the pains of globally managing states in your app are becoming more clear as frontend becomes more modular, the sheer usefulness of Redux makes it a go-to solution for many teams.&lt;br&gt;
So will we say bye-bye to Redux in 2020? Probably not entirely 😄&lt;br&gt;
However, the uprising of new features within frameworks that handle states (React hooks, Context-API etc) are painting the way to a future without a global store. Tools like &lt;a href="https://github.com/mobxjs/mobx"&gt;Mobx&lt;/a&gt;, which only a year ago were rather scarcely adopted, are becoming more popular every day thanks to their component-oriented and scalable nature. You can explore &lt;a href="https://blog.bitsrc.io/state-of-react-state-management-in-2019-779647206bbc"&gt;more alternatives here&lt;/a&gt;.&lt;br&gt;
&lt;em&gt;Read&lt;/em&gt;: &lt;a href="https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889"&gt;*Making Sense of React Hooks&lt;/a&gt;* — by &lt;a href="https://dev.toundefined"&gt;Dan Abramov&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  8. ESM CDN
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---ahkLvgh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4000/1%2AdSWVWelaiGClQXD6nGhBuA.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---ahkLvgh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/4000/1%2AdSWVWelaiGClQXD6nGhBuA.jpeg" alt=""&gt;&lt;/a&gt;&lt;br&gt;
ES Modules is the standard for working with modules in the browser, standardized by ECMAScript. Using ES modules you can easily encapsulate functionalities into modules which can be consumed via CDN etc. With the release of Firefox 60, all &lt;a href="https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/"&gt;major browsers will support&lt;/a&gt; ES modules, and the Node mteam is working on adding ES module support to &lt;a href="https://nodejs.org/en/"&gt;Node.js&lt;/a&gt;. Also, &lt;a href="https://www.youtube.com/watch?v=qR_b5gajwug"&gt;ES module integration for WebAssembly&lt;/a&gt; is coming in the next few years. Just imagine modular &lt;a href="https://github.com/teambit/bit"&gt;Bit&lt;/a&gt; UI components composed in your app via CDN…&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/"&gt;ES modules: A cartoon deep-dive — Mozilla Hacks — the Web developer blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/denoland/deno"&gt;denoland/deno&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  9. Progressive web apps. Still growing.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developers.google.com/web/progressive-web-apps"&gt;Progressive web applications&lt;/a&gt; take advantage of the latest technologies to &lt;a href="https://www.smashingmagazine.com/2016/08/a-beginners-guide-to-progressive-web-apps/"&gt;combine the best of web and mobile apps&lt;/a&gt;. Think of it as a website built using web technologies but that acts and feels like an app. Recent advancements in the browser and in the availability of service workers and in the Cache and Push APIs have enabled web developers to allow users to install web apps to their home screen, receive push notifications and even work offline.&lt;br&gt;
Since PWAs provide an intimate user experience and because all network requests can be intercepted through service workers, it is imperative that the app be hosted over HTTPS to prevent man-in-the-middle attacks, which also spells better security. Here’s a great talk by Facebook developer &lt;a href="https://dev.toundefined"&gt;Omer Goldberg&lt;/a&gt; outlining best practices for PWAs.&lt;br&gt;
&lt;/p&gt;
&lt;center&gt;&lt;/center&gt;
&lt;h2&gt;
  
  
  10. Designer-developer integrations
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MZ8AwG4x--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2A55RGwH_5D3mIZoVhSCXWOA.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MZ8AwG4x--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://cdn-images-1.medium.com/max/2000/1%2A55RGwH_5D3mIZoVhSCXWOA.gif" alt=""&gt;&lt;/a&gt;&lt;br&gt;
With the uprise of &lt;a href="https://dev.to/jonisar/ui-component-design-system-a-developer-s-guide-19fg"&gt;component-driven design systems&lt;/a&gt; to enable a &lt;a href="https://blog.bitsrc.io/building-a-consistent-ui-design-system-4481fb37470f"&gt;consistent UI across products and teams&lt;/a&gt;, &lt;a href="https://blog.bitsrc.io/7-tools-for-building-your-design-system-in-2020-452d9c9b3b8e"&gt;new tools have emerged&lt;/a&gt; to bridge the gap between designers and developers. &lt;a href="https://codeburst.io/ui-design-system-and-component-library-where-things-break-d9c55dc6e386"&gt;This is no simple task however&lt;/a&gt;; While code itself is really the only source of truth (this is what your user really gets), most tools try to bridge the gap from the designer’s end. In this category you can find Framer, Figma, Invision DSM and more.&lt;br&gt;
From the developer’s end you can see how platforms like &lt;a href="https://bit.dev"&gt;Bit.dev&lt;/a&gt;, which host your next-gen component library and helps create adoption for shared components. The platform provides rendered visualization for your actual source-code so that designers can collaborate wit developers and create discussions over the source-code itself, in a visual way.&lt;br&gt;
Another promising idea to take note of is &lt;a href="https://css-tricks.com/what-are-design-tokens/"&gt;design-tokens&lt;/a&gt;. Placing tokens in your code through which designers can really control simple styling aspects (e.g. colors) directly through external collaboration tools. Integrated with platforms like Bit.dev, this can create a tighter workflow than ever before.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__link"&gt;
  &lt;a href="/jonisar" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KIXYeytP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--_HzUKqXm--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/13629/9979db72-9117-41df-83ca-0404028463e3.jpg" alt="jonisar image"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/jonisar/ui-component-design-system-a-developer-s-guide-19fg" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;UI Component Design System: A Developer’s Guide&lt;/h2&gt;
      &lt;h3&gt;JoniSar ・ Oct 23 '19 ・ 10 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#design&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#ui&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#frontend&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;div class="ltag__link"&gt;
  &lt;a href="https://medium.com/codeburstio/ui-design-system-and-component-library-where-things-break-d9c55dc6e386" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tWTMxjIU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/fit/c/96/96/1%2ApLN3R5sML3dcjAvUZDWtOA.png" alt="Jonathan Saring"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://medium.com/codeburstio/ui-design-system-and-component-library-where-things-break-d9c55dc6e386" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;UI Design System and Component Library: Where Things Break | by Jonathan Saring | codeburst&lt;/h2&gt;
      &lt;h3&gt;Jonathan Saring ・ &lt;time&gt;Aug 22, 2019&lt;/time&gt; ・ 8 min read
      &lt;div class="ltag__link__servicename"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KBvj_QRD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/medium_icon-90d5232a5da2369849f285fa499c8005e750a788fdbf34f5844d5f2201aae736.svg" alt="Medium Logo"&gt;
        Medium
      &lt;/div&gt;
    &lt;/h3&gt;
&lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;div class="ltag__link"&gt;
  &lt;a href="https://medium.com/bitsrcio/7-tools-for-building-your-design-system-in-2020-452d9c9b3b8e" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tWTMxjIU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/fit/c/96/96/1%2ApLN3R5sML3dcjAvUZDWtOA.png" alt="Jonathan Saring"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://medium.com/bitsrcio/7-tools-for-building-your-design-system-in-2020-452d9c9b3b8e" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;7 Tools for Building Your Design System in 2020 | by Jonathan Saring | Bits and Pieces&lt;/h2&gt;
      &lt;h3&gt;Jonathan Saring ・ &lt;time&gt;Dec 4, 2019&lt;/time&gt; ・ 11 min read
      &lt;div class="ltag__link__servicename"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KBvj_QRD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/medium_icon-90d5232a5da2369849f285fa499c8005e750a788fdbf34f5844d5f2201aae736.svg" alt="Medium Logo"&gt;
        Medium
      &lt;/div&gt;
    &lt;/h3&gt;
&lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  11. Web assembly — into the future?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://webassembly.org/"&gt;Web assembly&lt;/a&gt; brings language diversity into web development to cover gaps created by JavaScript. It is defined as a “a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications”.&lt;br&gt;
In his post, &lt;a href="https://dev.toundefined"&gt;Eric Elliott&lt;/a&gt; &lt;a href="https://medium.com/javascript-scene/what-is-webassembly-the-dawn-of-a-new-era-61256ec5a8f6"&gt;elegantly outlines the concept’s benefits&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An improvement to JavaScript:&lt;/strong&gt; Implement your performance critical stuff in wasm and import it like a standard JavaScript module.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A new language:&lt;/strong&gt; WebAssembly code defines an AST (Abstract Syntax Tree) represented in a &lt;strong&gt;binary format&lt;/strong&gt;. You can &lt;strong&gt;author and debug in a text format&lt;/strong&gt; so it’s readable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A browser improvement:&lt;/strong&gt; &lt;strong&gt;Browsers will understand the binary format&lt;/strong&gt;, which means we’ll be able to compile binary bundles that compress smaller than the text JavaScript we use today. Smaller payloads mean faster delivery. Depending on &lt;strong&gt;compile-time optimization opportunities&lt;/strong&gt;, WebAssembly bundles may run faster than JavaScript, too!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Compile Target:&lt;/strong&gt; A way for other languages to get first-class binary support across the entire web platform stack
To learn more about this concept, why it’s useful, where it will be used and why it’s not here yet, I suggest &lt;a href="https://medium.com/javascript-scene/why-we-need-webassembly-an-interview-with-brendan-eich-7fb2a60b0723"&gt;this great post&lt;/a&gt; and &lt;a href="https://www.youtube.com/watch?v=aZqhRICne_M&amp;amp;feature=emb_title"&gt;this great video&lt;/a&gt;.
&lt;a href="https://medium.com/javascript-scene/why-we-need-webassembly-an-interview-with-brendan-eich-7fb2a60b0723"&gt;&lt;strong&gt;Why We Need WebAssembly: An Interview with Brendan Eich&lt;/strong&gt;
*Brendan Eich &amp;amp; Eric Elliott Discuss WebAssembly Details*medium.com&lt;/a&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/aZqhRICne_M"&gt;
&lt;/iframe&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Learn more
&lt;/h2&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="https://medium.com/bitsrcio/13-top-react-component-libraries-for-2020-488cc810ca49" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jpCwpTfl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/fit/c/96/96/1%2Aw12_5tQWwj3V1nS8wOc3Hg.jpeg" alt="Fernando Doglio"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://medium.com/bitsrcio/13-top-react-component-libraries-for-2020-488cc810ca49" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;13 Top React Component Libraries for 2020 | by Fernando Doglio | Bits and Pieces&lt;/h2&gt;
      &lt;h3&gt;Fernando Doglio ・ &lt;time&gt;Jun 8, 2020&lt;/time&gt; ・ 13 min read
      &lt;div class="ltag__link__servicename"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KBvj_QRD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/medium_icon-90d5232a5da2369849f285fa499c8005e750a788fdbf34f5844d5f2201aae736.svg" alt="Medium Logo"&gt;
        Medium
      &lt;/div&gt;
    &lt;/h3&gt;
&lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;div class="ltag__link"&gt;
  &lt;a href="https://medium.com/bitsrcio/11-top-angular-developer-tools-for-2020-3d2621f1e157" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--F8kFRNS---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/fit/c/96/96/2%2AePGCw-LWOt-vRWj4REfBlA.jpeg" alt="Giancarlo Buomprisco"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://medium.com/bitsrcio/11-top-angular-developer-tools-for-2020-3d2621f1e157" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;11 Top Angular Developer Tools for 2020 | by Giancarlo Buomprisco | Bits and Pieces&lt;/h2&gt;
      &lt;h3&gt;Giancarlo Buomprisco ・ &lt;time&gt;Dec 24, 2019&lt;/time&gt; ・ 8 min read
      &lt;div class="ltag__link__servicename"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KBvj_QRD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/medium_icon-90d5232a5da2369849f285fa499c8005e750a788fdbf34f5844d5f2201aae736.svg" alt="Medium Logo"&gt;
        Medium
      &lt;/div&gt;
    &lt;/h3&gt;
&lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;div class="ltag__link"&gt;
  &lt;a href="https://blog.bitsrc.io/top-10-vuejs-developer-tools-becd61375447" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gPPTQzS9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/fit/c/96/96/1%2A0yN1ln4bBjuXhg10DyW-6Q.jpeg" alt="Shanika Wickramasinghe"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://blog.bitsrc.io/top-10-vuejs-developer-tools-becd61375447" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;11 Top VueJS Developer Tools for 2020 | by Shanika Wickramasinghe | Bits and Pieces&lt;/h2&gt;
      &lt;h3&gt;Shanika Wickramasinghe ・ &lt;time&gt;Dec 24, 2019&lt;/time&gt; ・ 8 min read
      &lt;div class="ltag__link__servicename"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KBvj_QRD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/medium_icon-90d5232a5da2369849f285fa499c8005e750a788fdbf34f5844d5f2201aae736.svg" alt="Medium Logo"&gt;
        blog.bitsrc.io
      &lt;/div&gt;
    &lt;/h3&gt;
&lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>react</category>
      <category>javascript</category>
      <category>frontend</category>
      <category>ui</category>
    </item>
    <item>
      <title>Reuse React Components Between Apps Like a Pro</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Wed, 20 Nov 2019 13:58:52 +0000</pubDate>
      <link>https://dev.to/jonisar/reuse-react-components-between-apps-like-a-pro-2a39</link>
      <guid>https://dev.to/jonisar/reuse-react-components-between-apps-like-a-pro-2a39</guid>
      <description>&lt;p&gt;One of the reasons we love React is the truly reusable nature of its components, even compared to other frameworks. Reusing components means you can save time writing the same code, prevent bugs and mistakes, and keep your UI consistent for users across your different applications.&lt;/p&gt;

&lt;p&gt;But, reusing React between apps components can be harder than it sounds. In the past, this process involved splitting repositories, boiler-plating packages, configuring builds, refactoring our apps and more.&lt;/p&gt;

&lt;p&gt;In this post, I'll show how to &lt;a href="https://bit.dev/" rel="noopener noreferrer"&gt;use Bit&lt;/a&gt; (&lt;a href="https://github.com/teambit/bit" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;) in order make this process much easier, saving around 90% of the work. Also, it will allow you to gradually collect existing components from your apps into a reusable collection for your team to share - &lt;a href="https://bit.dev/collections" rel="noopener noreferrer"&gt;like these ones&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkb3sn7l6g011e1vs6l9d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkb3sn7l6g011e1vs6l9d.png" alt="reuse react components" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this short tutorial, we'll learn how to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Quickly setup a Bit workspace&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Track and isolate components in your app&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Define a zero-config React compiler&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Version and export components from your app&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use the components in a new app&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Bonus: Leveraging Bit to modify the component from the consuming app (yes), and syncing the changes between the two apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Quick Setup
&lt;/h1&gt;

&lt;p&gt;So for this tutorial, we've prepared &lt;a href="https://bit.dev/collections" rel="noopener noreferrer"&gt;an example React App on GitHub&lt;/a&gt; you can clone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/teambit/bit-react-tutorial
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;bit-react-tutorial
&lt;span class="nv"&gt;$ &lt;/span&gt;yarn 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, go ahead and install Bit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;bit-bin &lt;span class="nt"&gt;-g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we'll need a remote collection to host the shared components. You can set up on &lt;a href="https://docs.bit.dev/docs/bit-server" rel="noopener noreferrer"&gt;your own server&lt;/a&gt;, but let's use Bit's free component hub instead. This way our collection can be visualized and shared with our team, which is very useful.&lt;/p&gt;

&lt;p&gt;quickly head over to &lt;a href="https://bit.dev" rel="noopener noreferrer"&gt;bit.dev and create a free collection&lt;/a&gt;. It should take less than a minute.&lt;/p&gt;

&lt;p&gt;Now return to your terminal and run &lt;code&gt;bit login&lt;/code&gt; to connect your local workspace with the remote collection, where we'll export our components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cool. Now return to the project you've cloned and init a Bit workspace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit init &lt;span class="nt"&gt;--package-manager&lt;/span&gt; yarn
successfully initialized a bit workspace.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Next, let's track and isolate a reusable component from the app.&lt;/p&gt;

&lt;h1&gt;
  
  
  Track and isolate reusable components
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fad6zwhrb5zhde3ug03tc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fad6zwhrb5zhde3ug03tc.png" alt="reusable-react-component-example" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bit lets you track components in your app, and isolates them for reuse,  including automatically defining all dependencies. You can track multiple components using a glob pattern (&lt;code&gt;src/components/*&lt;/code&gt;) or specify a path for a specific component. In this example, we'll use the later.&lt;/p&gt;

&lt;p&gt;Let's use the &lt;code&gt;bit add&lt;/code&gt; command to track the "product list" component in the app. We'll track it with the ID 'product-list'. Here's &lt;a href="https://bit.dev/bit/react-tutorial/product-list" rel="noopener noreferrer"&gt;an example of how it will look like&lt;/a&gt; as a shared component in bit.dev.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit add src/components/product-list
tracking component product-list:
added src/components/product-list/index.js
added src/components/product-list/product-list.css
added src/components/product-list/products.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's run a quick &lt;code&gt;bit status&lt;/code&gt; to learn that Bit successfully tracked all the files of the component. You can use this command at any stage to learn more, it's quite useful!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit status
new components
&lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"bit tag --all [version]"&lt;/span&gt; to lock a version with all your changes&lt;span class="o"&gt;)&lt;/span&gt;

     &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; product-list ... ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Define a zero-config reusable React compiler
&lt;/h1&gt;

&lt;p&gt;To make sure the component can run outside of the project, we'll tell Bit to define a reusable React compiler for it. This is part of how Bit isolates components for reuse, while saving you the work of having to define a build step for every component.&lt;/p&gt;

&lt;p&gt;Let's import the &lt;a href="https://bit.dev/bit/envs/compilers/react" rel="noopener noreferrer"&gt;React compiler&lt;/a&gt; into your project's workspace. You can find more compiler &lt;a href="https://bit.dev/bit/envs" rel="noopener noreferrer"&gt;here in this collection&lt;/a&gt;, including &lt;a href="https://bit.dev/bit/envs/compilers/react-typescript" rel="noopener noreferrer"&gt;react-typescript&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit import bit.envs/compilers/react &lt;span class="nt"&gt;--compiler&lt;/span&gt;
the following component environments were installed
- bit.envs/react@0.1.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right now the component might consume dependencies from your project. Bit's build is taking place in an &lt;em&gt;isolated environment&lt;/em&gt; to make sure the process will also succeed on the cloud or in any other project. To build your component, run this command inside your react project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Version and export reusable components
&lt;/h1&gt;

&lt;p&gt;Now let's export the component to your collection. As you see, you don't need to split your repos or refactor your app. &lt;/p&gt;

&lt;p&gt;First, let's tag a version for the component. Bit lets you version and export individual components, and as it nows about each component's dependants, you can later bump versions for single component and all its dependants at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit tag &lt;span class="nt"&gt;--all&lt;/span&gt; 0.0.1
1 component&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; tagged
&lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"bit export [collection]"&lt;/span&gt; to push these components to a remote&lt;span class="s2"&gt;")
(use "&lt;/span&gt;bit untag&lt;span class="s2"&gt;" to unstage versions)

new components
(first version for components)
     &amp;gt; product-list@0.0.1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run a quick 'bit status' to verify if you like, and then export it to your collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit &lt;span class="nb"&gt;export&lt;/span&gt; &amp;lt;username&amp;gt;.&amp;lt;collection-name&amp;gt;
exported 1 components to &amp;lt;username&amp;gt;.&amp;lt;collection-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now head over to your bit.dev collection and see how it looks!&lt;br&gt;
You can &lt;a href="https://docs.bit.dev/docs/tutorials/bit-react-tutorial#preview-the-react-component" rel="noopener noreferrer"&gt;save a visual example for your component&lt;/a&gt;, so you and your team can easily discover, try and use this component later on.&lt;/p&gt;
&lt;h1&gt;
  
  
  Install components in a new app
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffq99v2ey4ti5vx9cbyo6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffq99v2ey4ti5vx9cbyo6.png" alt="reuse-react-component-in-new-app" width="597" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create a new React app using create-create-app (or your own).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npx create-react-app my-new-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Move over to the new app you created.&lt;br&gt;
Install the component from bit.dev:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;yarn add @bit/&amp;lt;username&amp;gt;.&amp;lt;collection-name&amp;gt;.product-list &lt;span class="nt"&gt;--save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! you can now &lt;a href="https://docs.bit.dev/docs/tutorials/bit-react-tutorial#use-in-your-application" rel="noopener noreferrer"&gt;use the component in your new app&lt;/a&gt;!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you want to use npm, run &lt;code&gt;npm install&lt;/code&gt; once after the project is created so a package-lock.json will be created and npm will organize dependencies correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Modify components from the consuming app
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5c2kp0wggc385upepu6y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5c2kp0wggc385upepu6y.png" alt="develop-reusable-react-component" width="590" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let's use Bit to &lt;a href="https://docs.bit.dev/docs/tutorials/bit-react-tutorial#modify-the-component" rel="noopener noreferrer"&gt;import the component's source-code&lt;/a&gt; from bit.dev and make some changes, right from the new app.&lt;/p&gt;

&lt;p&gt;First, init a Bit workspace for the new project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And import the component&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit import &amp;lt;username&amp;gt;.&amp;lt;collection-name&amp;gt;/product-list
successfully imported one component
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what happened:&lt;/p&gt;

&lt;p&gt;A new top-level components folder is created that includes the code of the component, with its compiled code and node_modules (in this case the node_modules are empty, as all of your node_modules are peer dependencies and are taken from the root project.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.bitmap&lt;/code&gt; file was modified to include the reference to the component&lt;br&gt;
The package.json file is modified to point to the files rather than the remote package. Your package.json now displays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bit/&amp;lt;username&amp;gt;.&amp;lt;collection-name&amp;gt;.product-list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file:./components/product-list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start your application to make sure it still works. As you'll see, no changes are required: Bit takes care of everything.&lt;/p&gt;

&lt;p&gt;Then, just go ahead and make changes to the code anyway you like!&lt;br&gt;
&lt;a href="https://docs.bit.dev/docs/tutorials/bit-react-tutorial#update-the-code" rel="noopener noreferrer"&gt;Here's an example&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now run a quick &lt;code&gt;bit status&lt;/code&gt; to see that the code is changed. Since Bit tracks the source-code itself (via a Git extension), it "knows" that the component is modified.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit status
modified components
&lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"bit tag --all [version]"&lt;/span&gt; to lock a version with all your changes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"bit diff"&lt;/span&gt; to compare changes&lt;span class="o"&gt;)&lt;/span&gt;

     &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; product-list ... ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now tag a version and export the component back to bit.dev:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bit tag product-list
1 component(s) tagged
(use "bit export [collection]" to push these components to a remote")
(use "bit untag" to unstage versions)

changed components
(components that got a version bump)
     &amp;gt; &amp;lt;username&amp;gt;.&amp;lt;collection-name&amp;gt;/product-list@0.0.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit &lt;span class="nb"&gt;export&lt;/span&gt; &amp;lt;username&amp;gt;.&amp;lt;collection-name&amp;gt;
exported 1 components to &amp;lt;username&amp;gt;.&amp;lt;collection-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now see the updated version with the changes in bit.dev!&lt;/p&gt;

&lt;h1&gt;
  
  
  Update changes in the first app (checkout)
&lt;/h1&gt;

&lt;p&gt;Switch back to the &lt;code&gt;react-tutorial&lt;/code&gt; app you cloned and exported the component from, and check for updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit import
successfully imported one component
- updated &amp;lt;username&amp;gt;.&amp;lt;collection-name&amp;gt;/product-list new versions: 0.0.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;bit status&lt;/code&gt; to see that an update is availabe for &lt;code&gt;product-list&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit status
pending updates
&lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"bit checkout [version] [component_id]"&lt;/span&gt; to merge changes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"bit diff [component_id] [new_version]"&lt;/span&gt; to compare changes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"bit log [component_id]"&lt;/span&gt; to list all available versions&lt;span class="o"&gt;)&lt;/span&gt;

    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &amp;lt;username&amp;gt;.react-tutorial/product-list current: 0.0.1 latest: 0.0.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merge the changes done to the component to your project. The structure of the command is &lt;code&gt;bit checkout &amp;lt;version&amp;gt; &amp;lt;component&amp;gt;&lt;/code&gt;. So you run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bit checkout 0.0.2 product-list
successfully switched &amp;lt;username&amp;gt;.react-tutorial/product-list to version 0.0.2
updated src/app/product-list/product-list.component.css
updated src/app/product-list/product-list.component.html
updated src/app/product-list/product-list.component.ts
updated src/app/product-list/product-list.module.ts
updated src/app/product-list/products.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bit performs a git merge. The code from the updated component is now merged into your code.&lt;/p&gt;

&lt;p&gt;Run the application again to see it is working properly with the updated component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;yarn start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. A change was moved between the two projects. Your application is running with an updated component.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;By being able to more easily reuse React components between applications you can speed your development velocity with React, keep a consistent UI, prevent bugs and mistakes and better collaborate as a team over a collection of shared components. It's also a useful way to create a reusable UI component library for your team in a gradual way without having to stop everything or lose focus. &lt;/p&gt;

&lt;p&gt;Feel free to try it out yourself, &lt;a href="https://github.com/teambit/bit" rel="noopener noreferrer"&gt;explore the project in GitHub&lt;/a&gt;. Happy coding! &lt;/p&gt;

</description>
      <category>javascript</category>
      <category>frontend</category>
      <category>react</category>
      <category>ui</category>
    </item>
    <item>
      <title>UI Component Design System: A Developer’s Guide</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Wed, 23 Oct 2019 12:08:08 +0000</pubDate>
      <link>https://dev.to/jonisar/ui-component-design-system-a-developer-s-guide-19fg</link>
      <guid>https://dev.to/jonisar/ui-component-design-system-a-developer-s-guide-19fg</guid>
      <description>&lt;p&gt;Component design systems let teams collaborate to introduce a &lt;a href="https://blog.bitsrc.io/building-a-consistent-ui-design-system-4481fb37470f" rel="noopener noreferrer"&gt;consistent user visual and functional experience&lt;/a&gt; across different products and applications.&lt;/p&gt;

&lt;p&gt;On the designer's side, a predefined style guide and set of reusable master components enable consistent design and brand presented to users across all different instances (products etc) built by the organization. This is why great teams like &lt;a href="https://eng.uber.com/introducing-base-web/" rel="noopener noreferrer"&gt;Uber&lt;/a&gt;, &lt;a href="https://airbnb.design/building-a-visual-language/" rel="noopener noreferrer"&gt;Airbnb&lt;/a&gt;, &lt;a href="https://polaris.shopify.com/" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; and many others work so hard to build it.&lt;/p&gt;

&lt;p&gt;On the developer's side, a &lt;a href="https://stg.bit.dev/design-system" rel="noopener noreferrer"&gt;reusable set of components&lt;/a&gt; helps to standardize front-end development across different projects, save time building new apps, reduce maintenance overhead and provide easier onboarding for new team members.&lt;/p&gt;

&lt;p&gt;Most importantly, on the user's side, a successful component design system means less confusion, better navigation of your products, warm and fuzzy brand-familiarity feeling and better overall satisfaction and happiness. For your business, this means better results.&lt;/p&gt;

&lt;p&gt;But, building a successful design system can be trickier than you might think. Bridging the gap between designers and developers is no simple task, both in the process of building your system as well as over time. In this post, we’ll walk-through the fundamentals of successfully building a component design system, using it across projects and products, and growing a thriving and &lt;a href="https://blog.bitsrc.io/getting-adoption-for-design-systems-a-practical-guide-cde86ee9bf40" rel="noopener noreferrer"&gt;collaborative component ecosystem within the organization&lt;/a&gt;, that brings everyone together. We’ll also introduce some shiny modern tools that can help you build it. Please feel free to comment below, ask anything, or share from your own experience! &lt;/p&gt;

&lt;h1&gt;
  
  
  Bridging the gap between design and development through components
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfaap481lgylikf7tb82.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfaap481lgylikf7tb82.png" alt="Component design systemst" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When building your system you will face several challenges. The first, is achieving true collaboration between &lt;a href="https://blog.bitsrc.io/let-everyone-in-your-company-see-your-reusable-components-270cd3213fe9" rel="noopener noreferrer"&gt;designers, developers and everyone else&lt;/a&gt; (product, marketing etc). This is hard. Designers use tools like Photoshop, Sketch etc which are built for generating “flat” visual assets that don’t translate into real code developers will use. Tools like &lt;a href="https://www.framer.com/" rel="noopener noreferrer"&gt;Framer&lt;/a&gt; aim to bridge this gap on the designer’s side.&lt;/p&gt;

&lt;p&gt;Developers work with Git (and GitHub) and use different languages and technologies (such as component-based frameworks: React, Vue etc) and have to translate the design into code as the source of truth of the design’s implementation. Tools like &lt;a href="https://bit.dev" rel="noopener noreferrer"&gt;Bit&lt;/a&gt; turn real components written in your codebase into a visual and collaborative design system (&lt;a href="https://bit.dev/collections" rel="noopener noreferrer"&gt;examples&lt;/a&gt;), making it easy to reuse and update components across apps, and visualizing them for designers.&lt;/p&gt;

&lt;p&gt;Modern components are the key to bridging this gap. They function as both visual UI design elements as well as encapsulated and reusable functional units that implement UX functionality that can be used and standardized across different projects in your organization’s codebase. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.dev/components" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo75adqf0n5tkkcrr8e8n.gif" alt="Alt Text" width="719" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To bridge the gap, you’d have to let designers and other non-coding stakeholders collaborate over the source of truth, which is code. You can use &lt;a href="https://bit.dev" rel="noopener noreferrer"&gt;Bit&lt;/a&gt; or similar tools to bridge this gap and build a collaborative component economy where developers can easily &lt;a href="https://blog.bitsrc.io/getting-adoption-for-design-systems-a-practical-guide-cde86ee9bf40" rel="noopener noreferrer"&gt;build, distribute and adopt components&lt;/a&gt; while designers and everyone else can collaborate to build and align the design implementation of components across applications.&lt;/p&gt;

&lt;h1&gt;
  
  
  Choosing your stack and tools
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://medium.com/memory-leak/introducing-redpoints-design-and-front-end-engineering-landscape-ab377302a164" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvb0ovbgkfao2jldbfhj.jpeg" alt="design-system-landscape" width="799" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The choice of technologies and tools is a major key in the success of your design system. We’ll try to narrow it down to a few key choices you’d have to make along the way:&lt;/p&gt;

&lt;h4&gt;
  
  
  Framework or no framework?
&lt;/h4&gt;

&lt;p&gt;Modern frameworks like React, Vue and Angular provide an environment where you can build components and build applications with components. Whether you choose a view library or a full-blown MVC, you can start building your components with a mature and extensive toolchain and community behind you. However, such frameworks might not be future proof, and can limit the reuse and standardization of components on different platforms, stacks and use-cases.&lt;/p&gt;

&lt;p&gt;Another way to go is &lt;a href="https://blog.bitsrc.io/9-web-component-ui-libraries-you-should-know-in-2019-9d4476c3f103" rel="noopener noreferrer"&gt;framework-agnostic web components&lt;/a&gt;. Custom components and widgets that build on the Web Component standards, will work across modern browsers, and can be used with any JavaScript library or framework that works with HTML.&lt;/p&gt;

&lt;p&gt;This means more reuse, better stability, abstraction and standardization, less work and pretty much everything else that comes with better modularity. While many people are sitting around waiting on projects like web-assembly, in the past year &lt;a href="https://blog.bitsrc.io/7-tools-for-developing-web-components-in-2019-1d5b7360654d" rel="noopener noreferrer"&gt;we see new tools and techs&lt;/a&gt; rise to bring the future today.&lt;/p&gt;

&lt;p&gt;The core concept of a standardized component system that work everywhere &lt;a href="https://hackernoon.com/7-frontend-javascript-trends-and-tools-you-should-know-for-2020-fb1476e41083" rel="noopener noreferrer"&gt;goes naturally well with the core concept of web components&lt;/a&gt;, so don’t be quick to overlook it despite the less mature ecosystem existing around it today.&lt;/p&gt;

&lt;h4&gt;
  
  
  Component library or no library?
&lt;/h4&gt;

&lt;p&gt;Building a component library is basically a way to reduce the overhead that comes with maintaining multiple repositories for multiple components. Instead, you group multiple components into one repository and distribute it like a multi-song CD music album. &lt;/p&gt;

&lt;p&gt;The tradeoff? App developers (component consumers) can’t use, update or modify individual components they need. They are struggling with the idea of coupling the development of their products to that of the library. Component collaboration platforms like &lt;a href="https://bit.dev" rel="noopener noreferrer"&gt;Bit&lt;/a&gt; can greatly mitigate this pain, by sharing your library as a “playlist” like system of components that people can easily discover, use, update and collaborate-over across projects and teams. Every developer can share, find, use and update components right from their projects.&lt;/p&gt;

&lt;p&gt;Most larger organization implement a library (&lt;a href="https://blog.bitsrc.io/11-react-component-libraries-you-should-know-178eb1dd6aa4" rel="noopener noreferrer"&gt;examples&lt;/a&gt;) to consolidate the development of their components, consolidate all development workflows around the project and control changes. In today's ecosystem, it’s hard to scale component-based design systems without libraries mostly due to development workflows (PRs, issues, deployment etc). In the future, we might see more democratized component economies where everyone can freely share and collaborate.&lt;/p&gt;

&lt;p&gt;When building your library you effectively build a multi-component monorepo. &lt;a href="https://github.com/teambit/bit" rel="noopener noreferrer"&gt;Open-source tools like bit-cli can help&lt;/a&gt; you isolate each component, automatically define all its dependencies and environments, test and build it in isolation, and share it as a standalone reusable unit. It also lets app-developers import and suggest updates to components right from their own projects, to increase the adoption of shared components.&lt;/p&gt;

&lt;h4&gt;
  
  
  Component discoverability and visualization
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa92ty8goncjotq1karfx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa92ty8goncjotq1karfx.png" alt="Component design systems examples" width="799" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When building and distributing components you must create a way for other developers, and for non-developers collaborating with you, to discover and learn exactly which components you have, what they look like, how they behave in different states and how to use them.&lt;/p&gt;

&lt;p&gt;If working with tools like Bit you get this out of the box, as all your components &lt;a href="https://bit.dev/collections" rel="noopener noreferrer"&gt;are visualized in a design system made from your actual components&lt;/a&gt;. Developers can use and develop components from the same place designers, marketers and product managers can view and monitor the components.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.dev/collections" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feaf1eaposw72qg37g2e3.gif" alt="Component design systems" width="600" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If not, you can create your own documentation portal or leverage tools like &lt;a href="https://storybook.js.org/" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt; to organize the visual documentation of the components you develop in a visual way. Either way, without making components visually discoverable it will be hard to achieve true reusability and collaboration over components.&lt;/p&gt;

&lt;h1&gt;
  
  
  Building your design system: top-down vs. bottom-up
&lt;/h1&gt;

&lt;p&gt;There are two ways to build a component design system. Choosing the right one is mostly based on who your are and what you need to achieve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design first, then implement reusable components
&lt;/h3&gt;

&lt;p&gt;The first, mostly used by larger organizations that need to standardize UX/UI and development across multiple teams and products, is to &lt;strong&gt;design components first&lt;/strong&gt; and then make sure this design is implemented as components (often building a library) and used everywhere. &lt;/p&gt;

&lt;p&gt;A super over-simplified structure of this workflow looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build a visual language and design components&lt;/li&gt;
&lt;li&gt;Implement components in a git-based project in GitHub/Gitlab etc&lt;/li&gt;
&lt;li&gt;Distribute using component-platforms like Bit and/or to package managers&lt;/li&gt;
&lt;li&gt;Standardize instances of components across projects and apps&lt;/li&gt;
&lt;li&gt;Collaboratively monitor, update and evolve components (using Bit or other tools)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Code first, then collect components into a design system
&lt;/h3&gt;

&lt;p&gt;The second, often used by smaller and younger teams or startups, is to &lt;strong&gt;build-first&lt;/strong&gt; and then collect existing components from your apps into one system, align the design, and keep going from there. This approach saves the time consumed by the design-system project, time which startups often can’t afford to spend. &lt;a href="https://github.com/teambit/bit" rel="noopener noreferrer"&gt;bit-cli&lt;/a&gt; introduces the ability to virtually isolate components from existing repositories, building and exporting each of them individually as a standalone reusable unit, and collect them into one visual system made of your real code. So, you can probably use it to collect your components into a system in a few hours without having to refactor, split of configure anything, which is a quick way to do it today.&lt;/p&gt;

&lt;p&gt;This workflow looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Isolate and collect components already existing in your apps into one collection (Bit is useful)&lt;/li&gt;
&lt;li&gt;Bring in designers and other stakeholders to learn what you have and introduce your visual language into this collection&lt;/li&gt;
&lt;li&gt;Update components across projects to align to your new collection&lt;/li&gt;
&lt;li&gt;Use these components to build more products and apps&lt;/li&gt;
&lt;li&gt;Collaboratively monitor, update and evolve components (using Bit or other tools)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Design systems and atomic design
&lt;/h4&gt;

&lt;p&gt;Through the comparison of components and their composition to atoms, molecules, and organisms, we can think of the design of our UI as a composition of self-containing modules put together.&lt;/p&gt;

&lt;p&gt;Atomic Design helps you &lt;a href="https://blog.bitsrc.io/atomic-design-and-ui-components-theory-to-practice-f200db337c24" rel="noopener noreferrer"&gt;create and maintain robust design systems&lt;/a&gt;, allowing you to roll out higher quality, more consistent UIs faster than ever before. &lt;/p&gt;

&lt;p&gt;Learn more in this post: &lt;a href="https://blog.bitsrc.io/atomic-design-and-ui-components-theory-to-practice-f200db337c24" rel="noopener noreferrer"&gt;Atomic Design and UI Components: Theory to Practice&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Collaboratively manage and update components
&lt;/h1&gt;

&lt;p&gt;Over time your design system is a living creature that changes as the environment does. Design might changes, and so should the components. Components might change to fit new products, and so should the design. So, you must think of this process as a 2-way collaborative workflow.&lt;/p&gt;

&lt;h4&gt;
  
  
  Controlling components changes across projects
&lt;/h4&gt;

&lt;p&gt;When a component is used in 2 or more projects, sooner or later you will have to change it. So, you should be able to update a component from one project to another, consolidate code-changes and update all dependent components impacted by the change.&lt;/p&gt;

&lt;p&gt;If you are using &lt;a href="https://bit.dev" rel="noopener noreferrer"&gt;Bit&lt;/a&gt; this is fairly easy. You can import a component into any project, make changes, and update them as a new version. Since Bit “knows” exactly which other components depend on this component in different projects, you can update all of them at once and learn that nothing breaks before updating. Since Bit extends Git, you can merge the changes across projects just like you do in a single repository. All the changes will be visually availbe to view and monitor in your shared &lt;a href="https://bit.dev" rel="noopener noreferrer"&gt;bit.dev&lt;/a&gt; component collection.&lt;/p&gt;

&lt;p&gt;If not, things become trickier, and your component infrastructure team will have to enforce updates to their libraries for all projects using these libraries, which impairs flexibility, creates friction and makes it hard to achieve true standardization through adoption. Yet, this is harder but not impossible, here is &lt;a href="https://medium.com/walmartlabs/how-to-achieve-reusability-with-react-components-81edeb7fb0e0" rel="noopener noreferrer"&gt;how Walmart Labs do it&lt;/a&gt;.  You will also have to make sure that both changes to code and design are aligned in both your design tools and library docs wikis, to avoid misunderstandings and mistakes.&lt;/p&gt;

&lt;h1&gt;
  
  
  Grow a component ecosystem in your organization
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4iy52k0jeekpifei8qqh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4iy52k0jeekpifei8qqh.png" alt="component-economyt" width="700" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building a design system is really about building a growing component ecosystem in your organization. This means that managing components isn’t a one-way street; you have to include the app-builders (component consumers) in this new economy, so that the components you build will actually use them in their applications and products. &lt;/p&gt;

&lt;p&gt;Share components that people can easily find and use. Let them collaborate and make it easy and fun to do so. Don’t force developers to install heavy libraries or dive-in too deep into your library just to make a small pull-request. Don’t make it hard for designers to learn exactly which components changes over time and make it easy for them to collaborate in the process.&lt;/p&gt;

&lt;p&gt;Your component design system is a &lt;strong&gt;living and breathing organism&lt;/strong&gt; that grows and evolves over time. If you try to enforce it on your organization, it might die. Instead, prefer legalization and democratization of components, their development and their design. Regulate this process to achieve standardization, but don’t block or impair adoption- at all costs. &lt;a href="https://bit.dev" rel="noopener noreferrer"&gt;Bit&lt;/a&gt; is probably the most prominent power-tool here too, but please do share more if you know them. &lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Design systems help to create consistency in the visual and functional experience you give you users, while forming your brand across different products and applications. Modern components, with or without a framework, let you implement this system as a living set of building blocks that can and should be shared across projects to standardize and speed development.&lt;/p&gt;

&lt;p&gt;As designers and developers use different tools, it’s critical to bring them together over a single source of truth, which is really your code since this is what your users really experience. A democratized and collaborative process between developers, designers, products, marketers and everyone else is the only way to grow a thriving and sustainable component ecosystem that breathes life into your design system.&lt;/p&gt;

&lt;p&gt;Modern tools built for this purpose, such as &lt;a href="https://bit.dev" rel="noopener noreferrer"&gt;Bit&lt;/a&gt; and others (&lt;a href="https://www.framer.com" rel="noopener noreferrer"&gt;FramerX&lt;/a&gt; and &lt;a href="https://builderx.io/" rel="noopener noreferrer"&gt;BuilderX&lt;/a&gt; are also interesting on the designer’s end) can be used to build, distribute and collaborate over components to turn your design system into a consistent and positive user experience everywhere, and to manage and collaborate over components across teams within the organization.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>design</category>
      <category>ui</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
