<?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>Managed Data Lake: A Guide for 2027</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Sun, 23 Aug 2026 09:05:42 +0000</pubDate>
      <link>https://dev.to/jonisar/managed-data-lake-a-guide-for-2027-39li</link>
      <guid>https://dev.to/jonisar/managed-data-lake-a-guide-for-2027-39li</guid>
      <description>&lt;h1&gt;
  
  
  Managed Data Lake: A Guide for 2027
&lt;/h1&gt;

&lt;p&gt;Apache Iceberg is the standard table format for production data lakes in 2027. Every major engine reads and writes it natively. The catalog ecosystem standardized on REST. You own your data on commodity storage with no lock-in.&lt;/p&gt;

&lt;p&gt;But Iceberg deliberately separates the table format from the system that keeps tables healthy. It gives you the &lt;em&gt;primitives&lt;/em&gt; for maintenance — &lt;code&gt;rewrite_data_files&lt;/code&gt;, &lt;code&gt;expire_snapshots&lt;/code&gt;, &lt;code&gt;remove_orphan_files&lt;/code&gt;, &lt;code&gt;rewrite_manifests&lt;/code&gt; — but not the intelligence to decide when, how, and in what order to run them. Without that operational layer, every Iceberg table degrades over time: small files accumulate, snapshots bloat metadata, sort orders drift from query patterns, orphan files inflate storage costs, and query performance decays silently until something breaks visibly.&lt;/p&gt;

&lt;p&gt;This operational gap is the central challenge of running a data lake at production scale. &lt;a href="https://lakeops.dev/blog/intelligent-lakehouse-like-netflix" rel="noopener noreferrer"&gt;Netflix built four internal services to address it&lt;/a&gt; — Autotune for compaction strategy selection, Polaris for catalog management, janitors for garbage collection, Metacat for cross-service observability — each staffed by dedicated teams over multiple years. &lt;a href="https://lakeops.dev/blog/iceberg-compaction-tools-compared" rel="noopener noreferrer"&gt;Google engineered automatic compaction and garbage collection directly into BigLake&lt;/a&gt;, so their managed Iceberg tables stay healthy regardless of write volume or query pattern changes.&lt;/p&gt;

&lt;p&gt;In 2027, you do not need to replicate that investment. This guide covers what "managed" actually means for a data lake, the degradation mechanics that make it necessary, the control plane architecture that solves it, and the practical paths to getting there — whether you are running 50 tables or 5,000.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Lakes Degrade — The Mechanics
&lt;/h2&gt;

&lt;p&gt;The degradation pattern is predictable and present in nearly every Iceberg lake running for more than three months without dedicated maintenance. Understanding these mechanics is necessary regardless of which management approach you choose.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Small-File Problem
&lt;/h3&gt;

&lt;p&gt;Every streaming writer — Flink, Spark Structured Streaming, Kafka Connect, RisingWave, CDC connectors — produces files sized by checkpoint intervals, not by optimal read patterns. A Flink job committing every 60 seconds across 100 active partitions creates 144,000 new files per day. Each of those files might be 1–5 MB. The target for efficient reads is 128–512 MB.&lt;/p&gt;

&lt;p&gt;Every query pays per-file overhead: an S3 GET request ($0.0004 per 1,000), a Parquet footer parse, a task-scheduling slot in the engine. This means query compute scales with file count rather than with actual data volume. You are paying for how the data was written, not for how much data exists.&lt;/p&gt;

&lt;p&gt;The insidious part is the rate. A batch table that loads once daily accumulates files slowly — degradation takes months to become visible. A streaming table crosses into problematic territory within days. A high-throughput streaming table (hundreds of partitions, sub-minute commits) can reach a degraded state within hours of going live if no background compaction runs alongside the writer. The longer you wait to compact, the heavier and more disruptive the operation becomes — it conflicts with active writers, consumes significant cluster resources, and takes hours instead of seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snapshot Accumulation
&lt;/h3&gt;

&lt;p&gt;Every Iceberg commit creates a new snapshot — that is the mechanism that enables time travel and atomic rollback. A streaming table at 5-minute commits accumulates ~288 snapshots per day. Without expiration, counts reach tens of thousands within weeks.&lt;/p&gt;

&lt;p&gt;Query planning reads the snapshot chain to resolve the current table state. Once you exceed roughly 1,000–2,000 retained snapshots, planning time degrades measurably. The &lt;code&gt;metadata.json&lt;/code&gt; file grows — production environments have seen it reach 400+ MB. The symptom is slow EXPLAIN, slow first-row-time, and users reporting that "the query hangs for a few seconds before anything happens." The data scan itself is fine. It is the &lt;em&gt;planning&lt;/em&gt; that has become expensive.&lt;/p&gt;

&lt;p&gt;The correct retention window depends on your time-travel requirements. For most analytics workloads, 3–7 days of snapshots is sufficient. Regulated industries (financial, healthcare) may require 30–90 days — but should understand the planning-time trade-off and invest in more aggressive manifest rewriting to compensate. Whatever your retention policy, it must be actively enforced. Iceberg does not expire snapshots on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manifest Fragmentation
&lt;/h3&gt;

&lt;p&gt;Manifests are Iceberg's lookup structure — they tell engines which data files exist in which partitions and what their column-level statistics are. Every commit appends at least one manifest entry. Over months of streaming writes, you accumulate hundreds or thousands of small manifest files.&lt;/p&gt;

&lt;p&gt;The query planner must read all of them to build an execution plan. This adds seconds of latency before any data file is opened. When Iceberg 1.11 introduced server-side scan planning (shifting metadata traversal off query engines entirely), lean manifests became even more valuable — the planning server benefits proportionally from fewer, larger manifest files.&lt;/p&gt;

&lt;p&gt;Most teams misdiagnose slow planning as an engine problem — they try tuning memory, scaling coordinators, or upgrading engine versions. It is almost always the manifests. Rewriting them into a smaller number of well-merged files is one of the highest-leverage operations available — and one of the least commonly automated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orphan Files
&lt;/h3&gt;

&lt;p&gt;Failed Spark stages, interrupted compaction, aborted writes, schema-evolution retries, OOM kills during large rewrites, and partial commits all leave files on object storage that no Iceberg snapshot references. They are invisible to &lt;code&gt;SELECT&lt;/code&gt; queries, invisible to Iceberg metadata, invisible to your data quality checks — but fully visible on your cloud bill.&lt;/p&gt;

&lt;p&gt;You cannot find them by querying your tables. You must compare what exists in object storage against what Iceberg metadata references — a set difference operation that requires listing entire prefixes and cross-referencing with the catalog. This is why orphan accumulation goes undetected for months. Without a dedicated process that runs this comparison regularly, you have no way to know how much you are spending on dead data.&lt;/p&gt;

&lt;p&gt;Production orphan sweeps routinely reclaim 20–40% of a lake's storage. For a 100 TB lake, that is 20–40 TB of billable storage serving no purpose. At $0.023/GB/month (S3 Standard), that is $460–$920 per month for one lake — just in orphan storage. Multiply by the number of accounts, regions, and environments your organization operates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sort-Order Drift
&lt;/h3&gt;

&lt;p&gt;Bin-pack compaction — merging small files into larger ones — solves the file-count problem but does nothing for data organization. If your data is not physically sorted by the columns queries actually filter on, Parquet min/max statistics span wide ranges and engines cannot skip irrelevant row groups.&lt;/p&gt;

&lt;p&gt;Consider a &lt;code&gt;transactions&lt;/code&gt; table sorted by &lt;code&gt;transaction_id&lt;/code&gt; (because it was the natural append key) but queried primarily by &lt;code&gt;customer_id&lt;/code&gt; and &lt;code&gt;event_date&lt;/code&gt;. Every file contains a wide range of customer IDs, so a predicate like &lt;code&gt;WHERE customer_id = 'abc123'&lt;/code&gt; cannot skip any files based on statistics alone — the engine scans everything.&lt;/p&gt;

&lt;p&gt;Sort the same data by &lt;code&gt;customer_id, event_date&lt;/code&gt; and the statistics become tight. A query for one customer touches one or two files instead of hundreds. The engine eliminates 95%+ of scan volume before reading a single row. This is not an incremental improvement. It is a categorical difference in query performance.&lt;/p&gt;

&lt;p&gt;The challenge is that sort orders set at table creation drift. The column that was the primary filter six months ago might not be today. New consumers join. Dashboard patterns shift. ML feature pipelines add access patterns the original designer never anticipated. Across hundreds of tables with different consumers, manual sort-order tuning is impractical — and sort orders that are wrong silently cost you compute on every query, every day, without generating any error or warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete File Accumulation
&lt;/h3&gt;

&lt;p&gt;Merge-on-read tables accumulate position and equality delete files with every UPDATE or DELETE operation. This is the mechanism that makes updates fast at write time — rather than rewriting the entire file, Iceberg records "this row is deleted" in a separate file.&lt;/p&gt;

&lt;p&gt;The cost lands on reads. Every query must reconcile live rows against all accumulated delete files for the partitions it touches. This reconciliation cost grows linearly with each write cycle. At high delete volumes — CDC workloads doing frequent upserts, GDPR compliance deleting records, slowly changing dimensions — the read-time overhead becomes significant. Queries that should complete in seconds spend most of their time reconciling deletes rather than reading actual data.&lt;/p&gt;

&lt;p&gt;Iceberg 1.11's deletion vectors (backed by Roaring bitmaps) improve the mechanism by storing deletes more compactly and enabling faster reconciliation. But the fundamental accumulation problem remains: unless compaction regularly &lt;em&gt;applies&lt;/em&gt; those deletes (physically rewriting files to exclude deleted rows and then removing the delete files), read performance degrades continuously.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Organizational Cost
&lt;/h3&gt;

&lt;p&gt;Beyond the technical debt, the team cost scales worst.&lt;/p&gt;

&lt;p&gt;Maintenance responsibilities accumulate incrementally: compaction scripts, expiration logic, monitoring, alerting, per-table configuration for heterogeneous workloads, debugging writer conflicts, handling OOM failures, on-call rotation for overnight maintenance failures, documentation, and onboarding. Each table added to the lake adds marginal load to a team that is not growing proportionally.&lt;/p&gt;

&lt;p&gt;Platform engineers who should be building data products, self-service tooling, or pipeline reliability improvements are instead maintaining an ever-growing library of operational scripts. The knowledge concentrates in a few heads — when those people leave, the lake degrades within weeks because nobody else knows what the DAGs do or why they are configured that way.&lt;/p&gt;

&lt;p&gt;This cost does not show up on cloud bills but manifests in team velocity, retention risk, and the growing gap between what the data platform promises and what it delivers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "Managed" Actually Means
&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%2F6iwkyf5nes57plqn5wfb.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%2F6iwkyf5nes57plqn5wfb.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The term is overloaded. Vendors use it to mean proprietary platforms you migrate into. Cloud providers use it for single-service table maintenance. Some teams use it to mean a weekly compaction cron job. Others equate it with having a catalog.&lt;/p&gt;

&lt;p&gt;A more useful definition: &lt;strong&gt;a managed data lake is one where every table stays healthy, every query hits well-organized data, and operational cost does not grow linearly with table count&lt;/strong&gt; — without requiring a platform team that scales proportionally with the lake.&lt;/p&gt;

&lt;p&gt;This definition has three important implications. First, it is about outcomes, not tools. Running compaction nightly does not make your lake managed if half your tables are still degraded. Second, it covers the full lifecycle — not just file compaction, but snapshot management, orphan cleanup, manifest optimization, sort alignment, policy enforcement, and observability. Third, it must scale sublinearly: managing 500 tables should not require 10x the effort of managing 50.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture
&lt;/h3&gt;

&lt;p&gt;Your data lake already has three layers: commodity object storage (S3, GCS, ADLS), a table format and catalog (Iceberg plus Polaris/Glue/Nessie/Gravitino/Lakekeeper), and compute engines (Spark, Trino, Snowflake, Athena, DuckDB, Flink). These give you storage economics, structural reliability, and query flexibility.&lt;/p&gt;

&lt;p&gt;What you are missing is the fourth layer: an &lt;strong&gt;operations control plane&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is not the same as the catalog. The catalog (Polaris, Glue, BigLake Metastore) is a &lt;em&gt;metadata&lt;/em&gt; control plane — it resolves table locations, handles access control, and provides the REST interface for multi-engine access. Every major catalog now implements the REST spec: Polaris (Apache top-level project since February 2026), Glue's REST adapter, BigLake Metastore, Nessie, Gravitino, Lakekeeper.&lt;/p&gt;

&lt;p&gt;The operations control plane sits above the catalog. It uses metadata as input, but its job is fundamentally different: observe structural health across every table, classify what needs attention, decide what maintenance to run, execute it safely and in the correct sequence, adapt compaction to real query patterns across all engines, enforce governance at lake scale, route queries to optimal engines, and feed outcomes back into future decisions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; is this operations control plane — a purpose-built product for Apache Iceberg that does nothing else. &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%2F629ldgrwaebx8v0urxab.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%2F629ldgrwaebx8v0urxab.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is not an engine, not storage, not a catalog. It connects to your existing catalogs (AWS Glue, Polaris, Gravitino, Nessie, Lakekeeper, S3 Tables) and engines (Spark, Trino, Flink, Snowflake, Athena, DuckDB), adds autonomous management across the entire lake, and requires no data movement or pipeline changes. Most teams have their first lake-wide health assessment within ten minutes of connecting a catalog.&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%2Fn4oifhfmn78lppomv6c1.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%2Fn4oifhfmn78lppomv6c1.png" alt=" " width="799" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key architectural property: it does not replace anything. If you remove the control plane, everything continues working exactly as before — just without the automation. Your storage, your engines, your catalog, your data — all unchanged. The control plane adds intelligence to an existing stack. It does not create dependency.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Maturity Spectrum
&lt;/h2&gt;

&lt;p&gt;Teams operating Iceberg follow a predictable progression. Identifying where you sit determines what to invest in next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1: No maintenance.&lt;/strong&gt; Iceberg adopted for format-level benefits. Pipelines land data. Nobody explicitly owns table health. Performance degrades and storage costs rise gradually, but the symptoms are not connected to a root cause. Common in organizations that migrated to Iceberg within the last year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2: Reactive fixes.&lt;/strong&gt; After an incident — slow dashboard, cost anomaly, failed SLA — an engineer writes a one-off compaction script or snapshot expiration job. The immediate pain resolves, but only for the tables that broke visibly. The rest of the lake continues degrading silently. The cycle repeats every few months on different tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 3: Scheduled automation.&lt;/strong&gt; The team builds Airflow DAGs or cron jobs running maintenance on a fixed schedule. Compaction nightly. Snapshot expiration weekly. Orphan cleanup monthly. This works for stable batch workloads but has structural limitations at scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wastes compute on healthy tables that do not need maintenance&lt;/li&gt;
&lt;li&gt;Misses tables that degrade between cycles (a streaming table needing hourly compaction gets nightly)&lt;/li&gt;
&lt;li&gt;Cannot adapt to changing workloads — new query patterns do not trigger sort-order changes&lt;/li&gt;
&lt;li&gt;Runs operations as independent jobs without sequencing coordination, creating race conditions&lt;/li&gt;
&lt;li&gt;The maintenance infrastructure itself requires ongoing maintenance: updating DAGs, handling failures, adjusting schedules, onboarding new tables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stage 4: Autonomous management.&lt;/strong&gt; A dedicated system continuously observes every table, classifies health based on combined structural, storage, and query signals, sequences operations in the correct order, adapts compaction strategy to real query patterns from cross-engine telemetry, enforces policies lake-wide, and feeds outcomes back into decisions. Maintenance is signal-driven rather than schedule-driven. Tables that need attention get it immediately. Healthy tables cost zero compute.&lt;/p&gt;

&lt;p&gt;Most production lakes sit between Stage 2 and Stage 3. The gap from Stage 3 to Stage 4 is where teams either build a dedicated platform (6–12 months, 2–3 senior engineers full-time, plus ongoing maintenance of the platform itself) or adopt an existing control plane. Stage 3 is where DIY efforts stall — building cross-engine telemetry collection, health classification logic, sequencing and conflict resolution, adaptive compaction strategy, and policy inheritance is a non-trivial distributed systems project that most teams underestimate.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Control Plane Works
&lt;/h2&gt;

&lt;p&gt;A control plane is not a collection of scripts with a dashboard. It is a closed-loop system where each stage feeds the next. The difference between Stage 3 automation (scheduled scripts) and Stage 4 automation (autonomous management) is entirely in this loop: observe → decide → act → optimize → govern → route → enable → learn.&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%2F3onve2pfkt3v8575rgzt.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%2F3onve2pfkt3v8575rgzt.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Observation: Unified Telemetry
&lt;/h3&gt;

&lt;p&gt;Everything starts with signals. Three telemetry streams feed every decision the system makes:&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;structural metadata&lt;/strong&gt; from Iceberg — file counts per partition, average file sizes, manifest depth, snapshot count and velocity, delete file ratios, current sort order configuration, partition layout, and schema history. This tells you the table's physical state — the raw structural facts that indicate health or degradation.&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%2Fs1ks0jcul0ym4fu83dc2.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%2Fs1ks0jcul0ym4fu83dc2.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;object storage metrics&lt;/strong&gt; — total bytes per prefix, orphan file counts, access patterns, request rates, storage class distribution. This reveals the cost reality that metadata alone does not capture. A table might look healthy in Iceberg metadata while harboring terabytes of orphaned files in its storage prefix.&lt;/p&gt;

&lt;p&gt;Third, and most critically, &lt;strong&gt;cross-engine query telemetry&lt;/strong&gt; — which columns appear in WHERE, JOIN, and GROUP BY clauses from every engine reading your tables. Trino knows its own queries. Spark knows its own. Snowflake knows its own. Athena knows its own. None of them knows what the others are doing on the same table. None of them can tell you what the &lt;em&gt;combined&lt;/em&gt; access pattern looks like across your full consumer base.&lt;/p&gt;

&lt;p&gt;A control plane &lt;a href="https://lakeops.dev/blog/iceberg-lakehouse-observability-guide" rel="noopener noreferrer"&gt;collects this telemetry from every connected engine&lt;/a&gt;, correlates it per table, and uses the combined signal to drive optimization decisions that no single engine could make alone. This is the fundamental capability that separates an operations control plane from single-engine maintenance tools — it sees the whole picture.&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%2F7799snwuiorspxq3km7g.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%2F7799snwuiorspxq3km7g.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This unified telemetry is what enables the system to classify every table as Healthy, Warning, or Critical — and to surface specific, actionable insights ranked by severity. You know exactly which of your 500 tables needs attention right now, what the problem is, what caused it, and what the fix should be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision: Health-Driven Prioritization
&lt;/h3&gt;

&lt;p&gt;Raw telemetry becomes actionable through health classification. Instead of blindly running compaction on every table overnight (Stage 3), the system targets precisely the tables that need attention — and leaves healthy tables alone.&lt;/p&gt;

&lt;p&gt;A table's health score reflects the combined severity of its degradation signals: file count relative to target, manifest fragmentation ratio, snapshot depth relative to retention policy, sort-order alignment with actual query patterns, orphan volume, and delete file accumulation. Each dimension has independent thresholds, and the composite determines the classification.&lt;/p&gt;

&lt;p&gt;When a table crosses from Healthy to Warning (file count exceeds target, sort order drifts from query patterns, manifest fragmentation passes threshold), maintenance triggers. When it returns to Healthy, operations pause. Critical tables get immediate attention regardless of scheduling. Warning tables are queued by priority. Healthy tables cost zero compute — no maintenance runs against them until conditions change.&lt;/p&gt;

&lt;p&gt;This feedback loop eliminates the two failure modes of Stage 3 automation: wasting compute on healthy tables that do not need attention, and missing tables that degrade between fixed-schedule cycles. A streaming table that crosses into Warning two hours after the last scheduled maintenance run does not wait until tomorrow night. It gets addressed when the signal fires.&lt;/p&gt;

&lt;h3&gt;
  
  
  Execution: Sequenced Maintenance
&lt;/h3&gt;

&lt;p&gt;The individual operations are well-understood — Iceberg provides the procedures. What matters is the order, the coordination, and the safety.&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%2Fc60qx1s0wr38nwhlg8si.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%2Fc60qx1s0wr38nwhlg8si.png" alt=" " width="799" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The correct sequence is: &lt;strong&gt;expire snapshots → remove orphan files → compact data files → rewrite manifests → refresh statistics&lt;/strong&gt;. Each step depends on the previous:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expiration dereferences files and snapshots that are past their retention window&lt;/li&gt;
&lt;li&gt;Orphan cleanup then safely removes those dereferenced files from storage — only possible after expiration has run&lt;/li&gt;
&lt;li&gt;Compaction operates on live data only — not on files that are about to be expired — reducing wasted work&lt;/li&gt;
&lt;li&gt;Manifest rewriting consolidates the new manifest structure that compaction created&lt;/li&gt;
&lt;li&gt;Statistics refresh ensures the newly organized data has accurate min/max values for predicate pushdown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running these in the wrong order wastes compute (compacting data that will be expired in the next step), creates safety risks (removing files that unexpired snapshots still reference), or produces suboptimal results (rewriting manifests before compaction means they immediately fragment again on the next compact).&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%2F039qnidy9trxjt6t1gp4.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%2F039qnidy9trxjt6t1gp4.png" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At Stage 3, these operations typically run as independent cron jobs — compaction in one DAG, expiration in another, orphan cleanup in a third, each with its own schedule and no awareness of the others. This creates exactly the sequencing failures described above. A control plane &lt;a href="https://lakeops.dev/blog/autonomous-iceberg-table-maintenance" rel="noopener noreferrer"&gt;runs them as a coordinated pipeline&lt;/a&gt; per table, every time, conflict-aware (never compacting partitions with active writers), with OCC-aware retry logic for optimistic concurrency conflicts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adaptive maintenance&lt;/strong&gt; bundles this further: rather than configuring each operation's frequency separately, a single data-driven policy monitors commit frequency, file accumulation rate, query load, and seasonal patterns — then runs the right operations at the right frequency, automatically. A high-velocity streaming table gets frequent bin-pack compaction to keep pace with the write rate. A stable batch table gets occasional deep sort compaction when access patterns shift. A table that received a sudden burst of writes gets immediate attention. One policy toggle replaces dozens of per-table DAG configurations.&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%2Fqdxtbug51rtk4fuw5tyw.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%2Fqdxtbug51rtk4fuw5tyw.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimization: Query-Aware Compaction
&lt;/h3&gt;

&lt;p&gt;This is where the control plane delivers its highest-impact differentiation from scripts and scheduled 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3rv130wlvdxabpk74bdr.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%2F3rv130wlvdxabpk74bdr.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Basic bin-pack compaction normalizes file sizes — merging many small files into fewer large ones. That is necessary (it solves the per-file overhead problem) but insufficient (it does nothing about data organization). A table with perfectly sized files but random data ordering still forces engines to scan everything because min/max statistics span the full value range.&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%2F39n704sn64ih8lvmdl6r.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%2F39n704sn64ih8lvmdl6r.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real performance leverage is in &lt;strong&gt;sort compaction&lt;/strong&gt;: physically rewriting data so records are ordered by the columns queries actually filter on. When data is correctly sorted, Parquet row-group min/max statistics become tight — each row group contains a narrow range of values for the sort columns. Engines read these statistics during planning and skip entire file groups whose ranges do not match the predicate. The difference is not incremental. It is the difference between scanning 100% of a table and scanning 5–15%.&lt;/p&gt;

&lt;p&gt;The challenge at scale is knowing what to sort by — and keeping it current. Static sort orders set at table creation drift within weeks as query patterns evolve. A column that was the primary filter six months ago might be secondary today. New consumers arrive. Dashboard logic changes. ML pipelines add new access patterns. Across hundreds of tables with different consumers across different engines, manual sort-order tuning is impractical.&lt;/p&gt;

&lt;p&gt;Cross-engine telemetry solves this. The control plane &lt;a href="https://lakeops.dev/blog/iceberg-compaction-strategies" rel="noopener noreferrer"&gt;observes which columns are actually filtered, joined, and grouped&lt;/a&gt; across all engines hitting each table — Trino, Spark, Snowflake, Athena, DuckDB — then physically re-sorts data to match the real combined access pattern. As patterns change, the sort order adapts. The optimization is continuous and automatic, not a one-time configuration that decays.&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%2Fvsv1m9u3qg9497sbpx1s.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%2Fvsv1m9u3qg9497sbpx1s.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LakeOps executes compaction on a purpose-built Rust/DataFusion engine — no JVM, no garbage collection pauses, no Spark clusters to provision or decommission. Compaction is event-driven (triggered by health signals, not cron schedules) and conflict-aware (never touching partitions with active writers, with built-in OCC retry). The engine completes bin-pack and sort operations in a fraction of the time Spark requires for the same workload, without the cluster management overhead.&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%2Fwzdy7uk3vu4sg3w1pjf4.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%2Fwzdy7uk3vu4sg3w1pjf4.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before committing to a sort-order change on a large table — where a wrong decision means rewriting terabytes of data for no benefit — &lt;strong&gt;layout simulations&lt;/strong&gt; let you test strategies safely. Simulations create a real Iceberg branch from the latest snapshot, apply the proposed layout to that branch, replay actual query patterns from cross-engine telemetry against it, and compare results against the current baseline. You see projected file counts, data-skipping improvement percentage, and scan reduction for each candidate strategy — before writing a single byte to production data. The branch is discarded after analysis. This is the verification step that makes autonomous sort optimization trustworthy at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Governance: Policies That Scale
&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%2F6i4mfxj7v5w2ried19e4.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%2F6i4mfxj7v5w2ried19e4.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At one table, configuration is a one-time decision. You pick a compaction strategy, set a retention window, choose a target file size, and move on. At 50 tables it becomes repetitive. At hundreds of tables across multiple catalogs and namespaces, it becomes governance entropy.&lt;/p&gt;

&lt;p&gt;The symptoms: some tables have compaction configured, others use defaults nobody verified. Retention windows are inconsistent — production tables with 90-day retention next to staging tables with no expiration at all. New tables inherit nothing because nobody templated the configuration. An engineer leaving means their table-specific tuning knowledge leaves with them. Auditors ask "what is your data retention policy?" and nobody can answer precisely because the truth is scattered across table properties, DAG configs, and Terraform state files.&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%2F1h21h8bfasactwwg8gjn.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%2F1h21h8bfasactwwg8gjn.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A control plane enforces &lt;a href="https://lakeops.dev/blog/managed-iceberg-2026" rel="noopener noreferrer"&gt;declarative policies with hierarchical inheritance&lt;/a&gt;: compaction strategy, target file size, snapshot retention, cleanup safety windows, and maintenance frequency defined at organization, catalog, namespace, or table level. Everything cascades downward — set a policy at the namespace level and every table in that namespace inherits it. New tables automatically pick up the correct configuration. Exceptions override at the leaf level when specific tables need different treatment. Policies are versioned, auditable, and diff-able — changes are tracked, reviewable, and reversible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routing: Multi-Engine Cost 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%2Fjn0d3g68nsrkqjhcdw46.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%2Fjn0d3g68nsrkqjhcdw46.png" alt=" " width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Running multiple engines on the same Iceberg tables is standard practice in 2027. The REST catalog spec means any engine can read any table. Most production lakehouses use at least three: Trino for interactive analytics, Spark for heavy ETL, and one of Snowflake/Athena/DuckDB for BI or ad-hoc queries. Each has radically different cost and latency profiles.&lt;/p&gt;

&lt;p&gt;A point lookup costs fractions of a cent on DuckDB running locally and significantly more on Snowflake's warehouse compute. A multi-table join with complex aggregations completes in seconds on Trino but might take minutes on DuckDB. A 10 TB batch transformation is cheapest on Spark with spot instances. Without routing, applications hardcode engine connections — every query goes to one engine regardless of whether a cheaper or faster option exists for that specific query shape.&lt;/p&gt;

&lt;p&gt;A control plane's &lt;a href="https://lakeops.dev/blog/iceberg-multi-engine-architecture" rel="noopener noreferrer"&gt;routing layer&lt;/a&gt; classifies query shapes (point lookups, analytical aggregations, complex joins, full-table scans) and sends each to the optimal engine based on cost targets, latency requirements, and current table health. Applications connect to a stable SQL endpoint; backend engine selection adapts as conditions change, as table health improves, and as new engines are added to the pool.&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%2F6xvar2hj00js9a83zx0k.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%2F6xvar2hj00js9a83zx0k.png" alt=" " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because the control plane already has cross-engine telemetry and table health data, routing decisions use the same signals that drive compaction — creating a virtuous cycle. Better-compacted tables unlock more routing options (a well-sorted table can serve point lookups from DuckDB that would be too slow on an unsorted table). Better routing data informs compaction priorities (tables with high cross-engine query volume get priority optimization).&lt;/p&gt;

&lt;h3&gt;
  
  
  AI Readiness: Agent Access at Scale
&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%2Fndtarkl0w7qvye40c7wb.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%2Fndtarkl0w7qvye40c7wb.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI agents are becoming primary consumers of lakehouse data — not a future prediction, but a present reality in many production environments. They issue SQL iteratively, at high frequency, without human review of each query. They explore schemas, join across tables, aggregate at scale, and do it hundreds of times per analysis cycle. This creates requirements that traditional table maintenance does not address: structured schema discovery (agents need to understand table relationships programmatically), cost and safety guardrails (an agent with no limits will issue expensive full-table scans), and storage that self-optimizes for agent access patterns that may differ from human patterns.&lt;/p&gt;

&lt;p&gt;LakeOps provides &lt;a href="https://lakeops.dev/blog/iceberg-with-ai-agents" rel="noopener noreferrer"&gt;an agent-native MCP interface&lt;/a&gt; — any MCP-compatible agent (Claude, GPT, custom models) connects with zero integration code and auto-discovers available tools for schema discovery, health analysis, and query execution. Guardrails stack composably: ReadOnlyGuard prevents DDL and DML, CostEstimateGuard rejects queries that would scan more than a cost threshold, PIIMaskGuard scrubs sensitive columns before results reach the agent, HumanApprovalGuard gates high-stakes operations for human review. These are not all-or-nothing — you compose them per use case.&lt;/p&gt;

&lt;p&gt;Agent query telemetry feeds back into the compaction engine alongside human telemetry — the lake self-optimizes for the combined workload. If agents start heavily filtering on a column that humans rarely use, the sort order adapts to serve both access patterns. The lake becomes agent-ready not by adding a separate AI layer, but by extending the same optimization loop to a new class of consumers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Learning: The Closed Loop
&lt;/h3&gt;

&lt;p&gt;The final stage is what transforms a management tool into a system that improves with time and scale. Every maintenance operation produces measurable outcomes: file count reduction, scan volume improvement, query latency change, storage reclaimed, compute saved. These outcomes are not just metrics to display — they feed back into future decisions.&lt;/p&gt;

&lt;p&gt;Sort compaction that improved dashboard latency by 8x on one table informs strategy selection for structurally similar tables. Routing decisions that reduced costs without impacting latency reinforce the model for similar query shapes. Compaction schedules that were too aggressive for a table's write rate — causing frequent OCC conflicts — get automatically dialed back. Target file sizes that produced optimal performance on one storage class get recommended for similar tables.&lt;/p&gt;

&lt;p&gt;This is why a control plane gets more effective as the lake grows. More tables under management means more signal about which strategies work for which table shapes. More engines means richer telemetry about cross-engine access patterns. More queries means more precise sort-order recommendations. The operational burden stays flat (or decreases) while the optimization quality increases. The system scales better than a team can — because it learns from the entire lake, not just the tables one engineer happened to look at.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing Your Path
&lt;/h2&gt;

&lt;p&gt;Three approaches to managed data lakes exist in 2027. Each suits different architectural choices and organizational realities. The right one depends on your engine diversity, cloud strategy, and team structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vendor-Managed Platforms
&lt;/h3&gt;

&lt;p&gt;Databricks (Predictive Optimization, Liquid Clustering) and Snowflake (automatic clustering, table maintenance) bundle operational management inside their platforms. If you run exclusively on one vendor's stack — all compute through their engine, all tables in their catalog — this is the lowest-friction path. Maintenance is handled within the ecosystem, tuned for their engine's access patterns, and requires no additional tooling.&lt;/p&gt;

&lt;p&gt;The trade-off is architectural scope. Optimization runs only within that vendor's engine. If you later want to query those tables from Trino, DuckDB, or a different vendor — the core value proposition of choosing Iceberg in the first place — maintenance optimization will not account for those external access patterns. The vendor optimizes for its own queries. Cross-engine workloads are invisible to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud-Native Services
&lt;/h3&gt;

&lt;p&gt;AWS S3 Tables and Google BigLake provide managed Iceberg maintenance within their respective clouds. Compaction, cleanup, and optimization run automatically for tables registered with that service. No additional tooling needed for those tables.&lt;/p&gt;

&lt;p&gt;The trade-off is scope. Maintenance applies to tables within that cloud service only. If your lake spans multiple catalogs, clouds, or includes self-managed Iceberg tables outside S3 Tables or BigLake, you need a separate solution for the rest. Cross-engine query-aware optimization is not typically available because the service lacks visibility into query workloads from external engines — it knows what Athena does but not what Trino or Spark are doing on the same tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dedicated Control Plane
&lt;/h3&gt;

&lt;p&gt;A platform like &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; operates across your entire stack — any catalog, any engine, any cloud — because it layers onto existing infrastructure rather than replacing it. It provides the full closed loop (observe → decide → act → optimize → govern → route → enable → learn) without constraining architectural choices.&lt;/p&gt;

&lt;p&gt;The trade-off is that you add a service to your stack rather than relying on built-in capabilities. The benefit is that optimization accounts for your actual cross-engine workload, management applies lake-wide regardless of where tables live, policies are consistent across catalogs and clouds, and you retain full engine flexibility — add or remove engines without changing your management layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Choose Each
&lt;/h3&gt;

&lt;p&gt;The decision framework is practical:&lt;/p&gt;

&lt;p&gt;If you run 5–20 tables on a single engine from a single vendor, use that vendor's built-in management. The tooling is sufficient, it is already there, and the additional complexity of a separate service is not justified.&lt;/p&gt;

&lt;p&gt;If you run 50+ tables across multiple engines, multiple catalogs, or multiple clouds, you need a layer that spans your stack. No single vendor or cloud service has visibility into your complete workload. Cross-engine telemetry, unified policies, and lake-wide governance require a purpose-built operations layer.&lt;/p&gt;

&lt;p&gt;Many organizations run hybrid approaches — vendor-managed tables for workloads that live entirely within one ecosystem, and a dedicated control plane for the open multi-engine portion. The approaches coexist because they solve different scoping problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Economics
&lt;/h2&gt;

&lt;p&gt;Four cost vectors compound in an unmanaged lake. Left unchecked, each grows independently and they reinforce each other. They are also the four that &lt;a href="https://lakeops.dev/blog/iceberg-cost-optimization-2026" rel="noopener noreferrer"&gt;a properly managed lake eliminates&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage&lt;/strong&gt; drops 40–60% as orphan files, stale snapshots, and unreferenced data are continuously cleaned. This is not theoretical — it is the immediate, measurable result of running a proper orphan sweep and snapshot expiration. Production deployments routinely discover that a significant portion of their billable storage is serving no analytical purpose. The savings are recurring: every month you do not clean, the waste accumulates further.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query compute&lt;/strong&gt; drops as sort-aware compaction aligns data to real access patterns. When engines skip 80% of row groups via tight min/max statistics instead of scanning everything, compute consumption falls proportionally. Queries that scanned 100 GB to return 100 rows now scan 5 GB. The per-query savings compound across every query, every user, every dashboard refresh, every scheduled report — thousands of times per day across the organization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance compute&lt;/strong&gt; drops when a purpose-built Rust engine replaces Spark clusters for compaction workloads. No JVM startup overhead, no garbage collection pauses, no idle executor cores between compaction runs, no over-provisioning for peak load. Event-driven execution means compute runs only when needed (health signal fires) rather than on a fixed schedule (cluster running overnight regardless of whether any table needs attention).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engineering time&lt;/strong&gt; redirects from maintenance firefighting to building data products. Policies replace scripts. Observability replaces guesswork. Health classification replaces manual triage. On-call incidents for table degradation approach zero because degradation is prevented rather than repaired. The platform team stops being reactive (fixing things that broke) and becomes proactive (building capabilities that generate value).&lt;/p&gt;

&lt;p&gt;The combined effect for teams running at production scale: the lake becomes cheaper to operate as it grows larger. More data, more tables, more engines — but costs grow sublinearly because the automation handles the marginal complexity without marginal engineering investment. This is the opposite of what happens without management, where cost and complexity grow linearly (or worse) with lake size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&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%2F5uljk4evd5eex8v8l3em.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%2F5uljk4evd5eex8v8l3em.png" alt=" " width="800" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The practical path, regardless of where you sit on the maturity spectrum:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with visibility.&lt;/strong&gt; Connect your catalogs and see the health of every table before any maintenance runs. You will likely be surprised by how many tables are degraded — most teams discover Critical and Warning states on tables they thought were fine. The first insight alone (which tables are Critical, what orphan volume exists, where manifest bloat lives, which sort orders have drifted) changes how you prioritize the next sprint. This step costs nothing and requires no maintenance operations — it is purely diagnostic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix the highest-impact tables first.&lt;/strong&gt; You do not need to manage everything on day one. Identify the tables with the most file fragmentation, the highest query volume, or the largest orphan accumulation — ideally, tables that score high on all three. Run sequenced maintenance on those: expire, clean, compact, rewrite manifests. The ROI is immediate: dashboards that were slow become fast, storage bills drop measurably, and the team sees what managed looks like on real tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Establish policies early.&lt;/strong&gt; Define your retention windows, target file sizes, compaction strategies, and cleanup schedules — even if you start with manual or semi-automatic execution. These decisions become the governance layer when you move to full automation. A clear, documented policy also prevents the entropy that accumulates when ten engineers configure tables differently based on individual judgment. Consistency across the lake matters more than perfection on any single table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expand to full automation.&lt;/strong&gt; Once you trust the signals and see the impact on your initial tables, enable adaptive maintenance across the lake. Every table gets the right maintenance at the right frequency — coordinated, sequenced, conflict-aware, and fully observable. At this point, your platform team stops maintaining DAGs and scripts and starts building the data products and self-service capabilities that generate value for the organization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add routing and AI access last.&lt;/strong&gt; Multi-engine routing and agent access depend on consistently healthy tables and predictable data layouts. They are the payoff of the operational foundation — not a starting point. A routing layer cannot optimize what it cannot predict. An AI agent cannot reliably query tables whose performance is unpredictable. Get the foundation right first; the advanced capabilities become straightforward once table health is reliable.&lt;/p&gt;

&lt;p&gt;The constraint throughout: your data never leaves your storage. Your engines remain yours to choose and change. Your catalogs remain yours. The control plane adds operational intelligence without replacing anything in your stack — and if you ever remove it, everything continues working exactly as before, just without the automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Every production data lake faces the same operational reality: without an active system keeping tables healthy, Iceberg's open architecture degrades into slow queries, rising costs, and engineering time spent on maintenance instead of product work.&lt;/p&gt;

&lt;p&gt;The problem is not the format. Iceberg provides exactly what it promises — reliable, open, engine-agnostic tables. The operational layer that continuously compacts, cleans, sorts, monitors, governs, and optimizes must exist separately. It must be built or adopted.&lt;/p&gt;

&lt;p&gt;Netflix and Google proved the architecture works by building it internally over years with dedicated teams. In 2027, the same architecture is available without that investment. The approaches range from vendor-managed (Databricks, Snowflake) to cloud-native (S3 Tables, BigLake) to dedicated control planes — each fitting a different point on the architectural complexity spectrum.&lt;/p&gt;

&lt;p&gt;The question is not whether your lake needs management — the degradation mechanics are inherent to how Iceberg works at scale. The question is which path fits your architecture, engine diversity, and team structure.&lt;/p&gt;

&lt;p&gt;For teams running open, multi-engine Iceberg lakehouses at production scale, &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; is the control plane that closes the operational loop — across any engine, catalog, and cloud. Connect your existing infrastructure, define your policies, and let the lake manage itself.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lakeops.dev/blog/intelligent-lakehouse-like-netflix" rel="noopener noreferrer"&gt;Intelligent Lakehouse: Build Like Netflix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lakeops.dev/blog/managed-iceberg-2026" rel="noopener noreferrer"&gt;The Nine Components of a Modern Iceberg Control Plane&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lakeops.dev/blog/from-databricks-snowflake-to-an-open-data-platform" rel="noopener noreferrer"&gt;From Databricks and Snowflake to an Open Data Platform&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>datalake</category>
      <category>iceberg</category>
      <category>dataengineering</category>
      <category>lakehouse</category>
    </item>
    <item>
      <title>Data Lakehouse: How and Why</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Wed, 19 Aug 2026 14:09:23 +0000</pubDate>
      <link>https://dev.to/jonisar/data-lakehouse-how-and-why-aco</link>
      <guid>https://dev.to/jonisar/data-lakehouse-how-and-why-aco</guid>
      <description>&lt;p&gt;&lt;em&gt;How the data lakehouse works in production — the internal mechanics, the data flow patterns, why tables degrade under real workloads, and the closed-loop control plane that keeps the architecture performing at scale.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a data lakehouse solves
&lt;/h2&gt;

&lt;p&gt;Every enterprise data platform eventually hits the same wall. Analytical data exists in two places: a data lake (cheap, scalable, ungoverned) and a data warehouse (expensive, fast, governed). Between them sits an ETL pipeline that copies, transforms, and reconciles — creating a permanent tax on engineering time, data freshness, and infrastructure spend.&lt;/p&gt;

&lt;p&gt;The symptoms are familiar. Dashboards show yesterday's numbers because the warehouse loads overnight. ML models train on stale copies because the warehouse does not support Python-native access. Storage bills grow because the same curated datasets live in both systems simultaneously. Governance fragments because access controls, lineage, and audit trails cannot span two architecturally different platforms.&lt;/p&gt;

&lt;p&gt;The data lakehouse resolves this by collapsing both systems into one: &lt;strong&gt;transactional guarantees applied directly to data on object storage&lt;/strong&gt;. One copy. One governance model. Any engine — SQL, ML, streaming, AI — reads and writes the same tables with full ACID safety. The warehouse is not replaced by a different product; the lake &lt;em&gt;becomes&lt;/em&gt; the warehouse through a metadata innovation called a table format.&lt;/p&gt;

&lt;p&gt;The practical result: BI dashboards, Spark ML pipelines, Flink streaming jobs, and autonomous AI agents all query the same governed tables — without ETL between them, without stale copies, without paying warehouse markup on storage that costs $0.023/GB/month in its native form.&lt;/p&gt;

&lt;h2&gt;
  
  
  The internal mechanics: how a lakehouse actually works
&lt;/h2&gt;

&lt;p&gt;A data lakehouse is not a monolithic system. It is four cooperating layers — each handling a specific responsibility, each independently evolvable. Understanding how they interact is the foundation for building one that performs in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  The storage layer
&lt;/h3&gt;

&lt;p&gt;All data resides on cloud object storage — S3, GCS, or ADLS. This is where the economics originate. Object storage provides eleven-nines durability at $20–25 per TB per year. A petabyte of analytical data costs roughly $20,000/year to persist — compared to $500,000+ when that same data sits inside a traditional warehouse with bundled compute.&lt;/p&gt;

&lt;p&gt;Storage is decoupled from everything above it. Shut down every query engine and the data remains. Add a new engine tomorrow — it reads from the same bucket. Move clouds — the files copy byte-for-byte with no format conversion.&lt;/p&gt;

&lt;p&gt;Data lands as &lt;strong&gt;Apache Parquet&lt;/strong&gt; files — a columnar format that stores values by column rather than by row. A query touching 5 columns from a 300-column table physically reads only those 5 columns. Parquet also embeds per-column min/max statistics in each file header, enabling query engines to skip entire files whose value ranges cannot satisfy the WHERE clause. This skip-scanning is what makes lakehouse queries competitive with warehouse queries — but only when files are properly sized and organized.&lt;/p&gt;

&lt;h3&gt;
  
  
  The table format layer
&lt;/h3&gt;

&lt;p&gt;A directory of Parquet files on S3 is a data lake — readable, but not transactional. The table format is the metadata innovation that adds the missing guarantees:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache Iceberg&lt;/strong&gt; — the dominant format as of 2026, supported natively by Spark, Trino, Flink, DuckDB, Snowflake, Databricks, Athena, and StarRocks — provides ACID commits, schema evolution, time travel, partition evolution, and concurrent multi-writer safety through a metadata tree architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the metadata tree works:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each Iceberg table maintains a hierarchy of metadata files on the same object storage as the data:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A metadata pointer&lt;/strong&gt; (tracked by the catalog) — points to the current table version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot files&lt;/strong&gt; — each representing the table's complete state at a point in time. Every write creates a new snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifest lists&lt;/strong&gt; — each snapshot references a manifest list that indexes the active manifests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifests&lt;/strong&gt; — each tracking a batch of data files with per-file statistics (path, partition values, row count, column min/max)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data files&lt;/strong&gt; — the actual Parquet files containing rows&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This tree enables two properties that make the lakehouse viable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Atomic commits.&lt;/strong&gt; Every write produces new data files and a new snapshot. The catalog atomically swaps the pointer from old snapshot to new. Readers always see a consistent state. Failed writes leave no trace — the pointer was never advanced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statistical pruning.&lt;/strong&gt; Query engines read manifests (kilobytes) to determine which data files (gigabytes) are relevant. A query filtering on &lt;code&gt;country = 'DE'&lt;/code&gt; checks manifest-level statistics and skips every file whose country column's min/max range excludes 'DE'. No data read. No I/O wasted. This is why lakehouse queries can match warehouse speed — but only when the metadata is current and the data layout aligns with query patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The critical implication:&lt;/strong&gt; every commit grows the tree. Every streaming write adds files. Every mutation adds delete markers. The tree that enables fast queries also accumulates entropy that &lt;em&gt;degrades&lt;/em&gt; query speed over time — unless something actively maintains it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The catalog layer
&lt;/h3&gt;

&lt;p&gt;The catalog is the coordination service that answers: where is the current metadata pointer for each table, and how do multiple engines safely coordinate writes?&lt;/p&gt;

&lt;p&gt;Every engine — Spark, Trino, Flink, DuckDB — consults the catalog before each read (to locate current metadata) and during each write (to commit atomically). Without the catalog, concurrent writers could corrupt table state. With it, they race safely: one commits, the other retries against updated state.&lt;/p&gt;

&lt;p&gt;Modern lakehouses use the &lt;strong&gt;Iceberg REST Catalog specification&lt;/strong&gt; — a standard HTTP API that every engine implements. This means adding a new engine is a configuration change (point it at the catalog URL), not an integration project. Catalog options include managed services (AWS Glue), open-source servers (Apache Polaris, Lakekeeper), federated solutions (Apache Gravitino), and Git-branching models (Project Nessie) — all speaking the same REST protocol.&lt;/p&gt;

&lt;p&gt;The catalog is also where access control, retention policies, and audit logging materialize. It is the governance anchor for multi-engine environments where every engine must see consistent permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The compute layer
&lt;/h3&gt;

&lt;p&gt;The lakehouse separates compute from storage, enabling &lt;strong&gt;multiple specialized engines&lt;/strong&gt; to operate on the same data concurrently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spark&lt;/strong&gt; — batch ETL, ML training, complex multi-stage transformations. The workhorse for heavy writes and joins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trino&lt;/strong&gt; — interactive SQL with sub-second response times. Excels at dashboard queries and ad-hoc exploration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flink&lt;/strong&gt; — streaming ingestion with exactly-once semantics. Continuous CDC from operational databases into lakehouse tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DuckDB&lt;/strong&gt; — embedded analytics for notebooks, CI/CD pipelines, and single-node processing. Zero infrastructure overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake / Athena / StarRocks&lt;/strong&gt; — managed engines for specific workload profiles (high-concurrency BI, serverless ad-hoc, real-time OLAP).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each engine discovers tables through the same REST catalog and reads/writes through Iceberg's transactional protocol. They coexist safely on the same tables through snapshot isolation. This is the key architectural advantage over the warehouse model: instead of one vendor's engine for everything, you use the best tool for each workload shape.&lt;/p&gt;

&lt;p&gt;But multi-engine access multiplies operational pressure. More engines means more write patterns, more file fragmentation profiles, and more divergent query patterns competing for the same table's physical layout — creating the operational challenge that defines production lakehouse management.&lt;/p&gt;

&lt;h2&gt;
  
  
  How data flows through a production lakehouse
&lt;/h2&gt;

&lt;p&gt;Theory aside — how does data actually move from source systems through the lakehouse to consumers? The &lt;strong&gt;medallion architecture&lt;/strong&gt; (bronze → silver → gold) is the standard pattern for organizing this flow, and each layer creates different operational characteristics that matter for long-term health.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bronze: the raw capture layer
&lt;/h3&gt;

&lt;p&gt;Source data lands in its original form. CDC events from PostgreSQL (captured by Debezium, written by Flink). Clickstream events from Kafka. Daily batch exports from SaaS APIs. Partner file drops. Everything arrives as append-only, schema-on-read, immutable records.&lt;/p&gt;

&lt;p&gt;Bronze is the replay source. When silver-layer transformation logic has a bug discovered months later, you rebuild from bronze. When a new use case requires fields that were previously ignored, they are already preserved in bronze. This immutability is non-negotiable — teams that apply transformations at ingestion lose the ability to retrospectively correct logic errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common bronze ingestion patterns:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streaming CDC&lt;/strong&gt; — Debezium captures row-level changes (inserts, updates, deletes) from PostgreSQL, MySQL, or MongoDB. Flink consumes these events and writes them to Iceberg tables with exactly-once semantics at sub-minute commit intervals. Each commit is small (a few thousand rows) but creates a new data file and a new snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event streams&lt;/strong&gt; — Application events (clickstreams, transactions, IoT sensor data, ad impressions) flow from Kafka topics into Flink or Spark Structured Streaming jobs that write to Iceberg. Event volumes are high and bursty — Black Friday traffic spikes create 10x the normal file accumulation rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch loads&lt;/strong&gt; — Daily or hourly exports from ERP systems, CRM platforms, and third-party APIs. These produce larger files but less frequently. Batch tables fragment slower but still accumulate snapshot overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File drops&lt;/strong&gt; — Partners or internal systems deposit CSV/JSON/Parquet files in S3. A detection pipeline validates schema, converts to Iceberg-managed Parquet, and registers the files in the catalog.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What this means operationally:&lt;/strong&gt; Streaming CDC with 5-minute commits creates approximately 8,600 new files per table per month. A lakehouse ingesting from 20 operational databases at sub-minute latency accumulates hundreds of thousands of files across its bronze layer within weeks. Individual file sizes average 5–20 MB — far below the 256–512 MB target where engines perform optimally. File count — not data volume — is what degrades query planning speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Silver: the governed truth layer
&lt;/h3&gt;

&lt;p&gt;Silver transforms bronze into a data model that the organization commits to: one row per entity, resolved foreign keys, deduplicated events, enforced types, quarantined bad records. Silver is not a cleanup step — it is an architectural commitment to a domain model that all downstream consumers depend on. If a table exists because one dashboard needs a specific aggregation, it belongs in gold, not silver.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key transformations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deduplication&lt;/strong&gt; — Late-arriving records, duplicate events from at-least-once delivery, and replay scenarios resolved to a single authoritative version. Deduplication runs as MERGE INTO with match conditions, producing delete markers for superseded records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type enforcement and validation&lt;/strong&gt; — Strings cast to proper types. Nulls validated against business rules. Referential integrity checked against dimension tables. Records failing validation quarantined to error tables with reason codes — never silently dropped, never propagated downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slowly changing dimensions (SCD Type 2)&lt;/strong&gt; — History tracked for entities that evolve over time. When a customer's tier changes, the old row is closed (end_date set) and a new row opened. This produces update operations on every change event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference data enrichment&lt;/strong&gt; — Raw IDs joined to meaningful attributes. Country codes resolved to names. Product SKUs expanded to categories, brands, and pricing tiers. Enrichment creates wider rows with higher value per query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal alignment&lt;/strong&gt; — Events from different sources (orders, payments, shipments) aligned to consistent time zones, granularity, and business calendars.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Silver tables are the enterprise source of truth. Every downstream consumer — dashboards, ML models, AI agents, compliance reports — starts from the same clean, governed layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means operationally:&lt;/strong&gt; Silver tables receive MERGE INTO operations — upserts that create delete markers (position-delete files or V3 deletion vectors) that every subsequent read must reconcile. A CDC table receiving 50,000 updates per hour generates thousands of delete markers per day. Query engines must read both the data files and the delete files, reconcile which rows are current, and filter accordingly — adding I/O overhead that increases linearly with delete-file count. Without periodic resolution of these markers, query latency creeps 2–5x higher while the table reports the same row count and appears unchanged to basic monitoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gold: the consumer-optimized layer
&lt;/h3&gt;

&lt;p&gt;Gold tables exist for performance: pre-aggregated metrics for dashboards, denormalized feature tables for ML training, materialized snapshots for compliance reporting. Gold trades storage redundancy for read speed — accepting wider tables and duplicated columns to eliminate joins at query time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gold layer patterns:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BI aggregations&lt;/strong&gt; — Daily/weekly/monthly rollups pre-joined by all relevant dimensions. A dashboard that previously ran a 30-second join across 5 silver tables now reads a single pre-materialized gold table in under 1 second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ML feature tables&lt;/strong&gt; — Wide, denormalized tables with one row per entity and all features materialized. Training jobs read a single scan rather than orchestrating multi-table joins. Feature freshness controlled by the gold refresh cadence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance snapshots&lt;/strong&gt; — Point-in-time materializations capturing exactly what was known at a specific date. Regulatory filings, board reports, and audit evidence stored as immutable gold snapshots with indefinite retention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serving tables&lt;/strong&gt; — Pre-computed results for user-facing analytics (product recommendations, customer health scores, real-time dashboards) with sub-second SLA requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What this means operationally:&lt;/strong&gt; Gold tables are typically overwritten on a schedule — daily, hourly, or triggered by upstream freshness signals. Each OVERWRITE operation atomically replaces the table's content: it writes new data files, creates a new snapshot pointing to them, but leaves the previous snapshot's files in storage until explicitly expired. A daily gold refresh creates 365 snapshots per year. At 50 GB per refresh, that is 18 TB of stale data files persisting in storage — even though only the latest 50 GB is logically current. Without snapshot lifecycle management, storage grows linearly forever from data that was replaced weeks or months ago.&lt;/p&gt;

&lt;h3&gt;
  
  
  The maintenance gradient
&lt;/h3&gt;

&lt;p&gt;The three layers create a gradient of operational need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bronze&lt;/strong&gt; → small-file pressure (compaction priority)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silver&lt;/strong&gt; → delete-file pressure (merge resolution priority)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gold&lt;/strong&gt; → stale-snapshot pressure (expiration priority)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No uniform maintenance schedule handles this correctly. Each table needs the &lt;em&gt;right&lt;/em&gt; operation at the &lt;em&gt;right&lt;/em&gt; cadence — determined by its ingestion velocity, mutation pattern, and query load. This is the insight that leads directly to the control plane model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the lakehouse unlocks: multiple workloads, one platform
&lt;/h2&gt;

&lt;p&gt;The economic case (10–50x cheaper storage) is straightforward. The architectural case is more powerful: &lt;strong&gt;eliminating the boundaries between workload types&lt;/strong&gt; that the two-system model enforced.&lt;/p&gt;

&lt;h3&gt;
  
  
  BI and reporting
&lt;/h3&gt;

&lt;p&gt;Dashboards query silver and gold tables directly through Trino or Snowflake. No warehouse sync delay. No overnight ETL window creating stale numbers. Schema changes in silver propagate to consumers without separate migration scripts — because the table format handles schema evolution as a metadata-only operation.&lt;/p&gt;

&lt;p&gt;Time travel lets analysts reproduce the exact state of any report at any prior date. In regulated industries (financial services, healthcare, insurance), "what did we report last quarter" must be answerable precisely and auditably. The lakehouse provides this natively through snapshot pinning — no separate archival infrastructure required.&lt;/p&gt;

&lt;p&gt;Performance depends on table health: well-compacted tables with accurate column statistics and sort-order alignment with dashboard filter patterns enable engines to prune 90%+ of files on selective queries. When a BI dashboard filters on &lt;code&gt;region = 'EMEA' AND quarter = 'Q2'&lt;/code&gt;, a properly maintained table skips every file that cannot contain matching rows. A degraded table forces a full scan regardless of how selective the filter is. This is why maintenance directly impacts end-user experience — and why it cannot be deferred.&lt;/p&gt;

&lt;h3&gt;
  
  
  ML and feature engineering
&lt;/h3&gt;

&lt;p&gt;ML pipelines read training data from the same governed tables that power dashboards — eliminating training-serving skew, the most common source of ML model degradation in production. When the training pipeline and the serving pipeline read from different copies (one from the warehouse, one from the lake), subtle differences in transformation logic cause the model to encounter data distributions in production that it never saw during training. The lakehouse eliminates this by providing one table for both.&lt;/p&gt;

&lt;p&gt;Experiment reproducibility comes free: pin to a snapshot ID, retrain on byte-for-byte identical data months later. Feature freshness improves because features and serving data live in the same transactional system — no nightly export, no stale copies, no drift between what the model trained on and what it serves against.&lt;/p&gt;

&lt;p&gt;Feature stores built on lakehouse tables inherit versioning, access control, lineage, and time-travel capabilities from the table format. Feature computation runs as standard Spark or Flink jobs writing to Iceberg tables — no separate feature-store infrastructure, no proprietary feature format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming analytics
&lt;/h3&gt;

&lt;p&gt;Flink and Spark Structured Streaming write directly to lakehouse tables with exactly-once transactional guarantees. Near-real-time dashboards and alerting read from the same tables — no separate streaming infrastructure, no Kafka-to-materialized-view pipeline, no divergent dual-path architecture.&lt;/p&gt;

&lt;p&gt;The same transformation logic runs as streaming (for freshness) or batch (for cost) by changing only the trigger configuration. This eliminates the lambda architecture's core problem: maintaining two parallel pipelines (one batch, one stream) with identical logic that inevitably drift apart. In the lakehouse model, there is one pipeline with a tunable latency/cost knob.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI agents as first-class consumers
&lt;/h3&gt;

&lt;p&gt;In 2026, AI agents autonomously discover schemas, formulate analytical queries, and feed results into multi-step reasoning chains. The lakehouse is their knowledge infrastructure — providing structured, governed, queryable data that agents access through standard protocols.&lt;/p&gt;

&lt;p&gt;The requirements for agent access differ from human access: agents generate unpredictable query patterns, cannot diagnose slow responses caused by table degradation, and burn tokens (and money) on retries when they hit poorly-maintained tables. They also need guardrails — limits on what they can read, how much they can scan, and what they can modify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why lakehouses degrade: the operational reality
&lt;/h2&gt;

&lt;p&gt;Here is what most guides skip. The four layers give you the architecture. They do not give you a system that &lt;em&gt;stays healthy&lt;/em&gt;. Every production lakehouse — without exception — degrades over time unless something actively maintains it. This is not a flaw; it is a consequence of how append-only transactional systems work.&lt;/p&gt;

&lt;h3&gt;
  
  
  The mechanics of degradation
&lt;/h3&gt;

&lt;p&gt;Iceberg never modifies files in place. Every write creates new files. Every snapshot preserves a complete view. This is what provides transactional safety — but it means the system accumulates structural debt continuously:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File fragmentation.&lt;/strong&gt; Each commit adds files. Streaming ingestion at 5-minute intervals into a table receiving 2 GB/hour creates ~8,600 files per month. Each file averages 12 MB — well below the 256–512 MB target where query engines perform optimally. The engine must open, plan, and coordinate reads across 100x more files than necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Planning overhead.&lt;/strong&gt; Engine query planners traverse the manifest tree to identify relevant files. At 1,000 files, planning takes milliseconds. At 50,000 files, planning takes 10–20 seconds — sometimes exceeding the query execution itself. The metadata hierarchy that enables fast pruning becomes a bottleneck when it indexes too many undersized files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete-file accumulation.&lt;/strong&gt; Every UPDATE and DELETE writes markers that subsequent readers must reconcile against data files at read time. A heavily-mutated table (CDC, SCD, late-arriving corrections) accumulates thousands of delete markers. Query latency increases 2–5x while the table's row count and size appear unchanged — making degradation invisible to basic monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage waste.&lt;/strong&gt; Expired logical content leaves physical files behind. Orphaned files from failed writes, aborted compaction, and expired snapshots accumulate in storage. Without active cleanup, production lakehouses accumulate terabytes of unreferenced files — pure cost with no analytical value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compounding pressure.&lt;/strong&gt; These four forces are not independent — they compound. More files means larger manifests. Larger manifests means slower planning. Slower planning means longer maintenance operations. Longer maintenance operations means less frequent runs. Less frequent runs means more file accumulation. The system has a natural tendency toward runaway degradation that accelerates once it begins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why manual maintenance does not scale
&lt;/h3&gt;

&lt;p&gt;The instinct is to write scripts: a Spark job that compacts every table nightly, a cron that expires old snapshots weekly. This works for 5–10 tables. It fails at production scale for structural reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tables need different cadences.&lt;/strong&gt; A streaming table needs compaction hourly. A daily-refresh table needs it weekly. A slowly-growing dimension needs it monthly. One schedule cannot serve all three without either wasting compute on healthy tables or neglecting degraded ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operations have dependencies.&lt;/strong&gt; Compacting files that will be expired next hour is wasted work. Rewriting manifests before compaction invalidates the rewrite. Computing statistics on pre-compaction layouts produces stale metadata. The five maintenance operations must run in a specific sequence — and re-sequence when conditions change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance competes with production.&lt;/strong&gt; Running compaction on the same Spark cluster that serves production queries creates resource contention. Running it concurrently with active writers on the same table causes commit conflicts. Scheduling around production windows requires coordination that scripts cannot perform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static thresholds drift.&lt;/strong&gt; "Compact when file count exceeds 1,000" is reasonable for one table's current state. But partition cardinality changes, ingestion velocity shifts, query patterns evolve. The threshold that was correct last month is wrong today — and nobody adjusts it until the table is already degraded.&lt;/p&gt;

&lt;h2&gt;
  
  
  The control plane: a closed-loop system
&lt;/h2&gt;

&lt;p&gt;The answer is not better scripts or more sophisticated cron. It is an architecturally different approach: a &lt;strong&gt;control plane&lt;/strong&gt; that operates the lakehouse as a closed-loop system — continuously sensing table state, reasoning about what each table needs, executing the right operations in the right sequence, and learning from outcomes to improve future decisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ┌──────────────────────────────────────────────────────┐
    │                                                      │
    │   SENSE  →  ASSESS  →  PLAN  →  EXECUTE  →  LEARN   │
    │     ↑                                         │      │
    │     └─────────────────────────────────────────┘      │
    │                (continuous per table)                 │
    │                                                      │
    └──────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the mental model for &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; — an autonomous &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;control plane for data lakehouses&lt;/a&gt; that implements this closed loop across every table in your lake, continuously, without cron jobs or 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp2eme0qkyc3yl2w4lagr.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%2Fp2eme0qkyc3yl2w4lagr.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sense: collecting structural telemetry
&lt;/h3&gt;

&lt;p&gt;The control plane continuously reads metadata from every connected catalog — file counts per partition, file size distributions, manifest depth, snapshot velocity, delete-file ratios, partition-level skew, and cross-engine query patterns. This is metadata inspection, not data access — the control plane never reads or moves your actual data.&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%2Fk95nu2oj6h39n10jeavi.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%2Fk95nu2oj6h39n10jeavi.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sensing is passive and lightweight. It adds no load to production engines. It works across catalogs (Polaris, Glue, Gravitino, Nessie, Lakekeeper) and across engines (Spark, Trino, Flink, DuckDB, Snowflake, Athena) — aggregating a unified view that no single component can provide alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Assess: health classification
&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%2Fugj9xl32jpwj5j0adbiu.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%2Fugj9xl32jpwj5j0adbiu.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Raw telemetry becomes actionable through &lt;strong&gt;health scoring&lt;/strong&gt;. Every table is continuously classified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Healthy&lt;/strong&gt; — file sizes within target, manifests compact, delete ratios low, sort order aligned with query patterns. No action needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning&lt;/strong&gt; — metrics drifting toward thresholds. File count growing. Delete ratio increasing. Not yet impacting queries — but will if left unaddressed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Critical&lt;/strong&gt; — active degradation impacting performance. Planning latency elevated. Read amplification high. Immediate maintenance required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thresholds are not static defaults. They account for each table's partition cardinality, ingestion velocity, engine mix, and historical maintenance response — adapting automatically as workloads evolve.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/solutions/observability" rel="noopener noreferrer"&gt;LakeOps observability&lt;/a&gt; surfaces this classification as a lake-wide dashboard: how many tables are healthy, warning, critical — with drill-down into specific metrics, proactive insights at severity levels, and cross-engine telemetry that reveals optimization opportunities no single engine can see.&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%2F9m0ukmbs7z22uuv1qk67.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%2F9m0ukmbs7z22uuv1qk67.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Plan: sequenced maintenance decisions
&lt;/h3&gt;

&lt;p&gt;For each table classified as Warning or Critical, the control plane determines &lt;strong&gt;which operations&lt;/strong&gt; to run and &lt;strong&gt;in what order&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%2Fmqrxk6d7g1bh6hg0ty0v.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%2Fmqrxk6d7g1bh6hg0ty0v.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sequencing is not arbitrary — it follows a dependency chain where each step's output becomes the next step's input:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 → Expire snapshots.&lt;/strong&gt; Remove snapshots beyond the retention policy. This dereferences files that are no longer needed — reducing the scope for subsequent operations and preventing compaction from processing files about to be garbage-collected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 → Remove orphan files.&lt;/strong&gt; Delete unreferenced files older than the safety window (default: 7 days, configurable). This captures files newly dereferenced by expiration, plus accumulated waste from failed writes and aborted operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 → Compact data files.&lt;/strong&gt; Merge small files into optimally-sized ones. Resolve delete markers so subsequent reads skip reconciliation overhead. Optionally sort data by query-relevant columns to enable statistical pruning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 → Rewrite manifests.&lt;/strong&gt; Consolidate the manifest tree to reflect the new compacted layout. After compaction reduces file count from 50,000 to 2,000, the manifest tree should reflect 2,000 files — not carry forward metadata from the old layout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 → Compute statistics.&lt;/strong&gt; Refresh Puffin column statistics (distinct values, null counts, extended min/max) on the compacted files. This ensures every engine benefits from accurate metadata for pruning decisions.&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%2Fxwje8njtnykgu64z7ioo.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%2Fxwje8njtnykgu64z7ioo.png" alt=" " width="799" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/solutions/managed-iceberg" rel="noopener noreferrer"&gt;LakeOps managed maintenance&lt;/a&gt; runs this five-step pipeline as a coordinated sequence per table — respecting dependencies, avoiding conflicts with active writers, and targeting only the partitions that need work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Execute: fast, conflict-aware action
&lt;/h3&gt;

&lt;p&gt;The control plane executes maintenance on a &lt;strong&gt;purpose-built engine&lt;/strong&gt; rather than borrowing production compute:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/solutions/compaction" rel="noopener noreferrer"&gt;LakeOps compaction&lt;/a&gt; runs on a dedicated Rust binary powered by Apache DataFusion — not Spark. The practical differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed:&lt;/strong&gt; A 1 TB TPC-DS compaction benchmark completes in under 4 minutes — roughly 29x faster than equivalent Spark operations on the same data. This speed means maintenance runs in minutes rather than hours, so tables never accumulate enough debt to degrade between cycles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; Sub-$5 per TB compacted — roughly 90% cheaper than running equivalent Spark clusters. This economic efficiency is what makes &lt;em&gt;continuous&lt;/em&gt; compaction viable rather than forcing teams into overnight-only windows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability:&lt;/strong&gt; Bounded memory with graceful spill-to-disk. No JVM garbage collection pauses. No OOM crashes on large tables. No cluster provisioning delay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict safety:&lt;/strong&gt; Aware of active writers and in-flight transactions. Never compacts partitions with concurrent writes. Never expires snapshots that active readers depend on.&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%2Fm98evplbjem6xk3evn4v.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%2Fm98evplbjem6xk3evn4v.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Intelligent sort: learned from production queries
&lt;/h4&gt;

&lt;p&gt;Beyond file consolidation, LakeOps compaction physically &lt;strong&gt;sorts&lt;/strong&gt; data by the columns that production queries actually filter on — learned automatically from cross-engine telemetry.&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%2Fm6e0ethgmcsldgf4ilgr.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%2Fm6e0ethgmcsldgf4ilgr.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the highest-leverage optimization in the entire stack. When data within each file spans a narrow value range for the sort columns, statistical pruning becomes surgical: the engine checks file-level min/max metadata and eliminates 90%+ of files before reading any data. The difference between an unsorted table (full scan on every query) and a properly sorted table (targeted reads on 5–10% of files) is typically 8–12x in query speed.&lt;/p&gt;

&lt;p&gt;The key insight is that &lt;strong&gt;the right sort order is not static&lt;/strong&gt;. Trino queries might filter on &lt;code&gt;customer_id&lt;/code&gt; and &lt;code&gt;order_date&lt;/code&gt;. Spark ML pipelines might filter on &lt;code&gt;event_type&lt;/code&gt; and &lt;code&gt;region&lt;/code&gt;. AI agents might filter on combinations nobody predicted. No single engine has the full picture of how the table is accessed. The control plane aggregates telemetry from all engines and computes the optimal sort strategy for the combined workload — updating it automatically as patterns shift 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%2Foy7zmkkkhrw9s3ovfgnb.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%2Foy7zmkkkhrw9s3ovfgnb.png" alt=" " width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Learn: outcome-driven improvement
&lt;/h3&gt;

&lt;p&gt;After each maintenance cycle, the control plane measures outcomes against expectations. Did file count reach the target? Did planning latency improve? Did the health score advance? Results feed back into the assessment model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tables that responded well to sort compaction on specific columns get those columns reinforced in future cycles&lt;/li&gt;
&lt;li&gt;Tables where expiration freed significant storage get more aggressive retention recommendations&lt;/li&gt;
&lt;li&gt;Tables where manifest rewriting dramatically improved planning get lower rewrite thresholds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This learning loop means the control plane improves over time — each cycle's outcomes inform the next cycle's decisions. Tables with unusual characteristics (very high partition cardinality, extremely bursty ingestion, mixed engine access) get customized strategies without manual configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance: policies that run themselves
&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%2F634d9v8czennky99hwoa.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%2F634d9v8czennky99hwoa.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Visibility without enforcement is just monitoring. The control plane turns observability into &lt;strong&gt;automated governance&lt;/strong&gt; through declarative policies:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/blog/rise-of-open-apache-lakehouse" rel="noopener noreferrer"&gt;LakeOps governance&lt;/a&gt; supports policies at multiple scopes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Organization-wide defaults&lt;/strong&gt; — baseline compaction targets, snapshot retention, orphan cleanup schedules applied to every table unless overridden&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Namespace-level rules&lt;/strong&gt; — production namespaces get aggressive maintenance; staging gets relaxed thresholds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-table exceptions&lt;/strong&gt; — compliance tables with 365-day retention; hot tables with 15-minute compaction cadence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Policies inherit downward: new tables automatically receive the governance rules of their namespace. No manual configuration per table. No tables falling through the cracks because someone forgot to add them to the maintenance script.&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%2Ftyiynclhjuor0zki6oxg.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%2Ftyiynclhjuor0zki6oxg.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every policy execution is logged with full audit trail — what ran, when, what changed, duration, bytes before/after. This is critical for compliance environments where "prove your data lifecycle management" is a regulatory requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query routing: making multi-engine economically rational
&lt;/h2&gt;

&lt;p&gt;Having multiple engines on the same data is architecturally elegant. But without routing intelligence, it becomes accidentally expensive: queries dispatched to the wrong engine pay the wrong pricing model and get the wrong performance profile.&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%2Fbdj5yttufa6vxs8f26yk.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%2Fbdj5yttufa6vxs8f26yk.png" alt=" " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A point lookup for 100 rows dispatched to Spark burns 30 seconds of cluster startup. That same query on DuckDB resolves in 0.3 seconds. A dashboard query running on Snowflake at $2/credit is 10x more expensive than the same query on a self-hosted Trino cluster. A full-table scan on DuckDB crashes; on Spark, it completes in minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/solutions/query-routing" rel="noopener noreferrer"&gt;LakeOps query routing&lt;/a&gt; provides a single SQL endpoint that dispatches queries to the optimal engine based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workload type&lt;/strong&gt; — define routing groups (BI/Reporting → cost-optimized, Data Science → performance-optimized, ETL → throughput-optimized) with different backend engine pools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table health state&lt;/strong&gt; — well-compacted tables are eligible for lightweight engines (DuckDB, Athena). Fragmented tables route to engines that tolerate planning overhead (Spark).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost/latency targets&lt;/strong&gt; — constraints per group that the router respects (under $0.05/query for BI, under 2s latency for dashboards)&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%2Frpv8zc2jn0qos73adcf8.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%2Frpv8zc2jn0qos73adcf8.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The routing layer and the maintenance layer form a reinforcing loop: as the control plane compacts and sorts tables, more engines become eligible per query shape. More routing options mean lower per-query cost. Lower cost justifies more frequent querying. More frequent querying generates better telemetry for compaction decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI agent enablement: the lakehouse as knowledge infrastructure
&lt;/h2&gt;

&lt;p&gt;In 2026, AI agents are not just consumers of pre-built reports — they autonomously explore data, formulate queries, and feed results into reasoning chains. The lakehouse becomes the knowledge layer that agents operate on.&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%2F9y58vun5yz7hc7ln6m1c.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%2F9y58vun5yz7hc7ln6m1c.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This creates specific requirements that a control plane addresses:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access interface.&lt;/strong&gt; &lt;a href="https://lakeops.dev/blog/mcp-iceberg-lakeops" rel="noopener noreferrer"&gt;LakeOps provides an MCP-native interface&lt;/a&gt; (Model Context Protocol) with PostgreSQL, MySQL, and Arrow Flight wire compatibility. Any MCP-compatible agent discovers catalogs, browses schemas, executes queries, and receives results — without custom integration per agent framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layered guardrails.&lt;/strong&gt; Agents need boundaries. The control plane enforces per-session constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ReadOnly&lt;/strong&gt; — blocks DDL and DML; agents explore but cannot modify&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CostEstimate&lt;/strong&gt; — rejects queries whose estimated scan exceeds configurable thresholds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIIMask&lt;/strong&gt; — hashes sensitive columns before results reach the model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HumanApproval&lt;/strong&gt; — pauses high-stakes operations for human review before execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Closed-loop optimization.&lt;/strong&gt; Agent query telemetry feeds back into the control plane's sort and compaction decisions. Tables that agents query heavily get optimized for agent access patterns. The lake adapts to AI workloads without manual intervention — queries get faster as agents use the system more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing: the practical sequence
&lt;/h2&gt;

&lt;p&gt;You do not need the full architecture on day one. Each step delivers independent value and sets up the next:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Start with Iceberg.&lt;/strong&gt; Choose Apache Iceberg for all new analytical tables. The ecosystem support is universal — every major engine reads and writes it natively. Existing Delta Lake tables interoperate through UniForm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Deploy a REST catalog.&lt;/strong&gt; This is the coordination point — get it right early. AWS Glue for managed simplicity. Self-hosted Polaris for full control. Gravitino if you need to federate across existing catalogs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Build the ingestion layer (bronze).&lt;/strong&gt; CDC from operational databases via Flink. Event streams from application telemetry. Batch loads from external sources. Keep it append-only and immutable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Build the semantic layer (silver).&lt;/strong&gt; Domain modeling, deduplication, quality enforcement. This becomes the organization's governed truth — every downstream consumer starts here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Connect the control plane.&lt;/strong&gt; Once data flows and tables accumulate, &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;connect LakeOps&lt;/a&gt; to your catalog. Ten minutes, no data movement, no infrastructure changes. Instant visibility into every table's health state. Start in manual-approval mode — review what the control plane recommends before enabling autonomous execution. Then enable &lt;a href="https://lakeops.dev/solutions/managed-iceberg" rel="noopener noreferrer"&gt;autopilot&lt;/a&gt; and let the closed loop run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Add specialized engines.&lt;/strong&gt; As workloads diversify, add Trino for interactive SQL, DuckDB for notebooks, Snowflake for governed BI access. The REST catalog makes each addition a configuration step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Build consumer-optimized tables (gold).&lt;/strong&gt; Pre-aggregations, feature tables, reporting snapshots. Optimize for specific access patterns once you understand what consumers need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Enable routing.&lt;/strong&gt; With 3+ engines, define &lt;a href="https://lakeops.dev/solutions/query-routing" rel="noopener noreferrer"&gt;routing groups&lt;/a&gt;. Let the control plane dispatch each query to the optimal backend automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Open AI access.&lt;/strong&gt; Expose tables to agents via &lt;a href="https://lakeops.dev/blog/mcp-iceberg-lakeops" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;. Configure guardrails per agent type. Agent telemetry feeds back into optimization priorities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing the options: lake, warehouse, lakehouse
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Characteristic&lt;/th&gt;
&lt;th&gt;Data lake&lt;/th&gt;
&lt;th&gt;Data warehouse&lt;/th&gt;
&lt;th&gt;Data lakehouse&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open files on object storage&lt;/td&gt;
&lt;td&gt;Proprietary format, vendor storage&lt;/td&gt;
&lt;td&gt;Open files on object storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transactions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Full ACID&lt;/td&gt;
&lt;td&gt;Full ACID (table format)&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;Manual, fragmented&lt;/td&gt;
&lt;td&gt;Vendor-managed, centralized&lt;/td&gt;
&lt;td&gt;Catalog-unified, multi-engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Depends entirely on maintenance&lt;/td&gt;
&lt;td&gt;Vendor-optimized, consistent&lt;/td&gt;
&lt;td&gt;Matches warehouse when maintained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Engine flexibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Any tool reads files&lt;/td&gt;
&lt;td&gt;Vendor's engine only&lt;/td&gt;
&lt;td&gt;Multiple engines, REST catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ML/AI workloads&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native but ungoverned&lt;/td&gt;
&lt;td&gt;Export required&lt;/td&gt;
&lt;td&gt;Native and governed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Streaming&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate infrastructure&lt;/td&gt;
&lt;td&gt;Separate infrastructure&lt;/td&gt;
&lt;td&gt;Same tables, same transactions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost at petabyte&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$20K/yr storage&lt;/td&gt;
&lt;td&gt;~$500K+/yr storage+compute&lt;/td&gt;
&lt;td&gt;~$20K/yr storage + operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance burden&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None (no transactions)&lt;/td&gt;
&lt;td&gt;Zero (vendor manages)&lt;/td&gt;
&lt;td&gt;Requires control plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vendor lock-in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low (open formats + catalog)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The lakehouse occupies a specific position: warehouse guarantees at lake economics, with the trade-off that operational maintenance is your responsibility rather than a vendor's. The control plane is what makes this trade-off viable at scale — providing the vendor-grade operational ease without the vendor lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economic model
&lt;/h2&gt;

&lt;p&gt;The lakehouse's cost advantage operates at four levels that compound:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 1: Storage decoupling.&lt;/strong&gt; Object storage at $20–25/TB/year vs. warehouse storage at $500–2,000/TB/year. For 200 TB of curated data, that is $4,000–5,000/year vs. $100,000–400,000/year. At petabyte scale, the savings fund entire platform teams. But storage cost is only the starting point — the larger savings come from operational efficiency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 2: Elimination of duplication.&lt;/strong&gt; The lake-to-warehouse ETL that copies, transforms, and reconciles data disappears. This eliminates: the compute cost of the ETL pipeline itself, the engineering hours maintaining it, the storage cost of the warehouse copy, and the reconciliation overhead when copies drift. For a 200 TB estate, duplication elimination saves $100K–400K/year in direct warehouse storage costs plus 1–3 FTEs worth of engineering time maintaining the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 3: Multi-engine routing efficiency.&lt;/strong&gt; When every query runs on the cheapest engine that meets its latency requirement, aggregate compute cost drops dramatically. A DuckDB point lookup costs fractions of a cent. A self-hosted Trino dashboard query costs pennies. A Spark batch join costs proportional to actual data processed. Without routing, organizations default to Snowflake pricing for everything — $2+ per credit regardless of query complexity. With routing, simple queries route to lightweight engines and only expensive workloads pay for expensive compute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 4: Maintenance economics.&lt;/strong&gt; The control plane's Rust-based engine compacts at sub-$5/TB — roughly 90% cheaper than equivalent Spark maintenance clusters. This cost reduction is not incremental — it is structural. It makes &lt;em&gt;continuous&lt;/em&gt; maintenance economically viable rather than forcing overnight-only windows where tables degrade for 23 hours between passes. Continuous maintenance means tables never accumulate enough degradation to require expensive recovery operations, queries never slow enough to warrant over-provisioned clusters as compensation, and engineering teams never spend emergency weekends rebuilding collapsed tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 5: Recovered engineering time.&lt;/strong&gt; Without autonomous operations, platform teams spend 20–40% of their capacity on table health — writing maintenance scripts, debugging failures, tuning thresholds, responding to "the dashboard is slow" tickets. With a control plane handling the closed loop, that time returns to building data products, onboarding new sources, and serving consumers. For a five-person platform team, that is 1–2 engineers' worth of recovered capacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Combined impact:&lt;/strong&gt; Organizations operating at 500+ TB report 3–5x total cost reduction compared to the warehouse-plus-lake architecture — combining storage savings, duplication elimination, routing-driven compute efficiency, cheaper maintenance, and recovered engineering 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%2Fo8sjqd0wrml604p64cc6.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%2Fo8sjqd0wrml604p64cc6.png" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When to build a data lakehouse
&lt;/h2&gt;

&lt;p&gt;Not every team needs this architecture. A single-engine, single-workload team at moderate scale gets reasonable value from a managed warehouse without the operational complexity. But the lakehouse becomes the correct choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple workload types coexist.&lt;/strong&gt; BI, ML, streaming, and AI workloads need the same data with different access patterns and different engines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale makes economics matter.&lt;/strong&gt; Above 50–100 TB of analytical data, the storage cost differential (10–50x) between warehouse and lakehouse becomes significant enough to fund an entire platform team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-engine is a requirement.&lt;/strong&gt; Different teams or workloads genuinely need different engines — Spark for ETL, Trino for interactive, Flink for streaming, DuckDB for development. The lakehouse makes this safe; the warehouse makes it impossible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor independence is a priority.&lt;/strong&gt; Open formats on your own storage mean no single vendor controls your exit path. Every component decision is reversible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data freshness matters.&lt;/strong&gt; Streaming ingestion directly into governed tables eliminates the ETL delay that warehouses impose between source and dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI/ML is a first-class workload.&lt;/strong&gt; Models and agents need native, governed access to analytical data — not exports, not API wrappers, not stale copies in separate feature stores.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If three or more of these apply, the lakehouse is not a future consideration — it is the architecture to build toward now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The data lakehouse is not a concept — it is the production standard for teams that need BI, ML, streaming, and AI on the same governed data without warehouse pricing or lake-quality compromises.&lt;/p&gt;

&lt;p&gt;Building one is an assembly problem: object storage for economics, a table format (Iceberg) for transactional guarantees, a REST catalog for coordination, and specialized engines for execution.&lt;/p&gt;

&lt;p&gt;Operating one is a systems problem: tables degrade through file fragmentation, metadata growth, delete-file accumulation, and storage waste. Bronze layers fragment through streaming commits. Silver layers accumulate delete-file debt through mutations. Gold layers waste storage through stale snapshots. The degradation is inherent to append-only transactional design — not a bug to fix but a force to continuously counteract.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;control plane&lt;/strong&gt; is the system that counteracts it. &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; implements the closed loop — sense structural telemetry across every table and catalog, assess health with adaptive scoring, plan sequenced maintenance respecting operation dependencies, execute on a dedicated Rust engine that is fast enough to run continuously, and learn from outcomes to improve future decisions. &lt;a href="https://lakeops.dev/solutions/compaction" rel="noopener noreferrer"&gt;Intelligent compaction&lt;/a&gt; that sorts by production query patterns across all engines. &lt;a href="https://lakeops.dev/solutions/managed-iceberg" rel="noopener noreferrer"&gt;Sequenced maintenance&lt;/a&gt; that runs the right operations in the right order. &lt;a href="https://lakeops.dev/solutions/observability" rel="noopener noreferrer"&gt;Observability&lt;/a&gt; that classifies every table's health and surfaces degradation before users feel it. &lt;a href="https://lakeops.dev/solutions/query-routing" rel="noopener noreferrer"&gt;Routing&lt;/a&gt; that dispatches queries to the optimal engine for each workload. &lt;a href="https://lakeops.dev/blog/rise-of-open-apache-lakehouse" rel="noopener noreferrer"&gt;Governance&lt;/a&gt; that enforces retention, maintenance, and access policies lake-wide without per-table configuration. And &lt;a href="https://lakeops.dev/blog/mcp-iceberg-lakeops" rel="noopener noreferrer"&gt;AI enablement&lt;/a&gt; that makes the lake agent-ready with discoverable schemas and layered guardrails.&lt;/p&gt;

&lt;p&gt;The architecture works because something continuously keeps it working. That something is the control plane.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Further learning:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://lakeops.dev/blog/autonomous-iceberg-table-maintenance" rel="noopener noreferrer"&gt;Autonomous Iceberg Table Maintenance&lt;/a&gt; — the closed-loop maintenance system in depth&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://lakeops.dev/blog/iceberg-lakehouse-observability-guide" rel="noopener noreferrer"&gt;Iceberg Lakehouse Observability Guide&lt;/a&gt; — seven pillars of lake monitoring&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://lakeops.dev/blog/intelligent-lakehouse-like-netflix" rel="noopener noreferrer"&gt;Intelligent Lakehouse: Build Like Netflix&lt;/a&gt; — how Netflix designed autonomous lakehouse operations&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; — root causes, measurement, and automated resolution&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://lakeops.dev/blog/mcp-iceberg-lakeops" rel="noopener noreferrer"&gt;How AI Agents Operate a Data Lake via MCP&lt;/a&gt; — the agent-native lakehouse interface&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://lakeops.dev/blog/iceberg-production-readiness-checklist" rel="noopener noreferrer"&gt;Apache Iceberg Production Readiness Checklist&lt;/a&gt; — ten operational dimensions for enterprise Iceberg&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://lakeops.dev/blog/iceberg-query-routing" rel="noopener noreferrer"&gt;Routing Multiple Query Engines with Iceberg&lt;/a&gt; — multi-engine dispatch patterns and optimization&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>programming</category>
      <category>softwareengineering</category>
      <category>data</category>
    </item>
    <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>
  </channel>
</rss>
