<?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>Why Apache Iceberg Needs a Table Management Platform</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Mon, 28 Sep 2026 11:28:05 +0000</pubDate>
      <link>https://dev.to/jonisar/why-apache-iceberg-needs-a-table-management-platform-1ckf</link>
      <guid>https://dev.to/jonisar/why-apache-iceberg-needs-a-table-management-platform-1ckf</guid>
      <description>&lt;p&gt;Every Iceberg table accumulates structural debt from its first commit. Files fragment. Snapshots pile up. Manifests bloat. Orphan files sit on object storage that no snapshot references and no query will ever read, but your bill counts every byte. Query performance degrades gradually, then suddenly — and because the architecture is deliberately decoupled, no single engine or catalog owns the operational picture. Nobody is responsible for the health of the lake as a whole.&lt;/p&gt;

&lt;p&gt;The format is not the problem. Iceberg ships the primitives: &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;, &lt;code&gt;compute_table_stats&lt;/code&gt;. These are &lt;code&gt;fsck&lt;/code&gt; and &lt;code&gt;defrag&lt;/code&gt;, not an operating system. They require external intelligence to decide which tables need attention right now, what parameters to use, in what order to run, how to avoid conflicts with the streaming writer committing every 30 seconds, and how to adapt when access patterns shift from one quarter to the next. The missing piece is not a tool. It is a layer.&lt;/p&gt;

&lt;p&gt;At 20 tables, you solve that with a script. At 200, the scripts are a full-time job. At 2,000, the scripts need their own team — and that team is no longer building data pipelines.&lt;/p&gt;

&lt;p&gt;A lakehouse control plane such as &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; exists to close this gap. It sits above the stack you already have — your catalogs (Glue, Polaris, Nessie, Gravitino, Lakekeeper, S3 Tables), your Iceberg tables on object storage, and your query engines — and supplies the operational loop the open components do not provide as one system: sense every table's health through continuous metadata analysis and cross-engine telemetry, decide what maintenance and physical layout each one needs, execute it in the correct dependency order on a dedicated engine, and feed the result 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%2Fm6u7bg54rgfwsi5jruq5.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%2Fm6u7bg54rgfwsi5jruq5.png" alt=" " width="800" height="450"&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%2Fezblyqzn6s9f5kw2eqbk.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%2Fezblyqzn6s9f5kw2eqbk.png" alt="A lakehouse control plane sits between query engines above and catalogs, table format, and object storage below — providing health detection, sequenced maintenance, cross-engine telemetry, layout optimization, policy governance, and query routing" width="800" height="450"&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%2F55rwdf6j8jxxjbaoweiz.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%2F55rwdf6j8jxxjbaoweiz.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article covers why that layer is necessary, what it actually contains, what breaks without it, and how the current landscape of tools, platforms, and managed services fills or fails to fill the gap. For deep dives on individual concerns, see the companion articles on &lt;a href="https://lakeops.dev/blog/iceberg-table-health-maintenance" rel="noopener noreferrer"&gt;table maintenance&lt;/a&gt;, &lt;a href="https://lakeops.dev/blog/iceberg-lakehouse-observability-guide" rel="noopener noreferrer"&gt;observability&lt;/a&gt;, and &lt;a href="https://lakeops.dev/blog/iceberg-compaction-strategies" rel="noopener noreferrer"&gt;compaction strategies&lt;/a&gt;.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What Iceberg Ships and What It Does Not
&lt;/h2&gt;

&lt;p&gt;It is worth being precise about what Iceberg's own maintenance surface covers, because the gap is both narrower and wider than most people assume.&lt;/p&gt;

&lt;h3&gt;
  
  
  The maintenance primitives (Iceberg 1.11.0)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Procedure&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rewrite_data_files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Merges small files, applies sort or Z-order, resolves position deletes. 15+ tuning parameters including &lt;code&gt;target-file-size-bytes&lt;/code&gt;, &lt;code&gt;min-input-files&lt;/code&gt;, &lt;code&gt;partial-progress.enabled&lt;/code&gt;, and &lt;code&gt;delete-file-threshold&lt;/code&gt;. Branch support added in 1.11.0.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rewrite_manifests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Consolidates manifest files for faster query planning. 1.11.0 added &lt;code&gt;sort_by&lt;/code&gt; to cluster manifest entries by partition transform rather than accepting default spec order.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rewrite_position_delete_files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compacts position delete files and removes dangling deletes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expire_snapshots&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes old snapshots and their exclusively-referenced data files.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;remove_orphan_files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deletes files on storage not referenced by any live snapshot. Safety threshold: 3-day default minimum age.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compute_table_stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generates Puffin NDV sketches for cost-based optimization.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compute_partition_stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Calculates partition-level statistics. Incremental from the last stats snapshot.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Trino's Iceberg connector adds &lt;code&gt;optimize&lt;/code&gt;, &lt;code&gt;optimize_manifests&lt;/code&gt;, &lt;code&gt;expire_snapshots&lt;/code&gt;, and &lt;code&gt;remove_orphan_files&lt;/code&gt; as &lt;code&gt;ALTER TABLE ... EXECUTE&lt;/code&gt; commands, with hard minimum-retention floors that prevent accidental deletion. Flink embeds maintenance directly in streaming jobs through &lt;code&gt;IcebergSink&lt;/code&gt;, with configurable triggers on commit count, file count, file size, and delete-file accumulation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What none of them provide
&lt;/h3&gt;

&lt;p&gt;Every one of these is a tool, not a system. The gap is not in execution capability. It is in the intelligence that decides when and how to use them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No scheduling.&lt;/strong&gt; No built-in interval, event, or condition-based trigger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No health detection.&lt;/strong&gt; No assessment of whether a table needs maintenance, how urgently, or what kind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cross-engine telemetry.&lt;/strong&gt; No awareness of query patterns from Trino, Spark, Athena, DuckDB, Snowflake, or Flink. Sort-order selection is guesswork.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sequencing.&lt;/strong&gt; No enforced dependency order. Compacting before expiring snapshots wastes compute on files about to be garbage-collected. Rewriting manifests before compaction produces metadata that goes stale immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No conflict coordination.&lt;/strong&gt; OCC retries exist, but nothing coordinates maintenance with concurrent writers to avoid the conflict in the first place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No fleet management.&lt;/strong&gt; No multi-table prioritization, no resource allocation across a lake of hundreds or thousands of tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No policy model.&lt;/strong&gt; No hierarchical defaults, no inheritance, no configuration that scales beyond one table at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apache Polaris — now an Apache Top-Level Project — illustrates the gap precisely. Since version 1.0, Polaris defines maintenance &lt;em&gt;policy types&lt;/em&gt;: &lt;code&gt;system.data-compaction&lt;/code&gt;, &lt;code&gt;system.metadata-compaction&lt;/code&gt;, &lt;code&gt;system.orphan-file-removal&lt;/code&gt;, &lt;code&gt;system.snapshot-expiry&lt;/code&gt;, attachable at catalog, namespace, or table level with inheritance. But — and this is the critical detail most articles miss — &lt;strong&gt;Polaris does not execute them.&lt;/strong&gt; The catalog stores the policy. Who runs it, when, and how is explicitly left to the user. This is a deliberate architectural choice and the right one for a catalog, but the gap between policy declaration and policy enforcement is exactly where a table management platform lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Breaks at Scale
&lt;/h2&gt;

&lt;p&gt;These are not hypothetical concerns. Each is a pattern that emerges between 200 and 2,000 tables — not from any single tool failing, but from the gaps between tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small files make queries exponentially worse
&lt;/h3&gt;

&lt;p&gt;A table with the same data stored in 500,000 × 5 MB files performs 10–50× worse on scan planning, costs 10–100× more in S3 GET requests, and takes dramatically longer to maintain than 5,000 × 500 MB files. In lab benchmarks, compacting 2,500 small files into properly sized ones improved query time from 3.6 seconds to 0.4 seconds for aggregations (8.4× improvement) and from 1.2 seconds to 96 milliseconds for point lookups (12.7× improvement). A 1 TB table costs roughly $0.25 to scan when well laid out and $5.00 when fragmented — a 20× difference in S3 API spend alone, before you count the compute time.&lt;/p&gt;

&lt;p&gt;Diagnose it:&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="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="p"&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="p"&gt;)&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;SUM&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;8388608&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;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&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="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="p"&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_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="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;HAVING&lt;/span&gt; &lt;span class="n"&gt;small_files&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&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;small_files&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;If a significant fraction of partitions show hundreds of files under 8 MB, every query against that table is paying a planning and API tax that compaction eliminates immediately. Streaming tables with per-minute commits are the most common source, but even daily batch tables fragment when they receive many small appends throughout the day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manifest bloat inflates planning time
&lt;/h3&gt;

&lt;p&gt;Planning cost scales with the number of manifests, not the number of partitions. A table with 50,000 partitions and 20 manifests plans quickly. A table with 50 partitions and 20,000 manifests — which is what a per-minute streaming writer produces in two weeks — plans slowly, because the planner must open and parse every manifest in the snapshot before execution begins. Five hundred manifests tracking 200,000 data files requires hundreds of object-store GET requests before any data is read.&lt;/p&gt;

&lt;p&gt;Iceberg 1.11.0 added server-side scan planning in the REST catalog spec, which moves manifest parsing from the engine to the catalog, and the &lt;code&gt;sort_by&lt;/code&gt; parameter for &lt;code&gt;rewrite_manifests&lt;/code&gt;, which clusters manifest entries by partition transform. Both help. Neither removes the need to keep the manifest tree healthy — they make a healthy tree faster and a bloated tree less catastrophic, but the underlying growth still requires periodic rewriting.&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;manifest_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;added_data_files_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;existing_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;total_entries&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;manifest_count&lt;/code&gt; exceeds 500 and is growing, planning latency is measurably degraded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orphan files silently consume 25–40% of storage
&lt;/h3&gt;

&lt;p&gt;Industry analyses consistently find that 25–40% of object-storage spend on typical production lakes covers bytes no query will ever read again: orphaned files from failed writes, superseded files from compaction, metadata from expired snapshots that nobody cleaned up. One lab measurement on a modest table found 127 MB of orphan metadata against 48 MB of actual data — a 2.8× metadata-to-data ratio. At Databricks, Predictive Optimization has vacuumed exabytes of unreferenced data, saving tens of millions of dollars — a good indication of how much accumulates when expiry does not keep pace with writes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete files impose read amplification
&lt;/h3&gt;

&lt;p&gt;Position delete files in Iceberg v2 require the engine to join each data file with its deletes at read time. Published benchmarks show that once even 3% of rows go through delete-and-insert cycles, query performance drops by roughly 1.5× purely from the file explosion, and the cost is dominated by the fixed overhead of having any delete files at all, not by the number of deleted rows.&lt;/p&gt;

&lt;p&gt;Iceberg v3 introduced deletion vectors (DVs), which replace position delete files with a per-data-file bitmap. DVs reduce the read-time join cost dramatically but do not eliminate the storage and statistics impact of keeping deleted rows in data files. The rows stay on storage, consuming space and widening column min/max bounds (which degrades pruning), until compaction rewrites the affected files. DVs make the read penalty cheaper; compaction is still the mechanism that removes deleted data.&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;content&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;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="p"&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_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;WHERE&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;IN&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Content 1 = position deletes, content 2 = equality deletes. Any non-trivial count against high-frequency update tables means compaction with delete resolution is overdue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snapshots compound everything
&lt;/h3&gt;

&lt;p&gt;A streaming table committing every minute accumulates over 4,000 snapshots per month. Each snapshot extends the metadata chain that the planner walks. Unreferenced data files pile up behind expired snapshots that nobody removed. A table with 90 days of snapshots and no expiry can have 10× more metadata files than data files — and metadata reads are per-query overhead that no amount of caching eliminates for cold starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maintenance storms degrade the lake they exist to fix
&lt;/h3&gt;

&lt;p&gt;IOMETE's engineering team &lt;a href="https://iomete.com/resources/blog/how-we-built-automated-maintenance" rel="noopener noreferrer"&gt;documented a subtler failure mode&lt;/a&gt;: at hundreds of tables, the maintenance workload itself degrades the lake. A cron job that compacts 500 tables at midnight generates a burst of metadata commits. Conflict rates spike, retries multiply, and the catalog becomes a bottleneck for production readers and writers that have nothing to do with maintenance.&lt;/p&gt;

&lt;p&gt;The irony is structural: maintenance exists to improve query performance, but uncoordinated maintenance at scale degrades it. Production writers hit &lt;code&gt;CommitFailedException&lt;/code&gt; because a compaction job holds a stale base snapshot. The compaction job retries, conflicts again, and eventually succeeds — but the writer's commit also retried, potentially doubling data. Or the compaction job succeeds, but the writer committed to a partition that compaction just rewrote, and now the compacted files are immediately fragmented again. Every &lt;code&gt;CommitFailedException&lt;/code&gt; represents wasted compute, and at scale the waste is dominated by maintenance-vs-maintenance and maintenance-vs-writer conflicts, not by the actual compaction 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqjod7udxtz1gvesyzb6q.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%2Fqjod7udxtz1gvesyzb6q.png" alt="Table health dashboard showing Healthy, Warning, and Critical classification across the lake with file sizes, record counts, and last-compacted timestamps" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Physical Layout: Why Binpack Is Not Enough
&lt;/h2&gt;

&lt;p&gt;This distinction is where most Iceberg maintenance articles lose the plot, and it matters enough to state plainly: &lt;strong&gt;compaction fixes file count. It does not fix data skipping.&lt;/strong&gt; If you bin-pack 5,000 small files into 50 correctly sized ones, you reduced planning overhead and API costs. But if every resulting file contains values from the full range of every column, no engine can skip any of them based on a &lt;code&gt;WHERE&lt;/code&gt; clause. You have fewer files, but you still scan all of them.&lt;/p&gt;

&lt;p&gt;The Iceberg scan planner discards files when column min/max bounds prove a file cannot contain matching rows. That only works when rows with similar values live in the same files. Getting them there requires clustering — and clustering is a separate operation from compaction, with different mechanics, costs, and decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pruning stack
&lt;/h3&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;Mechanism&lt;/th&gt;
&lt;th&gt;What it decides&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Partition pruning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Partition spec + hidden transforms&lt;/td&gt;
&lt;td&gt;Which groups of files can be ignored entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File pruning (clustering / sort order)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manifest-level and file-level column min/max bounds&lt;/td&gt;
&lt;td&gt;Which files within a partition can be skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Z-order / multi-dimensional clustering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Space-filling curve bit interleaving&lt;/td&gt;
&lt;td&gt;Balanced pruning across 2–4 equally important filter columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Row-group pruning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Parquet footer column statistics&lt;/td&gt;
&lt;td&gt;Which row groups within a file can be skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Target 128–512 MB for analytical tables&lt;/td&gt;
&lt;td&gt;Planning overhead vs pruning granularity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Partitioning decides which groups of files exist. Clustering decides whether the files inside those groups are skippable. These are different operations at different layers. Changing a partition spec is a metadata-only operation. Changing a sort order requires rewriting every data file in scope.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sort order vs Z-order
&lt;/h3&gt;

&lt;p&gt;A linear sort on &lt;code&gt;customer_id&lt;/code&gt; produces files where each file covers a narrow range of customer IDs. Queries filtering on &lt;code&gt;customer_id&lt;/code&gt; skip most files. Queries filtering on &lt;code&gt;event_type&lt;/code&gt; skip nothing, because &lt;code&gt;event_type&lt;/code&gt; values are scattered uniformly across the sorted files.&lt;/p&gt;

&lt;p&gt;Z-order interleaves bits from multiple columns so that rows close in multi-dimensional space end up in the same files. This gives balanced pruning across 2–4 filter columns, at the cost of slightly worse pruning on any single column compared to a linear sort on that column. Past 4 columns, Z-order dilutes rapidly, and past 6 it provides little benefit over random distribution.&lt;/p&gt;

&lt;p&gt;The decision between them depends on query patterns:&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;Layout&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queries overwhelmingly filter on one column&lt;/td&gt;
&lt;td&gt;Linear sort on that column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries filter on 2–4 columns with roughly equal frequency&lt;/td&gt;
&lt;td&gt;Z-order on those columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries filter on many different columns unpredictably&lt;/td&gt;
&lt;td&gt;Z-order on the 3–4 most common; accept limited pruning on the rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-cardinality point lookups on one key&lt;/td&gt;
&lt;td&gt;Linear sort + Parquet bloom filter on that key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why this requires telemetry
&lt;/h3&gt;

&lt;p&gt;The right layout depends on how queries actually access the table — and that changes. A table sorted by &lt;code&gt;order_date&lt;/code&gt; serves dashboard queries well. When a new fraud-detection pipeline starts filtering on &lt;code&gt;merchant_id&lt;/code&gt;, the sort order becomes irrelevant for that workload. Without cross-engine telemetry aggregating filter, join, and group-by frequency across Trino, Spark, Athena, Snowflake, and Flink, sort-order decisions are intuition that nobody revisits.&lt;/p&gt;

&lt;p&gt;This is the specific argument for a platform-level solution rather than per-table tooling. The optimal layout is not a property of the table. It is a property of the relationship between the table and every engine that reads it.&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%2Fv8id7867gjafzswo105x.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%2Fv8id7867gjafzswo105x.png" alt="Layout simulations showing query-aware sort strategies — clusterByOrderDate, cluster.order_type.by.status, and cluster.insert-time-line — with field-access frequency distribution and scan reduction comparison against baseline" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Table Management" Actually Means
&lt;/h2&gt;

&lt;p&gt;The term gets used loosely. For Iceberg tables in production, it decomposes into five operational concerns that must work as a unified system rather than as independent tools. Each concern feeds the others; assembling them from disconnected parts is technically possible and operationally fragile.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Observability
&lt;/h3&gt;

&lt;p&gt;You cannot maintain what you cannot measure. Iceberg has no native health dashboards, no cross-engine telemetry aggregation, no alerting. The metadata is there — snapshot history, manifest lists, file-level statistics — but extracting operational intelligence from it means running Spark or Trino SQL against &lt;code&gt;table.snapshots&lt;/code&gt;, &lt;code&gt;table.files&lt;/code&gt;, and &lt;code&gt;table.manifests&lt;/code&gt;, one table at a time.&lt;/p&gt;

&lt;p&gt;A management platform turns raw Iceberg metadata into continuous health classification. Every table is scored — Healthy, Warning, or Critical — based on signals that directly affect query performance: file count and size distribution, manifest depth, snapshot accumulation, delete-file ratio, partition skew, and sort-order alignment with actual query patterns.&lt;/p&gt;

&lt;p&gt;The word &lt;em&gt;continuous&lt;/em&gt; is the key. A quarterly audit catches problems months after they started costing money. Continuous scoring surfaces a streaming table's small-file explosion within hours, before downstream dashboards slow to a crawl.&lt;/p&gt;

&lt;p&gt;Cross-engine telemetry is the other half. When Trino, Spark, and Snowflake all read the same table, understanding which columns each engine filters, joins, and groups on — aggregated into a single access profile per table — is the foundation for layout optimization. Without it, sort-key selection is intuition that nobody revisits.&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%2F49lggk0hpw5i2ocqqbeg.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%2F49lggk0hpw5i2ocqqbeg.png" alt="Severity-ranked health insights showing table issues prioritized by operational impact — from critical small-file accumulation down to low-priority metadata cleanup" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Autonomous maintenance
&lt;/h3&gt;

&lt;p&gt;The maintenance lifecycle for an Iceberg table is a dependency chain, not a checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expire snapshots&lt;/strong&gt; — removes metadata references to old snapshots. This must go first because it shrinks the file set every subsequent step has to process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove orphan files&lt;/strong&gt; — deletes data files on storage that no snapshot references, including those just freed by expiry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compact data files&lt;/strong&gt; — merges small files into optimally sized ones, applying sort or Z-order clustering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite manifests&lt;/strong&gt; — consolidates manifest files so the metadata tree indexes the layout that now exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh statistics&lt;/strong&gt; — recomputes Puffin NDV sketches so optimizer estimates match the current file set.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The ordering matters. Compacting before expiring wastes compute — you are rewriting files that expiry would have garbage-collected. Rewriting manifests before compaction produces metadata that goes stale as soon as compaction changes the underlying files. Refreshing statistics before compaction produces estimates that describe a layout that no longer exists.&lt;/p&gt;

&lt;p&gt;Most teams start with Airflow DAGs or cron jobs calling Spark procedures. The &lt;a href="https://lakeops.dev/blog/airflow-lakehouse-maintenance" rel="noopener noreferrer"&gt;architectural limitations&lt;/a&gt; emerge as the fleet grows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed schedules cannot adapt to table state.&lt;/strong&gt; A table committing every 30 seconds needs compaction multiple times an hour. A weekly batch table needs it once. A uniform daily schedule over-compacts quiet tables and under-compacts hot ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-DAG coordination is absent.&lt;/strong&gt; Ensuring expiry completes before compaction starts, across 500 tables, requires manual orchestration Airflow was not designed for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://lakeops.dev/blog/iceberg-commit-conflicts" rel="noopener noreferrer"&gt;Commit conflicts accumulate.&lt;/a&gt;&lt;/strong&gt; Compaction jobs running alongside production writers compete for the metadata pointer via OCC. At scale, conflict rates become the dominant source of failed maintenance, wasted compute, and on-call pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linear configuration burden.&lt;/strong&gt; Each new table needs its own sort columns, target file size, retention window, and schedule. At 2,000 tables, maintaining the configurations is a full-time job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Intelligent compaction and layout
&lt;/h3&gt;

&lt;p&gt;Standard bin-pack compaction merges small files into bigger ones. As established above, it does not improve data skipping. &lt;strong&gt;Query-aware compaction&lt;/strong&gt; goes further: it builds a per-table access profile from all connected engines' query logs, identifies the filter, join, and grouping columns that drive data access, and physically re-sorts data files to match during compaction. When data is sorted by the columns queries actually hit, Parquet row-group min/max statistics become tight, and engines skip entire file groups without reading them. The difference is often an order of magnitude: a table that scanned 4 TB scans 400 GB after sort compaction, because min/max pruning eliminates 90% of files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layout simulation&lt;/strong&gt; is the related capability: testing a proposed sort strategy against real query patterns on an Iceberg branch before committing it to production. Bad sort decisions are expensive to reverse since you are rewriting all the data files in scope, so the ability to evaluate projected scan reduction before a full rewrite is not a nice-to-have. It is the mechanism that makes automated sort-order selection safe rather than reckless.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Multi-engine coordination
&lt;/h3&gt;

&lt;p&gt;Production Iceberg lakehouses rarely run a single engine. Trino handles interactive analytics. Spark runs ETL. DuckDB serves quick lookups. Snowflake powers BI. Athena covers ad-hoc. Flink processes streaming ingestion. Each has different cost characteristics, latency profiles, and strengths. Without coordination, every query goes to one engine regardless of whether it is the right one, and maintenance decisions cannot account for how all engines actually access the data.&lt;/p&gt;

&lt;p&gt;A management platform provides a routing layer that dispatches each query to the optimal engine based on query shape, cost model, and latency target. This creates a reinforcing loop with compaction: a well-sorted table enables cheaper engines to serve queries that a fragmented table would require an expensive engine for. Routing telemetry feeds back into sort-order selection, so both improve simultaneously.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Policy and governance
&lt;/h3&gt;

&lt;p&gt;At 50 tables, you configure each one individually. At 500, you need declarative policies that cascade hierarchically — organization, catalog, namespace, table — with inheritance and overrides. Define compaction targets, snapshot retention, orphan cleanup thresholds, and sort strategies once at the namespace level. New tables inherit the correct configuration automatically. Override individual tables when needed.&lt;/p&gt;

&lt;p&gt;Governance also means audit trails. Every maintenance operation — what ran, when, duration, files before and after, bytes reclaimed, the signal that triggered it — needs to be logged in a structured, queryable format. For compliance environments the trail is evidence; without it, you are proving GDPR erasure completion from Airflow logs and Spark driver output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why these five must be unified
&lt;/h3&gt;

&lt;p&gt;Each concern has standalone tools. The problem is that they are deeply coupled in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observability feeds maintenance.&lt;/strong&gt; Health scores determine which tables need attention and how urgently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance feeds compaction.&lt;/strong&gt; Expiry and cleanup must complete before compaction to avoid rewriting files about to be garbage-collected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compaction depends on telemetry.&lt;/strong&gt; The optimal sort order comes from knowing how all engines query the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing improves when compaction improves.&lt;/strong&gt; A well-sorted table enables cheaper engines, and routing telemetry feeds the next sort decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policies must span all four.&lt;/strong&gt; A retention policy affects snapshots (maintenance), which affects orphan accumulation (observability), which affects how much compaction processes, which affects file layout for every engine (routing).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The argument for a platform is not that individual tools are bad. It is that the interactions between concerns are where the operational value compounds, and independent tools by definition cannot exploit feedback loops across them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Diagnostic Runbook: Is Your Lake Degraded?
&lt;/h2&gt;

&lt;p&gt;Before choosing a tool, measure the problem. Run these queries against your most important tables and score the results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: File fragmentation
&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;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;total_files&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;8388608&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="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="p"&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="p"&gt;)&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="n"&gt;STDDEV&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="p"&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stddev_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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Healthy:&lt;/strong&gt; &lt;code&gt;small_files&lt;/code&gt; &amp;lt; 5% of &lt;code&gt;total_files&lt;/code&gt;, &lt;code&gt;avg_mb&lt;/code&gt; in the 128–512 MB range. &lt;strong&gt;Warning:&lt;/strong&gt; 5–30% small files. &lt;strong&gt;Critical:&lt;/strong&gt; &amp;gt; 30% small files, or &lt;code&gt;avg_mb&lt;/code&gt; under 32.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Manifest depth
&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;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="k"&gt;length&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_manifest_mb&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_data_files_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;existing_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;total_entries&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Healthy:&lt;/strong&gt; under 200 manifests. &lt;strong&gt;Warning:&lt;/strong&gt; 200–500. &lt;strong&gt;Critical:&lt;/strong&gt; over 500 and growing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Snapshot accumulation
&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;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_snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;MAX&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;newest_snapshot&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;Healthy:&lt;/strong&gt; retention window is enforced and recent. &lt;strong&gt;Warning:&lt;/strong&gt; snapshots older than 30 days with no policy. &lt;strong&gt;Critical:&lt;/strong&gt; thousands of snapshots spanning months.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Delete file burden
&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;content&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;files&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;record_count&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;rows&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any equality deletes outside a Flink pipeline are a red flag. Position delete counts exceeding 10% of data file count indicate compaction lag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Sort-order alignment
&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower_bound&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper_bound&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ub&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="o"&gt;/&lt;/span&gt; &lt;span class="p"&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&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;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;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;lb&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;Replace &lt;code&gt;customer_id&lt;/code&gt; with your primary filter column. If adjacent files have broadly overlapping bounds (lower_bound = 1, upper_bound = 10M on most files), the table is not clustered on that column and no amount of partitioning will fix file-level pruning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scoring
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All green&lt;/td&gt;
&lt;td&gt;Table is well-maintained&lt;/td&gt;
&lt;td&gt;Monitor; adjust cadence if write patterns change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1–2 warnings&lt;/td&gt;
&lt;td&gt;Targeted fix&lt;/td&gt;
&lt;td&gt;Run the relevant procedure(s); consider policy-based scheduling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any critical&lt;/td&gt;
&lt;td&gt;Active degradation&lt;/td&gt;
&lt;td&gt;Immediate maintenance; evaluate platform-level management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple criticals across tables&lt;/td&gt;
&lt;td&gt;Systemic gap&lt;/td&gt;
&lt;td&gt;The maintenance approach itself needs replacing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Current Landscape
&lt;/h2&gt;

&lt;p&gt;The Iceberg management space has converged on several architectural approaches, each making different trade-offs about scope, lock-in, and operational burden.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catalog-bundled maintenance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;What it provides&lt;/th&gt;
&lt;th&gt;What it does not&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Apache Polaris&lt;/strong&gt; (TLP since Feb 2026)&lt;/td&gt;
&lt;td&gt;Maintenance &lt;em&gt;policies&lt;/em&gt; (data-compaction, metadata-compaction, orphan-removal, snapshot-expiry) with catalog-namespace-table inheritance&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Does not execute them.&lt;/strong&gt; Policy declaration without enforcement.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Dremio Open Catalog&lt;/strong&gt; (on Polaris)&lt;/td&gt;
&lt;td&gt;Automatic compaction, DV resolution, clustering, manifest rewrite, vacuum — five operations, ~3h optimize / 24h vacuum, dedicated engine&lt;/td&gt;
&lt;td&gt;Only tables inside Dremio's catalog. Target file size not per-table configurable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Glue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Three optimizers: compaction (binpack/sort/z-order), snapshot retention, orphan deletion. $0.44/DPU-hour.&lt;/td&gt;
&lt;td&gt;No manifest rewrite, no statistics, no cross-engine sort awareness, no sequencing, Parquet only, &lt;a href="https://lakeops.dev/blog/aws-glue-iceberg-optimization" rel="noopener noreferrer"&gt;auto-suspends after 4 consecutive failures&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S3 Tables&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fully managed compaction (auto/binpack/sort/z-order), snapshot management, orphan cleanup — all enabled by default&lt;/td&gt;
&lt;td&gt;No manifest rewrite, no statistics, no cross-engine telemetry. Object monitoring fee penalizes the fragmentation compaction exists to fix.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snowflake&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Data compaction (bin-pack) + manifest compaction (always-on, cannot disable). Free for Snowflake-only writes since May 2026.&lt;/td&gt;
&lt;td&gt;No sort/z-order beyond Automatic Clustering. External-engine DML activates compaction credit charges. No exposed &lt;code&gt;expire_snapshots&lt;/code&gt; or &lt;code&gt;remove_orphan_files&lt;/code&gt; procedures.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Databricks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Predictive Optimization: OPTIMIZE + Liquid Clustering + VACUUM + ANALYZE, enabled by default since Nov 2024. Selects and evolves clustering keys from query telemetry.&lt;/td&gt;
&lt;td&gt;Requires Unity Catalog. Telemetry is scoped to Databricks workloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Starburst Galaxy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Icehouse LakeOps": automated compaction (hourly), snapshot expiry (daily, &amp;gt;30 days), manifest rewrite (daily), orphan removal. Per-table health dashboards.&lt;/td&gt;
&lt;td&gt;Schedule-driven, not health-driven. No cross-engine sort awareness.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lakekeeper&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatic snapshot expiry (per-warehouse, configurable) and adaptive orphan cleanup&lt;/td&gt;
&lt;td&gt;No compaction, no manifest rewrite, no layout optimization.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gravitino&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Table Maintenance Service: alpha, policy-driven compaction for identity-partitioned tables only&lt;/td&gt;
&lt;td&gt;CLI-driven, not self-scheduling. No snapshot expiry, orphan cleanup, or sort maintenance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nessie&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Pure catalog: Git-style branching and tagging for metadata.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern: each platform solves maintenance within its boundary and is blind to everything outside it. Databricks is the furthest along with query-driven clustering, but it requires Unity Catalog and sees only Databricks workloads. In a multi-engine, multi-catalog environment — which is what the open lakehouse was designed to enable — each boundary creates a gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open-source management tools
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;a href="https://amoro.apache.org/" rel="noopener noreferrer"&gt;Apache Amoro&lt;/a&gt;&lt;/strong&gt; (incubating)&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;~1,170&lt;/td&gt;
&lt;td&gt;Self-optimizing management service (AMS) with optimizer groups. Monitors tables, dispatches compaction, supports Iceberg + Paimon + Hudi.&lt;/td&gt;
&lt;td&gt;AMS calls &lt;code&gt;loadTable()&lt;/code&gt; on every planning cycle (default: every minute). At 2,000+ tables, this creates significant catalog and object-storage pressure. Still incubating (v0.8.1).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/linkedin/openhouse" rel="noopener noreferrer"&gt;OpenHouse&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;~400&lt;/td&gt;
&lt;td&gt;LinkedIn's RESTful control plane: declarative table provisioning, policy-based retention, Spark maintenance on Kubernetes. Proven at LinkedIn scale.&lt;/td&gt;
&lt;td&gt;Built on Iceberg 1.2.0 and Spark 3.1.2 — multiple major versions behind current Iceberg. Tied to LinkedIn's infrastructure patterns.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/nimtable/nimtable" rel="noopener noreferrer"&gt;Nimtable&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;~479&lt;/td&gt;
&lt;td&gt;Web platform for Iceberg observability and management by RisingWave Labs. Browse catalogs, visualize file and snapshot distribution, run SQL, acts as REST Catalog endpoint.&lt;/td&gt;
&lt;td&gt;Observability-focused; delegates maintenance execution to Spark or RisingWave. Not a compaction engine.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/nssalian/floe" rel="noopener noreferrer"&gt;Floe&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;~29&lt;/td&gt;
&lt;td&gt;Policy-based maintenance orchestrator. Declarative policies with glob-pattern table matching, health thresholds, scheduled execution of all four Iceberg procedures via Spark (Livy) or Trino. Multi-catalog. Web UI. Featured on &lt;a href="https://polaris.apache.org/blog/2026/02/04/floe-and-apache-polaris-policy-driven-table-maintenance-for-apache-iceberg/" rel="noopener noreferrer"&gt;Apache Polaris blog&lt;/a&gt; as the reference policy consumer.&lt;/td&gt;
&lt;td&gt;Orchestrator, not engine — dispatches to Spark/Trino for execution. Solo developer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/nimtable/iceberg-compaction" rel="noopener noreferrer"&gt;nimtable/iceberg-compaction&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;~140&lt;/td&gt;
&lt;td&gt;Purpose-built Rust compaction on DataFusion. Handles positional and equality deletes. Most active open-source compaction project.&lt;/td&gt;
&lt;td&gt;Full-table bin-pack only; sort/Z-order, incremental compaction, and orphan deletion are roadmap items.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/hupe1980/bergman" rel="noopener noreferrer"&gt;Bergman&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Rust maintenance engine on &lt;code&gt;iceberg-rust&lt;/code&gt; + DataFusion. All four operations plus dangling delete-file removal. Library-first.&lt;/td&gt;
&lt;td&gt;Pre-release, solo project. Implements its own commit layer because upstream &lt;code&gt;iceberg-rust&lt;/code&gt; lacks rewrite-commit support.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/basekick-labs/firn" rel="noopener noreferrer"&gt;Firn&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;~15&lt;/td&gt;
&lt;td&gt;JVM-free daemon; shells out to DuckDB for compaction. Binpack/sort/z-order, snapshot expiry, orphan cleanup. Multi-catalog (Lakekeeper, Glue, Polaris, Nessie).&lt;/td&gt;
&lt;td&gt;Pre-v1.0 and stale for several months.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/paulcaron16k/Zamboni" rel="noopener noreferrer"&gt;Zamboni&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Single-process maintenance via PyIceberg + DuckDB. Declarative layout config. Covers compaction, Z-order, partition evolution, manifest rewriting, snapshot expiry, orphan removal.&lt;/td&gt;
&lt;td&gt;Single-process; no fleet orchestration, no health-triggered scheduling.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/adelfardi/iceguard" rel="noopener noreferrer"&gt;IceGuard&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Multi-catalog web console for browsing and scheduling maintenance pipelines (Airflow-style chained actions).&lt;/td&gt;
&lt;td&gt;Very early stage.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few deserve deeper examination because they represent distinct architectural philosophies:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache Amoro&lt;/strong&gt; is the most ambitious open-source effort. Its architecture (AMS → Optimizer Groups → Optimizer containers) provides fleet-level visibility: a central dashboard shows every table's health, a scheduling layer assigns compaction tasks to optimizer groups, and the system detects "self-optimizing" conditions (small files, orphan snapshots, excessive delete files). The architectural concern is scale: at 2,000+ tables against S3-backed catalogs, the per-minute metadata load from &lt;code&gt;loadTable()&lt;/code&gt; calls becomes the bottleneck the system was designed to alleviate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenHouse&lt;/strong&gt; proves the concept at LinkedIn scale. It is a full RESTful control plane: declarative table provisioning, policy-driven retention, column-lineage-based governance, Spark maintenance jobs on Kubernetes, and an API-first design where tables are requested through a service and automatically provisioned with the right policies. The limitation is that it was built for LinkedIn's specific infrastructure and remains on Iceberg 1.2.0 — five major versions behind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nimtable/iceberg-compaction&lt;/strong&gt; has emerged as the most active open-source compaction engine. Written in Rust on DataFusion, it handles positional and equality deletes, runs without a JVM, and benchmarks competitively. Its scope is intentionally narrow: full-table bin-pack compaction. This is a compaction library, not a management platform, and it is useful precisely because of that focus.&lt;/p&gt;

&lt;p&gt;What none of them provides is the full loop: health detection driving prioritization, dependency-aware sequencing, cross-engine telemetry feeding layout decisions, conflict-safe execution, and a feedback cycle where outcomes improve future decisions. Each solves a slice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Flink fits
&lt;/h3&gt;

&lt;p&gt;Flink's &lt;code&gt;TableMaintenance&lt;/code&gt; is the most sophisticated per-table automation in Iceberg: event-driven triggers on commit count, file count, file size, and delete-file accumulation, all embedded in the streaming topology. But it is per-table, per-sink maintenance — not a management platform. No fleet-wide visibility, no cross-engine telemetry, no policy management, no multi-table prioritization. It is one component of what a platform provides.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a Control Plane Runs This Loop
&lt;/h2&gt;

&lt;p&gt;A table management platform occupies a specific architectural layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────┐
│  Query Engines (Trino, Spark, Flink, DuckDB...)  │
├──────────────────────────────────────────────────┤
│  TABLE MANAGEMENT PLATFORM / CONTROL PLANE       │ ← This layer
│  Health · Sequenced maintenance · Telemetry ·    │
│  Layout optimization · Policies · Routing        │
├──────────────────────────────────────────────────┤
│  Catalog (Polaris, Nessie, Glue, Gravitino...)   │
├──────────────────────────────────────────────────┤
│  Table Format (Apache Iceberg)                    │
├──────────────────────────────────────────────────┤
│  Object Storage (S3, GCS, ADLS)                  │
└──────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not replace the catalog, the engine, or the storage. It sits between them and supplies the operational intelligence that none of them individually possesses. A catalog manages metadata and access control. A query engine executes queries and maintenance procedures. A control plane decides which maintenance to run, when, in what order, with what parameters, and adapts those decisions continuously.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; is built for this layer. It connects to catalogs through standard APIs — Glue, Polaris, Nessie, Gravitino, Lakekeeper, S3 Tables — and to query engines for telemetry, without requiring data movement, pipeline changes, or an SDK. The architecture follows a closed-loop model:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sense&lt;/strong&gt; — reads Iceberg metadata from every connected catalog continuously. Each table is scored as Healthy, Warning, or Critical based on file layout, manifest depth, snapshot age, delete-file ratio, partition skew, and sort-order alignment with observed query patterns. Cross-engine telemetry from Trino, Spark, Snowflake, Athena, DuckDB, and Flink reveals which columns are filtered, joined, and grouped — per table, across the full engine fleet.&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%2F5ky5s4gpgayc4ogtsx7d.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%2F5ky5s4gpgayc4ogtsx7d.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan&lt;/strong&gt; — prioritizes tables by degradation severity. A production table with 50,000 small files gets compacted before a staging table with 200. Operations are sequenced in dependency order. Partitions with active writers are identified and excluded, eliminating the commit conflict failures that dominate &lt;a href="https://lakeops.dev/blog/autonomous-iceberg-table-maintenance" rel="noopener noreferrer"&gt;maintenance at scale&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%2F5qt2fplfu5qk5p5nar83.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%2F5qt2fplfu5qk5p5nar83.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimize&lt;/strong&gt; — compaction, snapshot expiration, orphan cleanup, manifest rewriting, and delete-file resolution execute on a Rust engine built on Apache DataFusion. No Spark clusters, no JVM overhead. Maintenance operations are staggered across the catalog to avoid the maintenance-storm pattern described earlier.&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%2Ff466pv40eu1qaizn86z4.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%2Ff466pv40eu1qaizn86z4.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learn&lt;/strong&gt; — outcomes feed back into the system. Tables where sort compaction produced large scan reduction get prioritized for sort maintenance. Sort orders adapt as query patterns evolve. Cadence tunes to each table's actual write velocity. This is the closed loop that independent tools cannot form: the one where every maintenance decision improves the next one.&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%2F453a84nhwxri301uh0wf.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%2F453a84nhwxri301uh0wf.png" alt=" " width="799" 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%2Fvnke27cjx6wyiw9nfpq9.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%2Fvnke27cjx6wyiw9nfpq9.png" alt="Per-table adaptive maintenance showing compaction, snapshot expiry, manifest rewrite, and orphan cleanup operations with status, timing, and dependency sequencing" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What this looks like concretely
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Sort-order selection from telemetry.&lt;/strong&gt; Rather than a human analyzing logs across three engines to guess which columns to sort by, the platform aggregates &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, and &lt;code&gt;GROUP BY&lt;/code&gt; column frequency across every connected engine and builds a per-table field-access profile. If 82% of Trino queries filter on &lt;code&gt;user_id&lt;/code&gt; and 71% of Athena reports filter on &lt;code&gt;campaign_id&lt;/code&gt;, the sort order maximizes pruning for that mix. When the mix changes — a new dashboard, a new pipeline — the sort order adapts in the next compaction cycle.&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%2Fkp384dw76xm2vky0hh7p.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%2Fkp384dw76xm2vky0hh7p.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layout simulation on Iceberg branches.&lt;/strong&gt; Before a sort-order change touches production data, the candidate layout is tested against real query patterns on an Iceberg branch: comparing projected scan reduction and file distribution to the baseline. Only a strategy that measurably improves pruning gets applied. Bad sort decisions are expensive to reverse (you are rewriting every data file in scope), and this mechanism is what makes automated layout selection safe rather than reckless. &lt;a href="https://lakeops.dev/blog/iceberg-lake-compaction" rel="noopener noreferrer"&gt;Compaction strategy details&lt;/a&gt; cover how binpack, sort, and Z-order selection differ per 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%2F8uhjt3z7dajzix286pau.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%2F8uhjt3z7dajzix286pau.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflict-aware compaction.&lt;/strong&gt; Rather than failing four times and suspending (as Glue's optimizer does), the platform monitors per-partition commit activity across engines and excludes partitions with active writers from the compaction scope. When a commit does lose an OCC race, it backs off, re-evaluates partition state, and reschedules rather than immediately retrying into the same conflict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hierarchical policies.&lt;/strong&gt; Maintenance configuration cascades from catalog to namespace to table, with more specific policies overriding broader defaults. New tables inherit the right configuration from their namespace automatically. This is the same model Polaris's policy framework uses for declaring intent, with the crucial difference that a control plane actually executes it.&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%2Fjmrytb53zgr2vyjfddkj.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%2Fjmrytb53zgr2vyjfddkj.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%2F1r7fbl5jyfghbrtfjc1t.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%2F1r7fbl5jyfghbrtfjc1t.png" alt="Lake-wide maintenance policies for compaction, orphan cleanup, snapshot expiry, and manifest rewrite — each with scope, schedule, and override configuration" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Staged adoption.&lt;/strong&gt; You can start in manual mode, executing operations per table and reviewing results. Then define scheduled policies at the namespace level. Then enable Adaptive Maintenance, where the platform monitors health signals and runs the right operations at the right time without fixed schedules or manual tuning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance.&lt;/strong&gt; The Rust/DataFusion compaction engine avoids the JVM entirely. Published benchmarks show 95% faster compaction than Spark on identical datasets: a 200 GB S3 Tables compaction completed in 221 seconds vs. 1,612 seconds for Spark (7.3×), while Spark OOMed on a 1.2 TB table that the Rust engine processed in approximately 11 minutes. A 5.5 TB, 10-table benchmark achieved 2,522 MB/s peak throughput with 81% file-count reduction. Sub-minute compaction cycles keep up with streaming writers instead of running as batch catch-up 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%2Fvyss3v9ktx6isb2sex6p.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%2Fvyss3v9ktx6isb2sex6p.png" alt=" " width="799" height="446"&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%2F5d47wwjy6yg4fccinoot.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%2F5d47wwjy6yg4fccinoot.png" alt="Compaction benchmark results comparing speed, cost per TB, and query improvement across engines — showing 95% faster compaction, up to 12× query improvement, and ~80% cost reduction" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;platform overview&lt;/a&gt; covers how these capabilities compose: from &lt;a href="https://lakeops.dev/solutions/observability" rel="noopener noreferrer"&gt;lake-wide observability&lt;/a&gt; through &lt;a href="https://lakeops.dev/solutions/compaction" rel="noopener noreferrer"&gt;query-aware compaction&lt;/a&gt;, multi-engine &lt;a href="https://lakeops.dev/blog/routing-multiple-query-engines-with-iceberg" rel="noopener noreferrer"&gt;query routing&lt;/a&gt;, declarative &lt;a href="https://lakeops.dev/solutions/governance" rel="noopener noreferrer"&gt;policy management&lt;/a&gt;, and AI-agent access with layered guardrails (ReadOnly mode, cost-estimate caps, PII masking, human approval for high-stakes operations).&lt;/p&gt;

&lt;p&gt;Three architectural guarantees matter for teams evaluating this category:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No data movement.&lt;/strong&gt; All operations happen through standard Iceberg commit APIs. Data stays on your storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No pipeline changes.&lt;/strong&gt; Connect catalogs and engines through standard APIs. No SDK, no code changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; Everything the platform produces is standard Iceberg: snapshots, manifests, data files. Disconnect it and your tables remain exactly as they are.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Evaluating the Options
&lt;/h2&gt;

&lt;p&gt;The right approach depends on where you are, not where you want to be:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Under 50 tables, single engine, single catalog, batch writes&lt;/td&gt;
&lt;td&gt;Scripts + standalone tools (Spark procedures, Trino &lt;code&gt;EXECUTE&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glue catalog, Athena-only, Parquet, batch, and compaction works fine&lt;/td&gt;
&lt;td&gt;AWS Glue optimizers. Add catalog-level defaults and stop there.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glue optimizer suspends on streaming writes or manifests are never rewritten&lt;/td&gt;
&lt;td&gt;Supplement Glue with external tools over the REST catalog for the missing operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single platform (Databricks, Snowflake) genuinely handles 90%+ of your workload&lt;/td&gt;
&lt;td&gt;Platform-bundled optimizer. Accept the boundary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple engines, multiple catalogs, or streaming writes with cross-engine access&lt;/td&gt;
&lt;td&gt;Control plane. The integration cost of assembling standalone tools exceeds the cost of a platform.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500+ tables, growing, with maintenance consuming more engineering time than pipelines&lt;/td&gt;
&lt;td&gt;Control plane. Manual approaches break at this scale.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Build it yourself (Airflow + Spark)
&lt;/h3&gt;

&lt;p&gt;Works when you have under ~100 tables, a single engine, and a platform team with capacity to own the scheduler, conflict handling, sequencing, health detection, and configuration management. The honest failure mode is not that it does not work. It is that it works until the person who built it changes teams.&lt;/p&gt;

&lt;p&gt;One detail worth knowing: Iceberg's &lt;code&gt;min-input-files&lt;/code&gt; default is 5, while Glue overrides it to 100. If you are moving from Glue compaction to Spark procedures, that difference explains why your tables feel under-compacted on Glue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adopt a catalog-bundled optimizer
&lt;/h3&gt;

&lt;p&gt;Works well when a single engine or platform is genuinely your primary query layer. Databricks Predictive Optimization is the most complete, with query-driven clustering key selection that evolves over time, but it requires Unity Catalog and sees only Databricks workloads. Snowflake's managed compaction is zero-config but bills you permanently the moment an external engine touches the table. Glue is the path of least resistance on AWS but cannot rewrite manifests, select sort orders, or survive streaming write conflicts. Each is useful within its scope and blind outside it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use open-source management tools
&lt;/h3&gt;

&lt;p&gt;Floe gives you policy-driven scheduling over Spark or Trino with multi-catalog reach. &lt;code&gt;nimtable/iceberg-compaction&lt;/code&gt; gives you a fast Rust compaction engine. Apache Amoro gives you a self-optimizing management service with streaming-aware triggers. These are real options for teams that want to own the infrastructure and avoid vendor dependency, with the honest caveat that none provides the full loop and most are pre-1.0.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adopt a dedicated control plane
&lt;/h3&gt;

&lt;p&gt;Works when the table fleet is large enough that per-table configuration is intractable, when multiple engines hit the same tables and need coordinated layout decisions, when streaming writes make conflict handling a constant concern, and when the engineering time spent maintaining the maintenance system exceeds the cost of a platform. This is where LakeOps fits: a managed control plane that layers onto your existing stack, runs the full operational loop, and scales sublinearly — going from 500 to 5,000 tables should not require 10× more configuration or human attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hybrid path
&lt;/h3&gt;

&lt;p&gt;These approaches compose more than most comparisons suggest. You can run Glue compaction for bulk binpack on the tables it handles well, connect an open-source tool for manifest rewriting, and add a control plane for the tables with streaming writes and multi-engine access patterns. The Glue REST catalog makes this possible; the question is whether the integration cost exceeds the cost of a single system.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Format Evolution Changes the Picture
&lt;/h2&gt;

&lt;p&gt;Iceberg v3 and the early v4 proposals affect what management platforms need to handle, but they do not reduce the need for management itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deletion vectors (v3)&lt;/strong&gt; replace position delete files with per-data-file bitmaps. The read-time penalty drops from a file join to a bitmap lookup, but deleted rows stay in data files, consuming storage and widening column statistics until compaction rewrites them. DVs make the read penalty cheaper; compaction remains the mechanism that removes deleted data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variant shredding (v3)&lt;/strong&gt; stores semi-structured data (JSON) in typed Parquet columns while preserving the original document. This creates wider tables with more columns, more partition statistics, and more metadata — making sort-order decisions more complex because shredded field access patterns may differ from top-level columns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Row lineage (v3)&lt;/strong&gt; assigns a monotonically increasing sequence number to every row, enabling engines to track individual rows across snapshots. For management platforms, lineage simplifies delete-file resolution: identify exactly which data files are affected by a batch of deletes and compact only those, rather than scanning the entire table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Geospatial bounds (v3)&lt;/strong&gt; add bounding-box statistics for geometry columns — a new class of statistics that management platforms need to maintain, and spatial clustering (Z-order on coordinates) interacts with them in ways that temporal sort does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata compaction (v4 proposal)&lt;/strong&gt; would move manifest consolidation into the format itself, potentially eliminating external &lt;code&gt;rewrite_manifests&lt;/code&gt; calls. If adopted, this removes one of the five maintenance procedures but does not affect the other four.&lt;/p&gt;

&lt;p&gt;The net effect: the individual procedures change — some get simpler, some get more complex, and new ones emerge. The need for an external system to coordinate them does not diminish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Market Is Heading
&lt;/h2&gt;

&lt;p&gt;The Iceberg management platform category is emerging because the open lakehouse reached a maturity inflection. Iceberg is the format — every major cloud and engine treats it as the default. The catalog layer has consolidated around the REST spec with Polaris, Nessie, and managed offerings. What remains unsolved is the operational layer above catalogs and engines: the layer that keeps tables healthy, queries fast, and costs controlled without requiring every team to become an Iceberg internals expert.&lt;/p&gt;

&lt;p&gt;The direction is visible from every side. Databricks acquired Tabular (the company founded by Iceberg's creators to build exactly this managed layer) and absorbed it into Predictive Optimization. Cyera paid $100–130M for Ryft, an Iceberg operations startup with $8M in seed funding — a security vendor paying nine figures for table-level operational intelligence, because governed, traceable data access cannot exist without maintained, well-managed tables underneath. Starburst launched "Icehouse LakeOps" as a named product for automated table health. SAP acquired Dremio, committing to Polaris and Iceberg as the foundation of its Business Data Cloud. Apache Amoro remains in incubation. Open-source projects are multiplying around the gap. And Iceberg v4 proposals include an adaptive metadata tree that would fold manifest consolidation into the format itself, potentially eliminating external &lt;code&gt;rewrite_manifests&lt;/code&gt; calls.&lt;/p&gt;

&lt;p&gt;The question is not whether Iceberg tables will be managed. It is whether that management intelligence lives inside a proprietary platform, gets assembled from scripts and tools by every team independently, or runs as a dedicated control plane that preserves the openness Iceberg was built to provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you are feeling the operational weight — slow queries traced to small files, storage costs climbing from orphan accumulation, maintenance DAGs consuming more engineering time than the pipelines they were supposed to enable — run the diagnostic runbook above against your most critical tables. Score them against the health criteria. If two or more tables show warnings or critical scores across multiple dimensions, the maintenance approach itself needs attention, not just the individual tables.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; connects to your existing catalogs and surfaces health classification immediately — often revealing degradation you knew existed but could not quantify.&lt;/p&gt;

&lt;p&gt;The adoption path is designed for progressive trust:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connect a catalog.&lt;/strong&gt; LakeOps connects to Glue, Polaris, Nessie, Gravitino, Lakekeeper, or S3 Tables through their standard APIs. No code changes, no SDK, no agents to deploy. Immediate visibility into every table's health, file distribution, manifest state, and snapshot history.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run manual maintenance.&lt;/strong&gt; Execute individual operations — expire snapshots, compact a single partition, rewrite manifests — and review the results. You control what runs and when.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Define policies at the namespace level.&lt;/strong&gt; Set compaction targets, retention windows, sort strategies, and maintenance cadence that new tables inherit automatically. Override specific tables when needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable &lt;a href="https://lakeops.dev/blog/iceberg-metadata-lifecycle-maintenance-optimization" rel="noopener noreferrer"&gt;adaptive maintenance&lt;/a&gt;.&lt;/strong&gt; The platform monitors health signals continuously and runs the right operations at the right time, in the correct dependency order, with conflict-safe execution. Tables that need attention get it immediately; tables that are healthy are left alone.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2Fy7zvvpl3qc52p21h89ub.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%2Fy7zvvpl3qc52p21h89ub.png" alt="Connected catalogs showing Glue, DynamoDB, REST, and S3 Tables — the platform attaches to the stack you already have without data movement" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The lake becomes self-maintaining. The on-call burden shifts from firefighting fragmentation and orphan accumulation to reviewing a structured event log. The platform team gets its capacity back for the work that actually produces value — building data products, not babysitting compaction jobs.&lt;/p&gt;

</description>
      <category>iceberg</category>
      <category>datalakehouse</category>
      <category>dataengineering</category>
      <category>opensource</category>
    </item>
    <item>
      <title>AWS Glue Iceberg Optimizer Alternatives for a Data Lakehouse</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Fri, 25 Sep 2026 09:34:47 +0000</pubDate>
      <link>https://dev.to/jonisar/aws-glue-iceberg-optimizer-alternatives-for-a-data-lakehouse-8f7</link>
      <guid>https://dev.to/jonisar/aws-glue-iceberg-optimizer-alternatives-for-a-data-lakehouse-8f7</guid>
      <description>&lt;p&gt;The AWS Glue Data Catalog's managed table optimizers are the path of least resistance for Iceberg maintenance on AWS. Three toggles, no Spark cluster to operate, $0.44 per DPU-hour. For a Glue-cataloged, Athena-queried, Parquet-only estate with batch write patterns, they are genuinely the right answer and this article is not trying to talk you out of them.&lt;/p&gt;

&lt;p&gt;The reason you are reading this is that something broke that assumption. Compaction cannot keep pace with streaming ingestion and suspends itself. Trino and Athena both hit the same tables and the sort order helps neither. You have three catalogs and the optimizer only sees one. Or you finally looked at query planning time and realized nothing in the Glue optimizer rewrites manifests, which means nothing ever has.&lt;/p&gt;

&lt;p&gt;This guide covers what the Glue optimizers actually do, with the real API parameters and documented defaults rather than the marketing summary; the specific constraints that cause teams to look elsewhere, including several that are genuine data-loss hazards rather than inconveniences; how to measure your real cost per terabyte instead of estimating it; and an honest survey of the alternatives, from Spark procedures through Flink, Trino, S3 Tables, the commercial platforms, four open-source projects, and dedicated control planes such as &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; that run maintenance as a coordinated loop across catalogs rather than as isolated 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%2Fp0geze7d7km08x2ak6ea.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%2Fp0geze7d7km08x2ak6ea.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One framing worth setting up front, because it changes the conclusion: since the Glue Iceberg REST Catalog endpoint shipped, this stopped being an either/or decision. You can keep Glue's managed compaction doing bulk binpack work and attach an external tool over the REST protocol for the operations Glue does not offer. That hybrid is usually the lowest-risk answer, and it is the one most comparisons never consider.&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%2Fqaxlg87jityn0as468aa.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%2Fqaxlg87jityn0as468aa.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Glue catalog only, Athena only, Parquet, batch writes, under ~50 tables&lt;/td&gt;
&lt;td&gt;Stay on the Glue optimizers. Add catalog-level defaults and stop there.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glue optimizer works, but planning is slow and manifests are never rewritten&lt;/td&gt;
&lt;td&gt;Supplement. Keep Glue compaction, add an external tool over the Glue REST catalog for &lt;code&gt;rewrite_manifests&lt;/code&gt; and statistics.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming ingestion; compaction keeps suspending itself&lt;/td&gt;
&lt;td&gt;Replace compaction specifically. The auto-suspend has no auto-resume, and AWS's documented mitigations require control Glue does not expose.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avro or ORC data files&lt;/td&gt;
&lt;td&gt;You cannot use Glue compaction at all. It is Parquet-only.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple catalogs, or multiple query engines with different filter patterns&lt;/td&gt;
&lt;td&gt;Replace with something that spans catalogs and sees cross-engine query telemetry.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-account or cross-Region tables, or us-west-1 / GovCloud&lt;/td&gt;
&lt;td&gt;Glue optimizers are unavailable. This is an architectural constraint, not a tuning problem.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What the Glue Optimizer Actually Does
&lt;/h2&gt;

&lt;p&gt;There are three optimizer types, and the API enum has not grown: &lt;code&gt;compaction&lt;/code&gt;, &lt;code&gt;retention&lt;/code&gt;, and &lt;code&gt;orphan_file_deletion&lt;/code&gt;. Two adjacent managed features are billed on the same meter and configured through the same catalog API, which is why they get mistaken for a fourth optimizer: &lt;strong&gt;column statistics generation&lt;/strong&gt;, which writes Iceberg Puffin files, and &lt;strong&gt;materialized view auto-refresh&lt;/strong&gt;. Neither is a &lt;code&gt;TableOptimizer&lt;/code&gt;, and the statistics one matters enough that it gets its own treatment below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compaction
&lt;/h3&gt;

&lt;p&gt;Merges small data files into larger ones using Iceberg's &lt;code&gt;rewriteDataFiles&lt;/code&gt;. The &lt;a href="https://docs.aws.amazon.com/glue/latest/dg/compaction-management.html" rel="noopener noreferrer"&gt;documented trigger&lt;/a&gt; is specific, and it is worth quoting because most write-ups paraphrase it into something vaguer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In the Data Catalog, the compaction process starts when a table or any of its partitions have more than 100 files. Each file must be smaller than 75% of the target file size.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three things that matter in that sentence. It is evaluated &lt;strong&gt;per partition&lt;/strong&gt;, not per table. The 100 is the default of a configurable parameter, not a constant. And the 75% is not an AWS invention, it is Iceberg's stock &lt;code&gt;min-file-size-bytes&lt;/code&gt; default surfacing through the managed wrapper.&lt;/p&gt;

&lt;p&gt;The real configuration surface is smaller than you might expect:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strategy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;binpack&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;binpack&lt;/code&gt;, &lt;code&gt;sort&lt;/code&gt;, or &lt;code&gt;z-order&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;minInputFiles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum data files in a partition before compaction acts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deleteFileThreshold&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum deletes in a data file to make it eligible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note what is &lt;strong&gt;not&lt;/strong&gt; there: there is no &lt;code&gt;targetFileSizeMB&lt;/code&gt; API parameter. Target file size is controlled only through the Iceberg table property &lt;code&gt;write.target-file-size-bytes&lt;/code&gt;, which defaults to 512 MB. This is a real difference from S3 Tables, which exposes target size directly in its maintenance API.&lt;/p&gt;

&lt;p&gt;Note also that &lt;code&gt;sort&lt;/code&gt; and &lt;code&gt;z-order&lt;/code&gt; require you to have already defined a &lt;code&gt;sort_order&lt;/code&gt; on the table. Glue will apply a sort order. It will never choose one, infer one, or update one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snapshot retention
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;snapshotRetentionPeriodInDays&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Table property takes precedence if set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;numberOfSnapshotsToRetain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Table property takes precedence if set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cleanExpiredFiles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;If &lt;code&gt;false&lt;/code&gt;, expiry is metadata-only and files stay on S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;runRateInHours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 to 168&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both criteria must be satisfied for a snapshot to survive: it must be within the retention period &lt;em&gt;and&lt;/em&gt; within the minimum count.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orphan file deletion
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;orphanFileRetentionPeriodInDays&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;location&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;table location&lt;/td&gt;
&lt;td&gt;Can be scoped to a sub-prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;runRateInHours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 to 168&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is a safety semantic here that almost nobody documents and that surprises people badly: &lt;strong&gt;the optimizer only deletes files created after the optimizer itself was created.&lt;/strong&gt; Files that predate the optimizer are never touched, regardless of age. If you enable orphan deletion on a table that has been accumulating cruft for two years, it will clean nothing until new orphans appear. Pre-existing orphans need a manual &lt;code&gt;remove_orphan_files&lt;/code&gt; run.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual CLI shape
&lt;/h3&gt;

&lt;p&gt;Configuration examples float around the internet with invented parameter names. The real structure nests the Iceberg config two levels deep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws glue create-table-optimizer &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--catalog-id&lt;/span&gt; 123456789012 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--database-name&lt;/span&gt; analytics &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-name&lt;/span&gt; events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; compaction &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-optimizer-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{
    "roleArn": "arn:aws:iam::123456789012:role/GlueOptimizerRole",
    "enabled": true,
    "vpcConfiguration": { "glueConnectionName": "analytics-vpc-connection" },
    "compactionConfiguration": {
      "icebergConfiguration": {
        "strategy": "sort",
        "minInputFiles": 50
      }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;vpcConfiguration&lt;/code&gt; is the under-documented piece: it is how you run optimizers against tables reachable only inside a customer VPC.&lt;/p&gt;
&lt;h3&gt;
  
  
  Catalog-level defaults, and two traps
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;UpdateCatalog&lt;/code&gt; lets you set optimizer defaults for a whole catalog, which is the only thing that makes this manageable past a few dozen tables. Precedence runs table-level config, then catalog-level config, then Iceberg table property.&lt;/p&gt;

&lt;p&gt;Two documented problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Catalog-level inheritance silently does not work when Data Catalog metadata encryption is enabled.&lt;/strong&gt; Every table must then be configured individually.&lt;/li&gt;
&lt;li&gt;AWS documents a &lt;strong&gt;known issue&lt;/strong&gt; where tables without their own configuration may fail to inherit the &lt;em&gt;disabled&lt;/em&gt; state from the catalog. Their own guidance is to audit the console and execution logs for optimizers running that you did not intend. That is a surprise-bill vector, not a cosmetic bug.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Pricing
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/glue/pricing/" rel="noopener noreferrer"&gt;$0.44 per DPU-hour&lt;/a&gt;, billed per second with a one-minute minimum per run. One DPU is 4 vCPU and 16 GB. AWS's worked example: 30 minutes on 2 DPUs costs $0.44.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Glue 6.0 price reduction does not apply to optimizers.&lt;/strong&gt; Glue 6.0 went GA in August 2026 with a headline 30% cut, and that cut is scoped to Spark ETL job DPU-hours on Glue 6.0. Table optimizers are not versioned, you cannot opt them into 6.0 pricing, and the optimizer meter still reads $0.44. Expect to see this conflated a lot over the next year.&lt;/p&gt;

&lt;p&gt;The one-minute minimum deserves more attention than it usually gets. A high-churn table that triggers many small compaction runs pays a floor of 60 seconds of DPU each time, and the reports of Glue compacting fewer than ten files per run several times an hour are exactly this failure mode: you are billed a minimum-duration run to do almost nothing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Which Iceberg version you get
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Glue 6.0&lt;/th&gt;
&lt;th&gt;Glue 5.1&lt;/th&gt;
&lt;th&gt;Glue 5.0&lt;/th&gt;
&lt;th&gt;Glue 4.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spark&lt;/td&gt;
&lt;td&gt;4.1.1&lt;/td&gt;
&lt;td&gt;3.5.6&lt;/td&gt;
&lt;td&gt;3.5.4&lt;/td&gt;
&lt;td&gt;3.3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iceberg&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.11.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.10.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.7.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.0.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Iceberg v3 deletion vectors and row lineage arrived in &lt;strong&gt;Glue 5.1&lt;/strong&gt;, not 6.0. Glue 6.0 added the remainder of v3: &lt;code&gt;VARIANT&lt;/code&gt; with shredding, geometry and geography types, and nanosecond timestamps. AWS Prescriptive Guidance explicitly lists Glue table maintenance as supporting v3, and states that after a v2 to v3 upgrade the next compaction removes the legacy v2 delete files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The blocker nobody mentions: Athena cannot read Iceberg v3 at all.&lt;/strong&gt; Glue 6.0 will happily write v3 tables that your Athena users cannot query. If Athena is in your read path, v3 is off the table regardless of what Glue supports, and since the v2 to v3 upgrade is atomic metadata-only but effectively irreversible once deletion vectors exist, this is a decision to make deliberately rather than discover.&lt;/p&gt;
&lt;h3&gt;
  
  
  Where it runs
&lt;/h3&gt;

&lt;p&gt;Glue optimizers are available in 15 regions. &lt;strong&gt;Not&lt;/strong&gt; available in us-west-1 (N. California) or any GovCloud region. If your lakehouse lives there, the evaluation is over before it starts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Measure Your Real Cost Before You Decide
&lt;/h2&gt;

&lt;p&gt;Almost every comparison of Glue optimizer costs against alternatives is built on estimates. It does not have to be, and this is the single most useful thing in this guide if you are trying to justify a change.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TableOptimizerRun&lt;/code&gt; returns an &lt;code&gt;IcebergCompactionMetrics&lt;/code&gt; struct containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dpu_hours&lt;/code&gt; — actual DPU-hours consumed by that run&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;number_of_dpus&lt;/code&gt; — DPUs allocated, rounded up&lt;/li&gt;
&lt;li&gt;&lt;code&gt;job_duration_in_hour&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;number_of_bytes_compacted&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;number_of_files_compacted&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So your real cost per terabyte is not a guess:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$/TB = (Σ dpu_hours × $0.44) / (Σ number_of_bytes_compacted / 2^40)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Pull the history with &lt;code&gt;ListTableOptimizerRuns&lt;/code&gt; and &lt;code&gt;BatchGetTableOptimizer&lt;/code&gt;, aggregate across your top tables for the last 30 days, and you have a defensible number instead of a vendor's. Do this before you evaluate anything, because it also tells you which tables are burning DPU-hours to accomplish very little, which is usually a more actionable finding than the total.&lt;/p&gt;
&lt;h3&gt;
  
  
  Do not forget the S3 request bill
&lt;/h3&gt;

&lt;p&gt;Compaction is read-many-small-files, write-few-large-files, plus listing. In us-east-1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PUT, COPY, POST and &lt;strong&gt;LIST&lt;/strong&gt;: $0.005 per 1,000&lt;/li&gt;
&lt;li&gt;GET: $0.0004 per 1,000&lt;/li&gt;
&lt;li&gt;DELETE: free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writes cost 12.5× more than reads per request, but reads dominate compaction because there are so many more of them, and they scale with &lt;strong&gt;input file count rather than bytes&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;GETs&lt;/th&gt;
&lt;th&gt;PUTs&lt;/th&gt;
&lt;th&gt;Request cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 TB as 1M × 1 MB files → 2,048 × 512 MB&lt;/td&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;2,048&lt;/td&gt;
&lt;td&gt;~$0.41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 TB as 10M × 100 KB files → 2,048&lt;/td&gt;
&lt;td&gt;10,000,000&lt;/td&gt;
&lt;td&gt;2,048&lt;/td&gt;
&lt;td&gt;~$4.01&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At ten million input files the S3 request bill rivals the compute bill. The modeling point that follows: &lt;strong&gt;compute cost scales with bytes, request cost scales with file count.&lt;/strong&gt; Any comparison that models only DPU-hours understates the cost of pathologically fragmented tables, which are precisely the tables you are trying to fix.&lt;/p&gt;

&lt;p&gt;LIST being billed at the PUT rate is also why orphan file deletion, a recursive prefix listing, is quietly the most expensive of the three optimizers on wide tables.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the Ceiling Is
&lt;/h2&gt;

&lt;p&gt;I have grouped the &lt;a href="https://docs.aws.amazon.com/glue/latest/dg/optimizer-notes.html" rel="noopener noreferrer"&gt;documented considerations and limitations&lt;/a&gt; by how much they should worry you, rather than by feature area. The first group can destroy data.&lt;/p&gt;
&lt;h3&gt;
  
  
  Correctness hazards
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Shared S3 locations cause cross-table deletion.&lt;/strong&gt; If two Data Catalog tables point at overlapping S3 locations and both have retention or orphan optimizers enabled, one table's optimizer will delete files the other table still references. AWS documents this as "unrecoverable data loss from unintended deletion." Every table needs a unique, non-overlapping prefix, including across databases. This is the single most dangerous thing on the limitations page and it is almost never mentioned in comparison articles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S3 lifecycle rules on Iceberg paths delete live files.&lt;/strong&gt; A lifecycle expiration rule that touches an Iceberg prefix will remove manifests and data files still referenced by current snapshots. Exclude Iceberg paths from lifecycle rules entirely. AWS's suggested backstop is S3 versioning, which helps you recover but does not prevent the incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one-million-file cap creates silent orphans.&lt;/strong&gt; Retention and orphan deletion each delete at most 1,000,000 files per run. When snapshot expiry exceeds that cap, the overflow does not error, it becomes orphan files that a later orphan-deletion pass has to find. That is a two-stage cleanup dependency across two independently scheduled optimizers, and nothing sequences it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orphan deletion ignores everything older than the optimizer.&lt;/strong&gt; Covered above, repeated here because it is the one that makes people think the optimizer is broken when it is behaving exactly as designed.&lt;/p&gt;
&lt;h3&gt;
  
  
  What the optimizer structurally does not do
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Consequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Manifest rewrite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Genuinely absent&lt;/td&gt;
&lt;td&gt;No API, no strategy, no mention anywhere. Manifest bloat degrades scan planning and is invisible in file-count metrics. This is the largest single gap.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sort order selection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sort&lt;/code&gt; and &lt;code&gt;z-order&lt;/code&gt; exist, but you must define &lt;code&gt;sort_order&lt;/code&gt; yourself and Glue never infers or evolves it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Partition statistics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;Iceberg 1.11 has &lt;code&gt;compute_partition_stats&lt;/code&gt;; there is no managed equivalent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standalone delete-file rewrite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deleteFileThreshold&lt;/code&gt; makes delete-heavy files eligible for &lt;em&gt;data&lt;/em&gt; compaction, which resolves deletes as a side effect. There is no &lt;code&gt;rewrite_position_delete_files&lt;/code&gt; equivalent; you cannot compact deletes without rewriting data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Manual or on-demand trigger&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;You cannot force a run, and you cannot prevent one from overlapping with your ETL window.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auto-resume after suspension&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;Manual re-enable only.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Non-Parquet compaction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;Parquet only.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target file size via API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;Table property only.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of these deserve expansion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parquet-only compaction&lt;/strong&gt; is buried on the compaction documentation page and absent from the limitations page, which is why nearly every comparison misses it. If any of your tables write Avro or ORC data files, Glue compaction simply does not apply to them. S3 Tables handles all three formats. This is a hard eligibility gate, not a performance consideration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Puffin statistics are a subtlety, not a gap.&lt;/strong&gt; It is tempting to write "Glue can't compute Iceberg statistics," and it would be wrong. Glue &lt;em&gt;does&lt;/em&gt; generate Puffin NDV sketches, through a separate column-statistics feature configured via &lt;code&gt;UpdateCatalog&lt;/code&gt; with &lt;code&gt;ColumnStatistics.Enabled&lt;/code&gt; and &lt;code&gt;ColumnStatistics.RoleArn&lt;/code&gt;. It runs weekly and samples 20% of records, and Athena needs &lt;code&gt;use_iceberg_statistics=true&lt;/code&gt; on the table to consume them. So the accurate statement is narrower: &lt;em&gt;the Glue table optimizer&lt;/em&gt; does not compute statistics, and the feature that does is on a separate schedule with no coupling to compaction. Since compaction is exactly what invalidates statistics, that decoupling is the actual problem. Your statistics can describe a file layout that a compaction run replaced four days ago.&lt;/p&gt;
&lt;h3&gt;
  
  
  No sequencing between the three
&lt;/h3&gt;

&lt;p&gt;The three optimizers run independently. There is no dependency graph and no way to configure one.&lt;/p&gt;

&lt;p&gt;Iceberg maintenance has a correct order, and it is not arbitrary. Expire snapshots first, so the later steps operate on a smaller file set and you are not rewriting files that are about to be dereferenced. Remove orphans next, including the ones expiry just created. Compact after that, against a clean current file set. Rewrite manifests last, so the metadata tree indexes the layout that now exists. Refresh statistics after that, so the optimizer's estimates match the files that will actually be read.&lt;/p&gt;

&lt;p&gt;Glue gives you three of those five operations, on three independent schedules, in whatever order they happen to fire. The concrete waste is compaction rewriting files that snapshot expiry dereferences an hour later, and orphan cleanup running before expiry finishes and missing everything it was supposed to catch.&lt;/p&gt;
&lt;h3&gt;
  
  
  Architectural boundaries
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single catalog.&lt;/strong&gt; Only tables in the Glue Data Catalog. REST catalogs (Polaris, Nessie, Gravitino, Lakekeeper), Hive Metastore, and anything else get nothing. Production lakehouses increasingly span several, and each additional catalog means another maintenance story with its own blind spots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cross-account tables and no cross-Region tables.&lt;/strong&gt; Cross-account &lt;em&gt;buckets&lt;/em&gt; work if the role has access, but cross-account &lt;em&gt;tables&lt;/em&gt; and resource links do not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No S3 Express One Zone.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Z-order does not support &lt;code&gt;Decimal&lt;/code&gt; or &lt;code&gt;TimestampWithoutZone&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability is CloudWatch job telemetry.&lt;/strong&gt; Success, failure, duration, DPU usage. It tells you a job ran. It does not tell you whether the table is healthy: file size distribution, manifest depth, delete-file accumulation, snapshot growth relative to write velocity, or whether the sort order still matches how the table is queried. Diagnosing that means querying Iceberg metadata tables in Athena per table and correlating manually, which is tractable at 20 tables and is not at 500.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The Concurrency Problem
&lt;/h2&gt;

&lt;p&gt;This is the failure mode that pushes the most teams off the managed optimizer, so it is worth understanding precisely rather than as "compaction sometimes fails."&lt;/p&gt;
&lt;h3&gt;
  
  
  What actually happens
&lt;/h3&gt;

&lt;p&gt;Reports from teams running hourly &lt;code&gt;MERGE&lt;/code&gt; jobs alongside Glue auto-compaction describe roughly one in ten compaction runs failing with:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compaction optimizer failed. Error: partial-progress.enabled is true but no rewrite
commit succeeded. Check the logs to determine why the individual commits failed. If
this is persistent it may help to increase partial-progress.max-commits which will
break the rewrite operation into smaller commits.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That message leaks two things AWS does not document anywhere else.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;Glue's managed compaction runs &lt;code&gt;rewrite_data_files&lt;/code&gt; with &lt;code&gt;partial-progress.enabled = true&lt;/code&gt;.&lt;/strong&gt; That is otherwise invisible internal behavior, and it explains a lot about the observed run patterns.&lt;/p&gt;

&lt;p&gt;Second, and this is the part that makes it maddening, &lt;strong&gt;the remediation the error recommends is not available to you.&lt;/strong&gt; &lt;code&gt;partial-progress.max-commits&lt;/code&gt; is not exposed through the optimizer API, and the managed Spark job is not accessible. The error tells you to turn a dial you cannot reach.&lt;/p&gt;
&lt;h3&gt;
  
  
  Then it gives up
&lt;/h3&gt;

&lt;p&gt;From the AWS documentation, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When compaction operations fail four consecutive times, AWS Glue catalog table optimization automatically suspends the optimizer to prevent unnecessary compute resource consumption.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Re-enabling is manual, through the console or API. There is no auto-resume, no documented backoff curve, and no alarm. A suspended optimizer is silent, and the table degrades in the background until somebody notices the query latency. If you run Glue compaction on tables with concurrent writers, build a CloudWatch alarm on optimizer state right now, independently of anything else in this article.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why the usual advice does not work
&lt;/h3&gt;

&lt;p&gt;The standard internet recommendation for Iceberg commit conflicts is to drop from serializable to snapshot isolation. AWS states plainly, in its own &lt;a href="https://aws.amazon.com/blogs/big-data/manage-concurrent-write-conflicts-in-apache-iceberg-on-the-aws-glue-data-catalog/" rel="noopener noreferrer"&gt;guidance on concurrent write conflicts&lt;/a&gt;, that this does not help here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For conflicts between streaming ingestion and compaction operations, which is one of the most common scenarios, snapshot isolation does not provide any additional benefits to the default serializable isolation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The two conflict classes behave differently and only one is retryable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Catalog commit conflict&lt;/th&gt;
&lt;th&gt;Data update conflict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exception&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CommitFailedException&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ValidationException&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cause&lt;/td&gt;
&lt;td&gt;Concurrent metadata pointer update&lt;/td&gt;
&lt;td&gt;Validation detected an overlapping data change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-retryable&lt;/td&gt;
&lt;td&gt;Yes, Iceberg retries the metadata commit&lt;/td&gt;
&lt;td&gt;No, table state changed and retry safety is undecidable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix&lt;/td&gt;
&lt;td&gt;Table properties&lt;/td&gt;
&lt;td&gt;Application-level retry with backoff and jitter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://iceberg.apache.org/docs/latest/configuration/" rel="noopener noreferrer"&gt;Iceberg-side properties&lt;/a&gt; you can actually set, with their defaults:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;commit.retry.num-retries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;commit.retry.min-wait-ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;commit.retry.max-wait-ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;commit.retry.total-timeout-ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1800000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AWS's recommended baseline for tables under frequent concurrent writes raises retries to 10 with a 10-second max wait; for maintenance operations they suggest keeping 4 retries but raising the minimum wait to 1,000 ms. Set these as table properties and Glue's compaction will honor them, since it is running stock Iceberg underneath.&lt;/p&gt;
&lt;h3&gt;
  
  
  The structural fix
&lt;/h3&gt;

&lt;p&gt;Retry tuning treats the symptom. The structural fix is to not attempt compaction on a partition that has an active writer, and to make the compaction window short enough that overlap is unlikely in the first place. Glue exposes neither: you cannot scope compaction with a &lt;code&gt;WHERE&lt;/code&gt; predicate through the managed optimizer, and you cannot control when it runs. AWS's own documented mitigation is to serialize your writes against compaction, which negates most of the value of "automatic."&lt;/p&gt;
&lt;h2&gt;
  
  
  What a Replacement Actually Needs
&lt;/h2&gt;

&lt;p&gt;Before looking at options, it helps to name the criteria the gaps above imply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sequenced operations&lt;/strong&gt;, where each step's output feeds the next, rather than three independent schedulers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifest rewrite and statistics refresh&lt;/strong&gt; as first-class operations, coupled to compaction rather than on separate cadences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query-aware layout&lt;/strong&gt;, where sort keys come from the columns queries actually filter and join on, across every engine hitting the table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health-driven triggers&lt;/strong&gt; based on table state, not fixed file counts, so a streaming table with 40,000 small files gets attention before a batch table slightly under target&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writer-aware conflict handling&lt;/strong&gt;, excluding hot partitions and backing off rather than failing four times and suspending&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-catalog reach&lt;/strong&gt;, because the catalog boundary is an accident of history, not a meaningful maintenance boundary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table health observability&lt;/strong&gt;, not job telemetry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An execution engine sized for the work&lt;/strong&gt;, since compaction is I/O-bound read-merge-write and a general-purpose JVM cluster is structurally over-provisioned for it&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/..." 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/..." alt="Uploading image" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The category this describes
&lt;/h3&gt;

&lt;p&gt;What that list adds up to is not a better compaction job. It is a control loop, and nothing in the open lakehouse stack ships one.&lt;/p&gt;

&lt;p&gt;A lakehouse control plane is the layer that runs it. It sits above the components you already have rather than replacing any of them: your catalogs, your Iceberg tables in S3, and your query engines all stay where they are. It attaches through standard catalog and Iceberg metadata APIs, so there is no data movement, no pipeline rewrite, and no change to the table format, which also means leaving is a matter of disconnecting it.&lt;/p&gt;

&lt;p&gt;The loop has four steps. &lt;strong&gt;Sense&lt;/strong&gt; table structure and query telemetry across every connected catalog and engine. &lt;strong&gt;Classify&lt;/strong&gt; each table's health from those signals. &lt;strong&gt;Plan&lt;/strong&gt; the operations and physical layout each table needs, in dependency order. &lt;strong&gt;Execute&lt;/strong&gt;, then measure the result and feed it back into the next decision.&lt;/p&gt;

&lt;p&gt;Glue's optimizers implement a fragment of the execute step. Everything else, the sensing, the classification, the sequencing, and the learning, is the part you are currently doing by hand or not at all. Hold that model while reading the options below, because it is the axis they actually differ on.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Alternatives Landscape
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Spark procedures: the DIY baseline
&lt;/h3&gt;

&lt;p&gt;Calling &lt;code&gt;rewrite_data_files&lt;/code&gt;, &lt;code&gt;rewrite_manifests&lt;/code&gt;, &lt;code&gt;expire_snapshots&lt;/code&gt;, &lt;code&gt;remove_orphan_files&lt;/code&gt;, and &lt;code&gt;compute_table_stats&lt;/code&gt; yourself gives you complete control and complete operational ownership. Every capability on the criteria list is achievable. You are building the scheduler, the health signals, the conflict handling, the sequencing, and the observability.&lt;/p&gt;

&lt;p&gt;Worth knowing: Iceberg's own &lt;code&gt;min-input-files&lt;/code&gt; default is &lt;strong&gt;5&lt;/strong&gt;, while Glue overrides it to 100. If your tables feel under-compacted on Glue relative to a Spark job you used to run, that difference is probably why.&lt;/p&gt;

&lt;p&gt;This is where most teams land after outgrowing the managed optimizer, and it trades a maintenance problem for a platform-engineering problem. Whether that is a good trade depends entirely on whether you have the headcount to own it properly. The honest failure mode is not that it does not work, it is that it works until the person who built it changes teams.&lt;/p&gt;
&lt;h3&gt;
  
  
  Trino: the underrated option if you already run it
&lt;/h3&gt;

&lt;p&gt;Trino's &lt;a href="https://trino.io/docs/current/connector/iceberg.html" rel="noopener noreferrer"&gt;Iceberg connector&lt;/a&gt; exposes maintenance through &lt;code&gt;ALTER TABLE ... EXECUTE&lt;/code&gt;, and it covers something Glue cannot do at all:&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;analytics&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;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_size_threshold&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'100MB'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-09-01'&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;analytics&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;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;optimize_manifests&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;analytics&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;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;expire_snapshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retention_threshold&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'7d'&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;analytics&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;EXECUTE&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="n"&gt;retention_threshold&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'7d'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three points in Trino's favor. &lt;code&gt;optimize&lt;/code&gt; accepts a &lt;code&gt;WHERE&lt;/code&gt; clause, which is exactly the partition-scoped compaction AWS recommends and Glue does not expose. &lt;code&gt;optimize_manifests&lt;/code&gt; clusters manifests by partitioning columns, closing the biggest Glue gap. And the retention floors (&lt;code&gt;iceberg.expire-snapshots.min-retention&lt;/code&gt; and &lt;code&gt;iceberg.remove-orphan-files.min-retention&lt;/code&gt;, both &lt;code&gt;7d&lt;/code&gt;) hard-fail rather than letting you delete live files, which is a real safety property Glue lacks.&lt;/p&gt;

&lt;p&gt;The catch for AWS shops: &lt;strong&gt;Athena is Trino-derived but does not expose &lt;code&gt;optimize_manifests&lt;/code&gt;.&lt;/strong&gt; You need actual Trino, and you are then running maintenance on your query cluster, competing with user workloads.&lt;/p&gt;
&lt;h3&gt;
  
  
  Flink TableMaintenance: only if Flink is already yours
&lt;/h3&gt;

&lt;p&gt;Flink can run maintenance inside the streaming job that writes the data, which eliminates a separate cluster and makes maintenance naturally writer-aware:&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="nc"&gt;IcebergSink&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forRowData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataStream&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tableLoader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableLoader&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rewriteDataFiles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RewriteDataFilesConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MAX_BYTES&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1073741824"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expireSnapshots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;ExpireSnapshotsConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RETAIN_LAST&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;ExpireSnapshotsConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MAX_SNAPSHOT_AGE_SECONDS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"604800"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteOrphanFiles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DeleteOrphanFilesConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MIN_AGE_SECONDS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"259200"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Available tasks are &lt;code&gt;ExpireSnapshots&lt;/code&gt;, &lt;code&gt;RewriteDataFiles&lt;/code&gt;, and &lt;code&gt;DeleteOrphanFiles&lt;/code&gt;. Scheduling triggers include &lt;code&gt;scheduleOnCommitCount&lt;/code&gt;, &lt;code&gt;scheduleOnDataFileCount&lt;/code&gt;, &lt;code&gt;scheduleOnDataFileSize&lt;/code&gt;, and several delete-file variants. &lt;strong&gt;None of them have defaults, so nothing is scheduled until you set one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two accuracy notes, because this area moves fast. &lt;code&gt;ConvertEqualityDeletes&lt;/code&gt;, which resolves Flink's equality delete backlog into v3 deletion vectors through a staging branch, is real and genuinely important for CDC pipelines, but as of now it appears only in Iceberg's nightly documentation and not the stable release. And &lt;strong&gt;&lt;code&gt;RewriteManifests&lt;/code&gt; is not merged&lt;/strong&gt; for Flink; the pull request is open and committers have leaned toward waiting for native metadata compaction in Iceberg v4. So Flink maintenance does not close the manifest gap either.&lt;/p&gt;
&lt;h3&gt;
  
  
  Amazon S3 Tables
&lt;/h3&gt;

&lt;p&gt;The most hands-off option, and the defaults are the opposite of Glue's: &lt;strong&gt;all maintenance is enabled by default&lt;/strong&gt;, so you are being billed for compaction from the moment you create a table.&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;Property&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Minimum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compaction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;targetFileSizeMB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512 MB&lt;/td&gt;
&lt;td&gt;64 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot management&lt;/td&gt;
&lt;td&gt;&lt;code&gt;minSnapshotsToKeep&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot management&lt;/td&gt;
&lt;td&gt;&lt;code&gt;maxSnapshotAgeHours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unreferenced file removal&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unreferencedDays&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unreferenced file removal&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nonCurrentDays&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Strategy accepts &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;binpack&lt;/code&gt;, &lt;code&gt;sort&lt;/code&gt;, or &lt;code&gt;z-order&lt;/code&gt;, and &lt;code&gt;auto&lt;/code&gt; picks &lt;code&gt;sort&lt;/code&gt; when the table has a sort order defined and &lt;code&gt;binpack&lt;/code&gt; otherwise. That is a genuinely better default than Glue's unconditional &lt;code&gt;binpack&lt;/code&gt;. S3 Tables also compacts Avro and ORC, which Glue does not, and it exposes target file size directly.&lt;/p&gt;

&lt;p&gt;The pricing model is where it gets interesting, and it changed substantially after AWS cut rates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Charge&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;$0.0265/GB/month, roughly 15% above S3 Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction, data&lt;/td&gt;
&lt;td&gt;$0.005/GB processed (binpack)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction, objects&lt;/td&gt;
&lt;td&gt;$0.002 per 1,000 objects processed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Object monitoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.025 per 1,000 objects per month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Object monitoring is the one to watch.&lt;/strong&gt; It is a recurring per-object charge on everything in the bucket, which means a fragmented table is penalized twice: once at compaction time and then forever on monitoring. It penalizes exactly the condition compaction exists to fix. AWS's own example puts a 1 TB, 10,486-object table at $0.26/month monitoring against a $27.14 storage bill, which is small, but it scales with object count rather than bytes.&lt;/p&gt;

&lt;p&gt;AWS's pricing text qualifies the $0.005/GB rate as applying to "default binpack compaction," which implies &lt;code&gt;sort&lt;/code&gt; and &lt;code&gt;z-order&lt;/code&gt; cost more. I could not find a published rate for those. Verify before modeling a sorted-table budget.&lt;/p&gt;

&lt;p&gt;Other constraints: 10 table buckets per account per Region by default, 10,000 tables per bucket, namespaces cannot nest, Parquet row group size is fixed at 128 MB, and data type &lt;code&gt;Fixed&lt;/code&gt; plus &lt;code&gt;brotli&lt;/code&gt; and &lt;code&gt;lz4&lt;/code&gt; compression are unsupported. The only way to stop compaction charges is to explicitly disable compaction.&lt;/p&gt;

&lt;p&gt;On throughput, published comparisons put S3 Tables managed compaction well behind alternatives on the same data, which matters less if you are optimizing for zero operational involvement and matters a lot if compaction needs to keep up with ingestion.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Glue Iceberg REST catalog changes the question
&lt;/h3&gt;

&lt;p&gt;This is the strategic point most comparisons miss entirely.&lt;/p&gt;

&lt;p&gt;Glue exposes an Iceberg REST Catalog endpoint at &lt;code&gt;https://glue.{region}.amazonaws.com/iceberg&lt;/code&gt;, configured with &lt;code&gt;type=rest&lt;/code&gt;, &lt;code&gt;warehouse=&amp;lt;account-id&amp;gt;&lt;/code&gt;, &lt;code&gt;rest.sigv4-enabled=true&lt;/code&gt;, &lt;code&gt;rest.signing-name=glue&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;catalog.type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;rest&lt;/span&gt;
&lt;span class="py"&gt;catalog.uri&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://glue.us-east-1.amazonaws.com/iceberg&lt;/span&gt;
&lt;span class="py"&gt;catalog.warehouse&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;123456789012&lt;/span&gt;
&lt;span class="py"&gt;catalog.rest.sigv4-enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;catalog.rest.signing-name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;glue&lt;/span&gt;
&lt;span class="py"&gt;catalog.rest.signing-region&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That decouples engine from catalog. PyIceberg, Trino, Flink, DuckDB, Snowflake, or a purpose-built maintenance daemon can all reach Glue-cataloged tables over a standard protocol while Glue's managed optimizers keep running.&lt;/p&gt;

&lt;p&gt;Which means "Glue optimizer versus an alternative" is often the wrong framing. A perfectly reasonable production setup keeps Glue compaction doing bulk binpack on the tables it handles well, and attaches an external tool over REST for manifest rewriting, statistics refresh, query-aware sort, and the tables Glue cannot touch. You are not migrating off Glue, you are filling in what it does not do.&lt;/p&gt;
&lt;h3&gt;
  
  
  Engine-integrated maintenance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Boundary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Dremio&lt;/strong&gt; Automatic Optimization&lt;/td&gt;
&lt;td&gt;Five operations in one pass: compaction, delete-file and DV resolution, clustering, partition-evolution alignment, and &lt;strong&gt;manifest rewriting&lt;/strong&gt;. Partition-aware, workload-timed, roughly 3h optimize and 24h vacuum cadence&lt;/td&gt;
&lt;td&gt;Only tables in Dremio's catalog; target file size not per-table configurable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Starburst&lt;/strong&gt; Galaxy / Enterprise&lt;/td&gt;
&lt;td&gt;Trino-native, scoped at table, schema, or catalog. Compaction, snapshot expiry, orphan cleanup, statistics&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Schedule-driven, not health-driven.&lt;/strong&gt; No table-health signal decides when to run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Databricks&lt;/strong&gt; Predictive Optimization&lt;/td&gt;
&lt;td&gt;Automatic &lt;code&gt;OPTIMIZE&lt;/code&gt;, &lt;code&gt;VACUUM&lt;/code&gt;, &lt;code&gt;ANALYZE&lt;/code&gt; on Unity Catalog managed tables, Delta and Iceberg. Liquid Clustering selects clustering columns from query telemetry&lt;/td&gt;
&lt;td&gt;Requires Unity Catalog; telemetry is scoped to Databricks workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Snowflake&lt;/strong&gt; managed Iceberg&lt;/td&gt;
&lt;td&gt;Data compaction plus &lt;strong&gt;manifest compaction&lt;/strong&gt;, which cannot be disabled. &lt;code&gt;TARGET_FILE_SIZE&lt;/code&gt; of &lt;code&gt;AUTO&lt;/code&gt; or 16/32/64/128 MB, settable at account through table scope&lt;/td&gt;
&lt;td&gt;See the billing cliff below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Cloudera&lt;/strong&gt; Lakehouse Optimizer&lt;/td&gt;
&lt;td&gt;Policy-based with schedule and event triggers; Spark binpack, sort, z-order&lt;/td&gt;
&lt;td&gt;Cloudera ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Snowflake's arrangement has a trap worth flagging. For &lt;code&gt;SNOWFLAKE_MANAGED&lt;/code&gt; external volume tables written only by Snowflake, compaction is bundled at no additional cost. Once an external engine performs DML or DDL through the Iceberg REST Catalog on or after 21 May 2026, that table begins incurring compaction credits permanently. One external write flips a free table into a billed one, and you can see it in &lt;code&gt;ICEBERG_STORAGE_OPTIMIZATION_HISTORY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The shared limitation across this whole category: each platform sees only its own workloads. In a multi-engine stack you get fragmented maintenance with no cross-engine coordination, which is the same problem as Glue with a nicer interface.&lt;/p&gt;

&lt;p&gt;For completeness, since it still comes up: &lt;strong&gt;there is no standalone Tabular product.&lt;/strong&gt; Tabular was acquired by Databricks in 2024 and its managed-maintenance thesis now lives inside Unity Catalog and Predictive Optimization.&lt;/p&gt;
&lt;h3&gt;
  
  
  Open source
&lt;/h3&gt;

&lt;p&gt;This category is young and moving fast, so here is where each project actually stands rather than how its README reads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/nssalian/floe" rel="noopener noreferrer"&gt;floe&lt;/a&gt;&lt;/strong&gt; — Java, Apache 2.0. A policy-based &lt;em&gt;orchestrator&lt;/em&gt; rather than an engine: it discovers tables, scores health into a "debt score" from snapshot count, small-file percentage, and delete ratio, then dispatches &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;, and &lt;strong&gt;&lt;code&gt;REWRITE_MANIFESTS&lt;/code&gt;&lt;/strong&gt; to Spark via Livy or to Trino. Multi-catalog across REST (Polaris, Lakekeeper, Gravitino), Hive, and Nessie, with a web UI. The most complete conceptual model of the four. Pre-1.0, and development has been quiet for several months.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/basekick-labs/firn" rel="noopener noreferrer"&gt;firn&lt;/a&gt;&lt;/strong&gt; — &lt;strong&gt;Go&lt;/strong&gt;, not Rust, despite how it is frequently described. A JVM-free daemon that shells out to DuckDB for compaction. Binpack, sort, z-order, snapshot expiry, orphan cleanup, across Lakekeeper, &lt;strong&gt;AWS Glue&lt;/strong&gt;, Polaris, and Nessie, on S3, GCS, and Azure. Pre-1.0 and currently stale. Its README's claim of a "20-30x cost premium" for S3 Tables traces to a competitor's benchmark and predates the S3 Tables price cut; do not repeat it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/hupe1980/bergman" rel="noopener noreferrer"&gt;bergman&lt;/a&gt;&lt;/strong&gt; — Rust, single binary on &lt;code&gt;iceberg-rust&lt;/code&gt; and DataFusion, no JVM. Notable for implementing &lt;strong&gt;manifest rewrite and dangling delete-file removal&lt;/strong&gt; in its own commit layer, on the correct observation that nothing else cleans those up. Triggered rather than scheduled, with Prometheus metrics and a daemon mode. Actively developed but effectively a solo pre-release project; interesting design, not something to put in front of production data yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/nimtable/iceberg-compaction" rel="noopener noreferrer"&gt;nimtable/iceberg-compaction&lt;/a&gt;&lt;/strong&gt; — Rust on DataFusion and &lt;code&gt;iceberg-rust&lt;/code&gt;, handling positional and equality deletes. The strongest community signal of the four and the most actively maintained. Scope is narrower than it looks: full-table compaction only today, with partial and incremental compaction, binpack/sort/zorder strategies, and orphan deletion all still unchecked roadmap items.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these provide cross-engine query telemetry or lake-wide table health scoring. They are good options if you want to own the infrastructure and avoid another vendor, provided you are honest about maturity.&lt;/p&gt;

&lt;p&gt;A structural note on where this is heading: Apache Polaris has become the de facto neutral catalog and explicitly does &lt;strong&gt;not&lt;/strong&gt; execute maintenance, which is the gap these projects are racing to fill. Meanwhile Iceberg v4 discussions include native metadata compaction, which is part of why the Flink &lt;code&gt;RewriteManifests&lt;/code&gt; work stalled. Manifest maintenance is a real gap today that the format itself may eventually close.&lt;/p&gt;
&lt;h2&gt;
  
  
  How a Control Plane Closes Each Gap
&lt;/h2&gt;

&lt;p&gt;Taking the control-plane model from earlier and making it concrete against the specific Glue limitations above. &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; connects to the Glue Data Catalog through standard IAM credentials and runs alongside it, so Glue stays your catalog and your ETL engine.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/irRsF9VYP20" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;
&lt;h3&gt;
  
  
  Sequencing, as an actual dependency chain
&lt;/h3&gt;

&lt;p&gt;The five-step order matters and Glue has no mechanism to express it. LakeOps runs snapshot expiration, then orphan cleanup, then compaction, then manifest optimization, with each step's output feeding the next. Cadence adapts per table to write velocity, so a streaming table may run the full cycle several times an hour while a weekly batch table runs once and an idle table is skipped entirely. The &lt;a href="https://lakeops.dev/blog/automating-iceberg-table-maintenance" rel="noopener noreferrer"&gt;sequencing model&lt;/a&gt; is worth reading if you are building this yourself, because the ordering constraints are the part that is easy to get subtly wrong.&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%2Fmplwn6o3wk94w8a4b0p3.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%2Fmplwn6o3wk94w8a4b0p3.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That also resolves the statistics decoupling described earlier. Puffin statistics are refreshed as part of the same chain, after the file layout changes, rather than on an independent weekly schedule that has no idea compaction happened.&lt;/p&gt;
&lt;h3&gt;
  
  
  Manifest rewrite as a first-class operation
&lt;/h3&gt;

&lt;p&gt;The largest Glue gap is simply a supported operation here, with its own policy type rather than a Spark script you maintain separately. Because it is sequenced after compaction, the rewritten manifests index the file set that actually exists, which is the condition that makes manifest rewriting worth doing at all.&lt;/p&gt;
&lt;h3&gt;
  
  
  Query-aware sort instead of a sort order you guessed
&lt;/h3&gt;

&lt;p&gt;Glue will apply a &lt;code&gt;sort_order&lt;/code&gt;. It will never tell you what it should be. That question is unanswerable from inside any single engine, because the right layout depends on how Athena, Trino, Spark, and everything else query the table simultaneously.&lt;/p&gt;

&lt;p&gt;LakeOps aggregates query telemetry across the connected engines, the &lt;a href="https://lakeops.dev/blog/aws-glue-iceberg-optimization" rel="noopener noreferrer"&gt;AWS Glue integration&lt;/a&gt; covering Athena, Trino, Spark, Redshift, DuckDB, Snowflake, and Flink, and builds a per-table field-access profile from filter, join, and group-by frequency. Compaction then applies the sort order that maximizes min/max data skipping for that actual mix, and the strategy updates as access patterns move. When a new dashboard starts filtering on a different column, the next compaction pass reflects it rather than waiting for someone to notice.&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%2F7zug49osdkj9pg48wsrv.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%2F7zug49osdkj9pg48wsrv.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before a sort-order change touches production files, the candidate layout is validated on an Iceberg branch and compared against the baseline on real metadata, so a rewrite that would not improve scan reduction does not happen. That matters because sort-order changes rewrite every file in scope and are expensive to reverse.&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%2F077k0brci50qllw70280.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%2F077k0brci50qllw70280.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Writer-aware conflict handling instead of suspension
&lt;/h3&gt;

&lt;p&gt;This is the most direct answer to the auto-suspend problem. Rather than attempting compaction and failing four times, LakeOps monitors per-partition commit activity across connected engines and &lt;strong&gt;excludes partitions with active writers from the compaction scope&lt;/strong&gt;, retrying them on a later cycle once commit frequency drops. When a commit does lose an optimistic-concurrency race, it backs off, re-evaluates partition state, and reschedules rather than retrying immediately into the same conflict.&lt;/p&gt;

&lt;p&gt;There is also a mechanical contribution: a compaction pass that finishes in 221 seconds instead of 1,612 on the same 200 GB overlaps a far smaller window with your writers, which lowers conflict probability before any scheduling logic is involved. The &lt;a href="https://lakeops.dev/blog/iceberg-commit-conflicts" rel="noopener noreferrer"&gt;commit conflict mechanics&lt;/a&gt; go deeper into why treating conflicts as expected behavior rather than as failures is the right operating model for multi-writer tables.&lt;/p&gt;

&lt;p&gt;learn more:&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/blog/category/compaction" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Fblog%2Fcategory%2Fcompaction%2Fopengraph-image%3F054201187d6d134f" height="630" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/blog/category/compaction" rel="noopener noreferrer" class="c-link"&gt;
            Compaction Articles - LakeOps Blog
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Iceberg compaction strategies, engines, and automation — binpack, sort, Z-order, streaming compaction, and production benchmarks.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  Health-based triggers instead of a fixed file count
&lt;/h3&gt;

&lt;p&gt;Glue's trigger is 100 files under 75% of target, per partition. That is a reasonable heuristic and it is the same heuristic for every table you 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fee5ym8kd6o9nuaj8c2gp.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%2Fee5ym8kd6o9nuaj8c2gp.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LakeOps evaluates each table across file count and size distribution, manifest depth, snapshot accumulation, delete-file ratio, partition skew, and sort-order alignment with observed query patterns, then prioritizes the most degraded tables first. The last signal has no equivalent in any threshold-based system, because it is not a property of the table at all. It is a property of the relationship between the table and its workload, and it degrades without a single byte being written.&lt;/p&gt;

&lt;h3&gt;
  
  
  An execution engine sized for the job
&lt;/h3&gt;

&lt;p&gt;Compaction is I/O-bound read-merge-write. Running it on a general-purpose JVM cluster means JVM startup, GC pauses, executor provisioning, and idle capacity, compounded across hundreds of runs per day. LakeOps runs &lt;a href="https://lakeops.dev/solutions/compaction" rel="noopener noreferrer"&gt;compaction on a Rust and DataFusion engine&lt;/a&gt; with bounded memory and disk spill instead. Published benchmarks on a 200 GB table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Spark&lt;/th&gt;
&lt;th&gt;S3 Tables&lt;/th&gt;
&lt;th&gt;LakeOps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200 GB binpack&lt;/td&gt;
&lt;td&gt;1,612s&lt;/td&gt;
&lt;td&gt;6,300s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;221s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak throughput&lt;/td&gt;
&lt;td&gt;~350 MB/s&lt;/td&gt;
&lt;td&gt;~32 MB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,522 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per TB&lt;/td&gt;
&lt;td&gt;~$50&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory model&lt;/td&gt;
&lt;td&gt;JVM heap, OOM risk&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;td&gt;Bounded, no OOM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two production results from the same benchmark set: a 1,192 GB table that OOM'd Spark completed in 11 minutes, and a high-ingestion table went from 42,633 files to 69 in 138 seconds. Across 5.5 TB the file count dropped from 101,223 to 19,170. The &lt;a href="https://lakeops.dev/blog/replace-spark-iceberg-compaction" rel="noopener noreferrer"&gt;full Spark replacement analysis&lt;/a&gt; has the methodology.&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%2Fjxx7osdbk8bf459a62m0.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%2Fjxx7osdbk8bf459a62m0.png" alt=" " width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Set that against the &lt;code&gt;$/TB&lt;/code&gt; number you calculated from &lt;code&gt;dpu_hours&lt;/code&gt; earlier. That comparison is the whole business case, and you now have both sides of it measured rather than estimated. The broader &lt;a href="https://lakeops.dev/solutions/cost-optimization" rel="noopener noreferrer"&gt;cost picture&lt;/a&gt; also includes the storage side, since snapshot expiry and orphan cleanup reclaim bloat that never shows up in a compaction bill.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple catalogs, one loop
&lt;/h3&gt;

&lt;p&gt;Glue, REST catalogs (Polaris, Nessie, Gravitino, Lakekeeper), Hive Metastore, and S3 Tables are connected simultaneously, with every table discovered, health-scored, and maintained under consistent policies. If you run Glue for analytics, a REST catalog for streaming, and S3 Tables for a newer project, the catalog boundary stops being a maintenance boundary.&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%2Fal00qw8o9oekkq80w83b.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%2Fal00qw8o9oekkq80w83b.png" alt=" " width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Policies instead of per-table CLI calls
&lt;/h3&gt;

&lt;p&gt;Maintenance configuration cascades catalog-wide, then namespace, then table, with more specific policies overriding broader defaults. New tables inherit the right configuration from their namespace automatically. Compared with managing &lt;code&gt;create-table-optimizer&lt;/code&gt; calls per table, and with the catalog-level inheritance caveats documented on the Glue side, this is the difference between configuration you can reason about and configuration scattered across an account.&lt;/p&gt;

&lt;p&gt;Create a policy:&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%2Fi27dhvv1z452uo0ob8za.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%2Fi27dhvv1z452uo0ob8za.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And let it run:&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%2Fqs4rf2l1ikpzajo33row.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%2Fqs4rf2l1ikpzajo33row.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Table health, not job telemetry
&lt;/h3&gt;

&lt;p&gt;Every maintenance operation is logged with before-and-after metrics: file counts, bytes reclaimed, duration, and the signal that triggered it. Severity-ranked insights flag excessive manifests, partition skew, and small-file buildup before query latency moves. The &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;platform overview&lt;/a&gt; covers how observability, maintenance, compaction, governance, and routing compose into the single 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh7g6y7zazni06h56molh.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%2Fh7g6y7zazni06h56molh.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-Side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;AWS Glue optimizer&lt;/th&gt;
&lt;th&gt;S3 Tables&lt;/th&gt;
&lt;th&gt;LakeOps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compaction strategies&lt;/td&gt;
&lt;td&gt;Binpack, sort, z-order&lt;/td&gt;
&lt;td&gt;Auto, binpack, sort, z-order&lt;/td&gt;
&lt;td&gt;Binpack, sort, z-order, query-aware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File formats&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Parquet only&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Parquet, Avro, ORC&lt;/td&gt;
&lt;td&gt;Parquet, Avro, ORC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger model&lt;/td&gt;
&lt;td&gt;Fixed: &amp;gt;100 files per partition&lt;/td&gt;
&lt;td&gt;Managed, continuous&lt;/td&gt;
&lt;td&gt;Health-based, prioritized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sort order selection&lt;/td&gt;
&lt;td&gt;You define it&lt;/td&gt;
&lt;td&gt;You define it&lt;/td&gt;
&lt;td&gt;Cross-engine telemetry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance sequencing&lt;/td&gt;
&lt;td&gt;Three independent schedules&lt;/td&gt;
&lt;td&gt;Managed, opaque&lt;/td&gt;
&lt;td&gt;Dependency-ordered pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest rewrite&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Not supported&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not exposed&lt;/td&gt;
&lt;td&gt;First-class operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Statistics refresh&lt;/td&gt;
&lt;td&gt;Separate weekly feature, 20% sample&lt;/td&gt;
&lt;td&gt;Not exposed&lt;/td&gt;
&lt;td&gt;Sequenced after compaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catalog scope&lt;/td&gt;
&lt;td&gt;Glue only&lt;/td&gt;
&lt;td&gt;S3 Tables only&lt;/td&gt;
&lt;td&gt;Glue, REST, S3 Tables, HMS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent writes&lt;/td&gt;
&lt;td&gt;Suspends after 4 failures&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;td&gt;Hot-partition exclusion, backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-demand trigger&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-account / Region&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Per-bucket&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target file size control&lt;/td&gt;
&lt;td&gt;Table property only&lt;/td&gt;
&lt;td&gt;API parameter&lt;/td&gt;
&lt;td&gt;Policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;CloudWatch job metrics&lt;/td&gt;
&lt;td&gt;CloudWatch&lt;/td&gt;
&lt;td&gt;Per-table health, insights, audit trail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution engine&lt;/td&gt;
&lt;td&gt;Managed Spark (JVM)&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;td&gt;Rust + DataFusion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layout validation&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Branch-based simulation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;15 regions, no us-west-1 or GovCloud&lt;/td&gt;
&lt;td&gt;Region-dependent&lt;/td&gt;
&lt;td&gt;Catalog-reachable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Making the Call
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stay with the Glue optimizers when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Every table is in the Glue Data Catalog and every data file is Parquet&lt;/li&gt;
&lt;li&gt;Athena or Redshift is effectively your only query engine, so there is no cross-engine sort conflict to resolve&lt;/li&gt;
&lt;li&gt;Write patterns are batch, and compaction is keeping up without suspending&lt;/li&gt;
&lt;li&gt;You are under roughly 50 tables, where per-table configuration and manual metadata queries are still tractable&lt;/li&gt;
&lt;li&gt;Catalog-level defaults cover your policy needs, and metadata encryption is off so inheritance actually works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a real and common environment. Glue's optimizers are zero-infrastructure and cost nothing to leave running. Enable them, set catalog-level defaults, put a CloudWatch alarm on optimizer suspension, exclude your Iceberg prefixes from S3 lifecycle rules, and move on to a more interesting problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supplement rather than replace when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Glue compaction is working fine but planning time has grown and nothing rewrites manifests&lt;/li&gt;
&lt;li&gt;Your statistics are stale relative to compaction&lt;/li&gt;
&lt;li&gt;Most of your tables fit Glue's model and a handful do not&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Connect over the Glue Iceberg REST catalog, leave the managed optimizers running, and add the operations Glue does not offer. This is the lowest-risk path and it is underused because most people frame the decision as a migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replace when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streaming ingestion.&lt;/strong&gt; Compaction that suspends itself is worse than no compaction, because it fails silently. If you are seeing repeated &lt;code&gt;partial-progress.enabled is true but no rewrite commit succeeded&lt;/code&gt;, the remediation the error suggests is not reachable through the managed optimizer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-engine reads.&lt;/strong&gt; When Trino, Athena, Spark, and DuckDB hit the same tables, a static sort order set once at table creation is a guess that helps one workload and costs the others. The &lt;a href="https://lakeops.dev/blog/reduce-athena-costs-iceberg" rel="noopener noreferrer"&gt;Athena cost analysis&lt;/a&gt; shows how directly bytes scanned tracks layout on scan-priced engines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple catalogs.&lt;/strong&gt; The catalog boundary is not a meaningful maintenance boundary, and maintaining a separate pipeline per catalog is how drift starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hundreds of tables.&lt;/strong&gt; Per-table configuration and per-table metadata queries do not scale, and CloudWatch cannot tell you which table to look at first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost at scale.&lt;/strong&gt; Run the &lt;code&gt;dpu_hours&lt;/code&gt; calculation. If your measured &lt;code&gt;$/TB&lt;/code&gt; is materially above what a purpose-built engine delivers, and you are compacting petabytes, that gap is a line item rather than a rounding error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avro or ORC.&lt;/strong&gt; Glue compaction does not apply. This one is binary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;The Glue optimizers are not a bad product. They are a narrowly scoped one: three independent operations, on one catalog, for one file format, triggered by one heuristic, with job-level telemetry. Inside that scope they work well and cost little.&lt;/p&gt;

&lt;p&gt;What they are not is an operating model. Iceberg maintenance is a loop, not a set of jobs, and the parts Glue leaves out are the parts that make the loop work: sensing table health rather than job success, choosing a physical layout from how tables are actually queried, sequencing operations so each one's output feeds the next, and handling concurrent writers as normal rather than as failure. Those gaps are structural, not a roadmap item you can wait out.&lt;/p&gt;

&lt;p&gt;The most useful next step is not choosing a tool. It is measuring what you have. Pull &lt;code&gt;dpu_hours&lt;/code&gt; and &lt;code&gt;number_of_bytes_compacted&lt;/code&gt; from &lt;code&gt;ListTableOptimizerRuns&lt;/code&gt; for your top twenty tables, query the &lt;code&gt;manifests&lt;/code&gt; and &lt;code&gt;files&lt;/code&gt; metadata tables to see whether planning overhead and file distribution are actually where you assumed, and check whether any optimizer has quietly suspended itself. Most teams find at least one surprise, and the surprise usually decides the question.&lt;/p&gt;

&lt;p&gt;If the answer is that you need the loop and not just the jobs, a control plane like &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; runs it on top of the Glue catalog you already have, without moving data or changing the table format. If the answer is that Glue covers you, that is a good outcome too, and now you know which limitations to watch for.&lt;/p&gt;

</description>
      <category>iceberg</category>
      <category>aws</category>
      <category>dataengineering</category>
      <category>lakehouse</category>
    </item>
    <item>
      <title>Apache Iceberg Query Optimization: Production Guide</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Thu, 24 Sep 2026 12:18:02 +0000</pubDate>
      <link>https://dev.to/jonisar/apache-iceberg-query-optimization-production-guide-2l0k</link>
      <guid>https://dev.to/jonisar/apache-iceberg-query-optimization-production-guide-2l0k</guid>
      <description>&lt;p&gt;Every query against an Iceberg table is a negotiation about how much data the engine is allowed to ignore. Read the metadata, throw away everything that cannot match, scan what survives. A well-maintained table lets the planner discard 95–99% of files before a single Parquet footer is opened. A neglected table forces a near-full scan no matter how selective your &lt;code&gt;WHERE&lt;/code&gt; clause looks.&lt;/p&gt;

&lt;p&gt;That gap is almost never the engine's fault. Trino, Spark, Snowflake, and DuckDB all run the same fundamental pruning logic against the same Iceberg metadata. What differs is whether your physical layout and statistics give that logic anything to work with. A &lt;code&gt;customer_id = 481&lt;/code&gt; filter is worthless if every file in the table contains customer IDs 1 through 10,000,000, and no amount of cluster tuning will fix it.&lt;/p&gt;

&lt;p&gt;This guide covers the entire surface: how Iceberg's scan planner actually works and what to measure inside it, how statistics get collected (and silently dropped), the difference between partitioning and clustering, why declaring a sort order often changes nothing, how bloom filters and Puffin statistics fit, what format v3 deletion vectors change for reads, and the engine-specific knobs that matter on Trino and Spark. It ends with a triage runbook and a checklist.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;p&gt;If you only read one section, read this table. It maps the symptom you are staring at to the mechanism that causes it.&lt;/p&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;Most 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;Selective &lt;code&gt;WHERE&lt;/code&gt; still scans most of the table&lt;/td&gt;
&lt;td&gt;File min/max bounds overlap; data is not clustered on the filter column&lt;/td&gt;
&lt;td&gt;Global sort or Z-order during compaction (not just binpack)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seconds of latency before the first byte is read&lt;/td&gt;
&lt;td&gt;Manifest fragmentation, or manifests clustered on the wrong partition field&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rewrite_manifests&lt;/code&gt; with &lt;code&gt;sort_by&lt;/code&gt; on the hot transform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pruning does nothing and bounds are simply absent&lt;/td&gt;
&lt;td&gt;Metrics mode is &lt;code&gt;none&lt;/code&gt;, or the column fell past the 100-column inference cap&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;write.metadata.metrics.column.&amp;lt;col&amp;gt;&lt;/code&gt; = &lt;code&gt;truncate(16)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Point lookup on a high-cardinality column is slow&lt;/td&gt;
&lt;td&gt;Min/max cannot prune a scattered key&lt;/td&gt;
&lt;td&gt;Parquet bloom filter on that column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table got slower after a CDC backfill&lt;/td&gt;
&lt;td&gt;Position or equality delete accumulation&lt;/td&gt;
&lt;td&gt;Compact with a low &lt;code&gt;delete-file-threshold&lt;/code&gt;; move to v3 deletion vectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Joins pick catastrophic orders&lt;/td&gt;
&lt;td&gt;Missing or stale Puffin NDV, or CBO is switched off&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;compute_table_stats&lt;/code&gt; / &lt;code&gt;ANALYZE&lt;/code&gt;, refreshed after every compaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sort order is declared but nothing improved&lt;/td&gt;
&lt;td&gt;Writers applied a &lt;em&gt;local&lt;/em&gt; sort, so file ranges still overlap&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;write.distribution-mode&lt;/code&gt; to &lt;code&gt;range&lt;/code&gt;, or sort globally during rewrite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the one that costs teams the most time, and it gets its own section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Iceberg Actually Plans a Query
&lt;/h2&gt;

&lt;p&gt;Iceberg planning is a funnel, and the &lt;a href="https://iceberg.apache.org/spec/#scan-planning" rel="noopener noreferrer"&gt;table spec&lt;/a&gt; defines each stage precisely. Each stage is cheaper than the one beneath it, and each stage's usefulness depends entirely on the quality of the metadata written at commit time. Knowing the stages by name matters, because the diagnostics and the fixes are different at each level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: Manifest list filtering
&lt;/h3&gt;

&lt;p&gt;Planning starts at the manifest list, a single Avro file that indexes every manifest in the snapshot. Each entry carries a &lt;code&gt;partitions&lt;/code&gt; array of &lt;code&gt;field_summary&lt;/code&gt; structs: &lt;code&gt;lower_bound&lt;/code&gt;, &lt;code&gt;upper_bound&lt;/code&gt;, &lt;code&gt;contains_null&lt;/code&gt;, and &lt;code&gt;contains_nan&lt;/code&gt; for each partition field across all data files that manifest tracks.&lt;/p&gt;

&lt;p&gt;Iceberg converts your query predicate into partition space using &lt;strong&gt;inclusive projection&lt;/strong&gt;, a deliberate widening that guarantees no file which might match is ever excluded. For a table partitioned by &lt;code&gt;days(event_time)&lt;/code&gt;, the filter &lt;code&gt;event_time &amp;gt;= TIMESTAMP '2026-03-01'&lt;/code&gt; projects to a bound on &lt;code&gt;event_time_day&lt;/code&gt;. &lt;code&gt;ManifestEvaluator&lt;/code&gt; then tests that projected predicate against each manifest's summary. Manifests covering only January and February are dropped whole, along with every file they describe.&lt;/p&gt;

&lt;p&gt;This is the cheapest prune available, and it is the one hidden partitioning buys you. Users filter on &lt;code&gt;event_time&lt;/code&gt;; the planner does the transform arithmetic. Change the partition spec later and the queries keep working, because the projection is computed per spec at plan time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: Data file filtering
&lt;/h3&gt;

&lt;p&gt;For each surviving manifest, the planner reads individual file entries. Every entry carries per-column metrics: &lt;code&gt;lower_bounds&lt;/code&gt;, &lt;code&gt;upper_bounds&lt;/code&gt;, &lt;code&gt;null_value_counts&lt;/code&gt;, &lt;code&gt;nan_value_counts&lt;/code&gt;, &lt;code&gt;value_counts&lt;/code&gt;, and &lt;code&gt;column_sizes&lt;/code&gt;, all keyed by field ID.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;InclusiveMetricsEvaluator&lt;/code&gt; tests the predicate against those metrics and answers one question: &lt;em&gt;could this file contain a matching row?&lt;/em&gt; It returns &lt;code&gt;false&lt;/code&gt; only when the metrics prove no match. One detail dominates everything else in this guide:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a column has no bounds recorded, &lt;code&gt;InclusiveMetricsEvaluator&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt;. Missing statistics do not fail loudly. They silently disable file pruning for that column.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Its counterpart, &lt;code&gt;StrictMetricsEvaluator&lt;/code&gt;, answers the opposite question: do &lt;em&gt;all&lt;/em&gt; rows in this file match? That powers metadata-only deletes and residual computation rather than read pruning.&lt;/p&gt;

&lt;p&gt;A note on &lt;code&gt;value_counts&lt;/code&gt;, because it is widely misdescribed: it is the total row count for that column &lt;strong&gt;including&lt;/strong&gt; nulls and NaNs, not the count of non-null values. Null-aware pruning comes from comparing &lt;code&gt;null_value_counts&lt;/code&gt; against &lt;code&gt;record_count&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: Residual evaluation
&lt;/h3&gt;

&lt;p&gt;This stage is invisible in most articles and is the reason partition-aligned filters are effectively free.&lt;/p&gt;

&lt;p&gt;Once a file survives, Iceberg computes a &lt;strong&gt;residual&lt;/strong&gt;: the part of the predicate that still needs row-level evaluation after the partition value is taken into account. Take a table partitioned by &lt;code&gt;days(ts)&lt;/code&gt; and a query with &lt;code&gt;ts &amp;gt; TIMESTAMP '2026-03-01 08:00:00'&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For a file in partition &lt;code&gt;ts_day = 2026-03-05&lt;/code&gt;, strict projection proves every row already satisfies the predicate. The residual collapses to &lt;code&gt;alwaysTrue()&lt;/code&gt; and the engine skips per-row filter evaluation entirely.&lt;/li&gt;
&lt;li&gt;For the boundary partition &lt;code&gt;ts_day = 2026-03-01&lt;/code&gt;, the residual stays &lt;code&gt;ts &amp;gt; '2026-03-01 08:00:00'&lt;/code&gt; and must be checked per row.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Trino's &lt;code&gt;EXPLAIN&lt;/code&gt; output you can see this split directly: predicates absorbed into the scan's &lt;code&gt;constraint&lt;/code&gt; are enforced by Iceberg and never re-evaluated, while a residual &lt;code&gt;Filter&lt;/code&gt; node above the scan is per-row work. If you see your main predicate in the &lt;code&gt;Filter&lt;/code&gt; node rather than the constraint, the filter is not partition-aligned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 4: Inside the Parquet file
&lt;/h3&gt;

&lt;p&gt;Once a file is opened, Iceberg's reader applies three row-group filters in a deliberate order, cheapest first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ParquetMetricsRowGroupFilter&lt;/code&gt;&lt;/strong&gt; — per-row-group min/max and null counts from the footer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ParquetDictionaryRowGroupFilter&lt;/code&gt;&lt;/strong&gt; — if the column chunk is dictionary-encoded, the dictionary is an exact membership test. No value in the dictionary means no matching row, with no false positives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ParquetBloomRowGroupFilter&lt;/code&gt;&lt;/strong&gt; — a probabilistic test, only consulted if the first two could not rule the group out, because it costs an extra read.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Worth knowing precisely: &lt;strong&gt;Iceberg's own reader prunes at row-group granularity only.&lt;/strong&gt; It does not use Parquet's column index / offset index to skip individual pages. Trino uses its own Parquet reader and does apply page-level filtering, which is one real, structural reason the same table can prune differently on Trino than on Spark.&lt;/p&gt;

&lt;h3&gt;
  
  
  Column projection
&lt;/h3&gt;

&lt;p&gt;Iceberg pushes the projected column set into the reader, fetching only the columns in your &lt;code&gt;SELECT&lt;/code&gt; plus those referenced by predicates. Projection matches on stable field IDs, not names, so renames and schema evolution never break it. On a 300-column table, projection alone routinely removes 90%+ of I/O, which is why &lt;code&gt;SELECT *&lt;/code&gt; on wide tables is an expensive habit rather than a stylistic one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where planning runs, and why it can be the bottleneck
&lt;/h3&gt;

&lt;p&gt;Planning cost scales with the number of manifests opened, not the amount of data returned. Three execution models exist today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trino&lt;/strong&gt; plans on the coordinator, multithreaded. &lt;code&gt;iceberg.planning-threads&lt;/code&gt; (default 2× coordinator cores) controls manifest reads and &lt;code&gt;iceberg.split-manager-threads&lt;/code&gt; controls split generation. There is no distributed planning, so a badly fragmented table turns the coordinator into a bottleneck for the whole cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spark&lt;/strong&gt; supports distributed planning via &lt;code&gt;spark.sql.iceberg.data-planning-mode&lt;/code&gt; and &lt;code&gt;spark.sql.iceberg.delete-planning-mode&lt;/code&gt;, each accepting &lt;code&gt;AUTO&lt;/code&gt;, &lt;code&gt;LOCAL&lt;/code&gt;, or &lt;code&gt;DISTRIBUTED&lt;/code&gt; and defaulting to &lt;code&gt;AUTO&lt;/code&gt;. In &lt;code&gt;DISTRIBUTED&lt;/code&gt; mode, manifest scanning becomes a Spark job over the &lt;code&gt;entries&lt;/code&gt; metadata table. &lt;code&gt;AUTO&lt;/code&gt; decides based on estimated manifest volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side scan planning&lt;/strong&gt; shipped in Iceberg 1.11.0 and is the development most guides still describe as "proposed." The REST catalog spec defines &lt;code&gt;planTableScan&lt;/code&gt;, &lt;code&gt;fetchPlanningResult&lt;/code&gt;, &lt;code&gt;cancelPlanning&lt;/code&gt;, and &lt;code&gt;fetchScanTasks&lt;/code&gt; endpoints, with a client-side &lt;code&gt;scan-planning-mode&lt;/code&gt; property (&lt;code&gt;client&lt;/code&gt; or &lt;code&gt;server&lt;/code&gt;) and per-table override via &lt;code&gt;LoadTableResult&lt;/code&gt;. Spark can now delegate planning to the catalog. Before you plan around it, check what your catalog actually advertises in &lt;code&gt;GET /v1/config&lt;/code&gt; under &lt;code&gt;endpoints&lt;/code&gt;; vendor support is uneven and claims run ahead of implementations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Measure planning before you tune it
&lt;/h3&gt;

&lt;p&gt;Do not guess at which stage is failing. Iceberg emits a &lt;code&gt;ScanReport&lt;/code&gt; through its &lt;code&gt;MetricsReporter&lt;/code&gt; interface with exactly the fields you need:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;totalPlanningDuration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Planning latency, isolated from execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;scannedDataManifests&lt;/code&gt; / &lt;code&gt;skippedDataManifests&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Whether Stage 1 is working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;resultDataFiles&lt;/code&gt; / &lt;code&gt;skippedDataFiles&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Whether Stage 2 is working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;resultDeleteFiles&lt;/code&gt; / &lt;code&gt;skippedDeleteFiles&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Your merge-on-read burden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;totalFileSizeInBytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bytes the scan committed to reading&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ratio &lt;code&gt;skippedDataManifests / scannedDataManifests&lt;/code&gt; is the single best signal for manifest clustering quality, and &lt;code&gt;skippedDataFiles / (skippedDataFiles + resultDataFiles)&lt;/code&gt; is the best signal for sort-order quality. In Trino, &lt;code&gt;system.runtime.queries&lt;/code&gt; separates &lt;code&gt;analysis_time&lt;/code&gt;, &lt;code&gt;planning_time&lt;/code&gt;, and &lt;code&gt;execution_time&lt;/code&gt;, and &lt;code&gt;EXPLAIN ANALYZE VERBOSE&lt;/code&gt; gives you &lt;code&gt;physicalInputBytes&lt;/code&gt; per operator. In Spark, the SQL tab's &lt;code&gt;BatchScan&lt;/code&gt; node reports files read and files skipped directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Layer Nobody Owns
&lt;/h2&gt;

&lt;p&gt;Here is the structural problem the rest of this guide keeps running into.&lt;/p&gt;

&lt;p&gt;Iceberg specifies how pruning works. Engines implement it. Neither decides what your table's physical layout should be. Nothing in the format observes that 80% of production queries filter on &lt;code&gt;region&lt;/code&gt; and reorders your files accordingly. Nothing notices that last night's compaction invalidated your Puffin statistics. Nothing tracks that a table's manifest count crossed the threshold where planning latency became user-visible. Those are continuous operational decisions, and the open lakehouse ships them as your problem.&lt;/p&gt;

&lt;p&gt;At ten tables you solve it with a Spark job and a cron entry. At a few hundred, across several engines and catalogs, you are running a control loop by hand with no telemetry, and the decisions go stale faster than you can revisit them.&lt;/p&gt;

&lt;p&gt;A lakehouse control plane such as &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; is the layer that runs that loop. It sits above the stack you already have rather than replacing any of it: your catalogs (AWS Glue, DynamoDB-backed, REST catalogs like Polaris, Nessie, Gravitino, and Lakekeeper, or S3 Tables), your Iceberg tables in object storage, and your query engines. It attaches through standard catalog and Iceberg metadata APIs, so there is no data copy, no pipeline rewrite, and no change to the table format.&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%2Fsmjbmy05fkd2a07w7zjv.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%2Fsmjbmy05fkd2a07w7zjv.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The loop it runs is the one the open stack leaves unowned: &lt;strong&gt;sense&lt;/strong&gt; table structure and query telemetry, &lt;strong&gt;classify&lt;/strong&gt; each table's health, &lt;strong&gt;plan&lt;/strong&gt; the maintenance and layout each table needs, &lt;strong&gt;execute&lt;/strong&gt; in dependency order, then &lt;strong&gt;measure&lt;/strong&gt; the result and feed it back into the next decision. &lt;/p&gt;

&lt;p&gt;Learn more:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/solutions/managed-lakehouse" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Fsolutions%2Fmanaged-lakehouse%2Fopengraph-image%3Fed2dd9c18c9a67a6" height="630" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/solutions/managed-lakehouse" rel="noopener noreferrer" class="c-link"&gt;
            Managed Iceberg Lakehouse | LakeOps
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Connect your catalog — every Iceberg table maintained, compacted, and governed across your lakehouse. Trino, Spark, Snowflake, Athena.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/solutions/table-maintenance" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Fsolutions%2Ftable-maintenance%2Fopengraph-image%3Fcc25e3976702c419" height="630" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/solutions/table-maintenance" rel="noopener noreferrer" class="c-link"&gt;
            Iceberg Lakehouse Table Maintenance | LakeOps
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Autonomous Iceberg table maintenance for your data lakehouse. Snapshot expiry, orphan cleanup, manifest rewrite, and delete-file resolution — no cron.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Later in this guide, once the underlying mechanics are on the table, I will get specific about how that applies to sort-order selection, manifest health, and statistics lifecycle.&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%2F7fx96aom198m9gv3cu1w.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%2F7fx96aom198m9gv3cu1w.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep reading first. Every fix below is worth understanding whether you automate it or not.&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%2F3jxdumu29fgmb2z81qos.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%2F3jxdumu29fgmb2z81qos.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Statistics: The Fuel for Everything
&lt;/h2&gt;

&lt;p&gt;Stage 2 pruning runs entirely on column bounds. If you optimize one thing before touching layout, make it this, because the failure mode is invisible and the fix is a table property.&lt;/p&gt;

&lt;h3&gt;
  
  
  What gets collected, and what does not
&lt;/h3&gt;

&lt;p&gt;Metrics collection is governed by a small set of &lt;a href="https://iceberg.apache.org/docs/latest/configuration/" rel="noopener noreferrer"&gt;table properties&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.metadata.metrics.default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;truncate(16)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mode for all columns without an explicit override&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.metadata.metrics.column.&amp;lt;col&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unset&lt;/td&gt;
&lt;td&gt;Per-column override; wins over everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.metadata.metrics.max-inferred-column-defaults&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cap on how many columns get inferred metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Modes are &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;counts&lt;/code&gt;, &lt;code&gt;truncate(L)&lt;/code&gt;, and &lt;code&gt;full&lt;/code&gt;. And then there is the behavior that catches almost everyone:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 100-column cap applies only to the &lt;em&gt;inferred&lt;/em&gt; default.&lt;/strong&gt; If you explicitly set &lt;code&gt;write.metadata.metrics.default&lt;/code&gt;, the cap is bypassed completely. Setting it to &lt;code&gt;'truncate(16)'&lt;/code&gt; on a 2,000-column table looks like a no-op because it is "the default value," but it switches metrics on for all 2,000 columns and can inflate your manifests by an order of magnitude. This is a common self-inflicted wound: someone "tunes" statistics and makes planning dramatically slower.&lt;/p&gt;

&lt;p&gt;Two more details from the implementation that are worth internalizing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The column count uses projected field IDs, which &lt;strong&gt;includes struct fields themselves&lt;/strong&gt;, not just leaves. A table with 60 leaf columns nested inside 45 structs is already past 100. Selection is pre-order, so it is the columns declared late in a wide schema that silently lose their bounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort-order columns are automatically promoted to &lt;code&gt;truncate(16)&lt;/code&gt;&lt;/strong&gt; when the effective default is &lt;code&gt;none&lt;/code&gt; or &lt;code&gt;counts&lt;/code&gt;. So &lt;code&gt;write.metadata.metrics.default = 'none'&lt;/code&gt; still keeps bounds on your declared sort keys. Note the asymmetry: Z-order and Hilbert clustering do &lt;em&gt;not&lt;/em&gt; get this promotion, because they do not change the table's declared &lt;code&gt;SortOrder&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The configuration that is actually correct
&lt;/h3&gt;

&lt;p&gt;For wide tables, be explicit in both directions. Turn the default off, then turn metrics on for the columns that matter:&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;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.metrics.default'&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.event_time'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'truncate(16)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'write.metadata.metrics.column.customer_id'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'full'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'write.metadata.metrics.column.region'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'full'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'write.metadata.metrics.column.order_id'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'truncate(16)'&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 the one case where setting &lt;code&gt;default&lt;/code&gt; explicitly is right, precisely because you want to bypass the inference machinery and take control. Use &lt;code&gt;full&lt;/code&gt; for short, high-value keys and &lt;code&gt;truncate(16)&lt;/code&gt; for strings. Never use &lt;code&gt;full&lt;/code&gt; on a column holding URLs, JSON blobs, or stack traces: an untruncated multi-kilobyte bound gets copied into both the lower and upper bound map for &lt;em&gt;every&lt;/em&gt; data file, and the manifests become larger than some of the data.&lt;/p&gt;
&lt;h3&gt;
  
  
  Verifying which columns actually have bounds
&lt;/h3&gt;

&lt;p&gt;Do not assume. Ask the table. Spark's &lt;code&gt;files&lt;/code&gt; metadata table exposes a &lt;code&gt;readable_metrics&lt;/code&gt; struct that decodes the binary bounds into their real types:&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;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower_bound&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cid_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper_bound&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cid_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;null_value_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cid_nulls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;record_count&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="k"&gt;FROM&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;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;2&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 &lt;code&gt;cid_min&lt;/code&gt; and &lt;code&gt;cid_max&lt;/code&gt; come back &lt;code&gt;NULL&lt;/code&gt;, that column has no metrics and no query filtering on it will ever prune. If they are populated but span nearly the full domain in every row, metrics exist but the data is not clustered. Those are two different problems with two different fixes, and this query distinguishes them in one shot.&lt;/p&gt;

&lt;p&gt;To see the raw picture of which field IDs have bounds at all:&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;map_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lower_bounds&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;fields_with_bounds&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;LIMIT&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;h3&gt;
  
  
  Measuring clustering quality
&lt;/h3&gt;

&lt;p&gt;Overlap is what kills Stage 2. A useful single number is how many distinct lower bounds exist relative to file 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="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;files&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;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower_bound&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;distinct_lower_bounds&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;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper_bound&lt;/span&gt;
        &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower_bound&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_range_width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper_bound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&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;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower_bound&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;global_range&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;When &lt;code&gt;avg_range_width&lt;/code&gt; approaches &lt;code&gt;global_range&lt;/code&gt;, every file covers the whole domain and pruning is mathematically impossible. When &lt;code&gt;distinct_lower_bounds&lt;/code&gt; is a small fraction of &lt;code&gt;files&lt;/code&gt;, many files start at the same value and their ranges overlap heavily. Either result points at the same fix: cluster the data.&lt;/p&gt;
&lt;h2&gt;
  
  
  Partitioning: The Coarse Prune
&lt;/h2&gt;

&lt;p&gt;Partitioning decides which &lt;em&gt;groups&lt;/em&gt; of files can be ignored wholesale. It is the cheapest prune and the one with the most permanent consequences, because the partition spec shapes how every subsequent write lands.&lt;/p&gt;
&lt;h3&gt;
  
  
  Choosing transforms
&lt;/h3&gt;

&lt;p&gt;Partition on the columns that appear in most query filters. Time nearly always wins, because nearly every analytical query bounds a time range:&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&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;region&lt;/span&gt;      &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_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;amount&lt;/span&gt;      &lt;span class="nb"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;payload&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="n"&gt;days&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;The available transforms are &lt;code&gt;year&lt;/code&gt;, &lt;code&gt;month&lt;/code&gt;, &lt;code&gt;day&lt;/code&gt;, &lt;code&gt;hour&lt;/code&gt;, &lt;code&gt;bucket(N, col)&lt;/code&gt;, &lt;code&gt;truncate(W, col)&lt;/code&gt;, and &lt;code&gt;identity&lt;/code&gt;. Format v3 adds multi-argument transforms for both partitioning and sorting.&lt;/p&gt;

&lt;p&gt;Practical guidance that holds up in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick granularity from write volume, not from query habit.&lt;/strong&gt; The target is partitions large enough to hold files near your target file size. A table ingesting 2 GB/day should be partitioned by &lt;code&gt;day&lt;/code&gt;, not &lt;code&gt;hour&lt;/code&gt;; hourly partitioning gives you 24 partitions of ~85 MB each and you have manufactured a small-file problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bucket(N, col)&lt;/code&gt; is for high-cardinality equality predicates&lt;/strong&gt;, typically a join or lookup key: &lt;code&gt;PARTITIONED BY (bucket(64, customer_id))&lt;/code&gt;. It also enables storage-partitioned joins on Spark and bucket-aware execution on Trino.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;truncate(W, col)&lt;/code&gt; is underused&lt;/strong&gt; for string keys with meaningful prefixes, such as &lt;code&gt;truncate(4, country_code)&lt;/code&gt; or a hashed ID prefix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;identity&lt;/code&gt; on anything high-cardinality is a trap.&lt;/strong&gt; &lt;code&gt;PARTITIONED BY (customer_id)&lt;/code&gt; on a million customers produces a million partition directories, a million-plus manifest entries, and planning that is slower than the scan you were trying to avoid.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  The over-partitioning failure mode
&lt;/h3&gt;

&lt;p&gt;Over-partitioning is the most common self-inflicted Iceberg performance problem, and it is genuinely counterintuitive because each individual partition looks fine. The costs compound in three places at once: more files means more S3 GET requests and more open-file overhead per query; more partition values means more manifest entries to evaluate during planning; and small files means compression ratios and dictionary encoding both degrade, so the bytes on disk grow too.&lt;/p&gt;

&lt;p&gt;Check for it directly:&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;files&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;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_count&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;rows&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;files&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 &lt;code&gt;avg_mb&lt;/code&gt; sits well under 100 across most partitions, either the partition granularity is too fine or compaction is not keeping up. Both are fixable, but they are different fixes and the query above tells you which by showing whether the problem is uniform (granularity) or concentrated in recent partitions (compaction lag).&lt;/p&gt;
&lt;h3&gt;
  
  
  Partition evolution
&lt;/h3&gt;

&lt;p&gt;Iceberg changes the partition spec without rewriting data. Old files keep their old spec; new writes use the new one. The planner evaluates each spec independently at plan time, so both layouts prune correctly and queries need no changes.&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;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;REPLACE&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;FIELD&lt;/span&gt; &lt;span class="n"&gt;days&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="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;hours&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="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;ADD&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;FIELD&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;Two operational caveats. First, a table carrying several specs makes planning modestly more complex and makes &lt;code&gt;partitions&lt;/code&gt; metadata harder to reason about, so evolution is not free forever. Second, if you want old data physically moved into the new spec, that is a &lt;code&gt;rewrite_data_files&lt;/code&gt; pass with &lt;code&gt;output-spec-id&lt;/code&gt;, and it costs a full rewrite of the affected range. Plan it deliberately rather than discovering it on next month's compute bill.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sort Order and Clustering: Where Queries Are Actually Won
&lt;/h2&gt;

&lt;p&gt;Partitioning determines which file groups exist. &lt;strong&gt;Clustering determines whether the files inside those groups are skippable.&lt;/strong&gt; They are separate layers and collapsing them is the most common conceptual error in Iceberg performance work.&lt;/p&gt;

&lt;p&gt;When data is clustered on a predicate column, each file holds a narrow contiguous slice of the value domain, the manifest bounds are tight, and &lt;code&gt;InclusiveMetricsEvaluator&lt;/code&gt; eliminates most files. When it is not, every file holds a random sample of the full domain, bounds overlap completely, and Stage 2 becomes a no-op even though the statistics are present and correct.&lt;/p&gt;

&lt;p&gt;This is also why &lt;strong&gt;binpack compaction is not clustering&lt;/strong&gt;. Binpack fixes file count. Merging 900 unsorted 5 MB files into 40 unsorted 512 MB files makes your &lt;code&gt;LIST&lt;/code&gt; and &lt;code&gt;GET&lt;/code&gt; costs better and your planning faster, and changes file-level skipping by approximately nothing. Large unsorted files still fail file pruning. If you have been running scheduled binpack compaction and wondering why query latency never improved, this is why.&lt;/p&gt;
&lt;h3&gt;
  
  
  Local sort vs global sort: the distinction that decides everything
&lt;/h3&gt;

&lt;p&gt;You can declare a sort order and get zero pruning improvement. This happens constantly and the reason is that &lt;em&gt;where&lt;/em&gt; the sort happens matters more than &lt;em&gt;that&lt;/em&gt; it happened.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;local sort&lt;/strong&gt; orders rows within each writer task. You get N files, each internally sorted, whose &lt;code&gt;[lower_bound, upper_bound]&lt;/code&gt; ranges &lt;strong&gt;overlap heavily&lt;/strong&gt;, because each task saw a random sample of the data. &lt;code&gt;InclusiveMetricsEvaluator&lt;/code&gt; cannot skip any of them for a point lookup. Local sort buys better compression and better in-file row-group pruning. It does not buy file-level skipping.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;global sort&lt;/strong&gt; shuffles first so that each file receives a disjoint value range. Bounds do not overlap, and file-level skipping works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The control for this is &lt;code&gt;write.distribution-mode&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Shuffle&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Whatever the upstream partitioning gave you. Risks a small-file explosion unless you pre-sort or enable fanout writes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hash exchange on the partition key&lt;/td&gt;
&lt;td&gt;Each partition value lands in one task, so file counts stay sane. &lt;strong&gt;Does not order rows within the file.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;range&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sample, then range-partition&lt;/td&gt;
&lt;td&gt;Global ordering across files. Disjoint bounds. Real file skipping. More expensive to write.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Spark's effective default has been &lt;code&gt;hash&lt;/code&gt; since Iceberg 1.2.0, and &lt;code&gt;range&lt;/code&gt; when the table has a declared sort order. Per-operation overrides exist as &lt;code&gt;write.delete.distribution-mode&lt;/code&gt;, &lt;code&gt;write.update.distribution-mode&lt;/code&gt;, and &lt;code&gt;write.merge.distribution-mode&lt;/code&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;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;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;customer_id&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="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.distribution-mode'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'range'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One more piece of honesty from the spec: writers &lt;strong&gt;should&lt;/strong&gt; apply the declared sort order but are explicitly &lt;strong&gt;not required&lt;/strong&gt; to, with streaming writes called out as the case where it is legitimately too expensive. A declared sort order is a hint, not a guarantee. Verify it against reality rather than trusting the table property:&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;sort_order_id&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;files&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;1073741824&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;gb&lt;/span&gt;
&lt;span class="k"&gt;FROM&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="n"&gt;sort_order_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;sort_order_id = 0&lt;/code&gt; means unsorted. A table that declares a sort order and shows mostly zeros is telling you its writers are ignoring it, and the fix is compaction with an explicit sort strategy rather than a table property change.&lt;/p&gt;
&lt;h3&gt;
  
  
  Choosing sort keys
&lt;/h3&gt;

&lt;p&gt;Order matters, and it matters more than most teams expect. A linear sort gives the leading column tight, nearly disjoint bounds. The second column is only clustered &lt;em&gt;within&lt;/em&gt; runs of equal values in the first, so its bounds are meaningfully wider. By the third or fourth column, the clustering benefit is close to noise.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query pattern&lt;/th&gt;
&lt;th&gt;Recommended layout&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One dominant filter column&lt;/td&gt;
&lt;td&gt;Linear sort, that column leading&lt;/td&gt;
&lt;td&gt;Tightest possible bounds on the column that matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entity plus time range (&lt;code&gt;customer_id&lt;/code&gt; + &lt;code&gt;event_time&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ORDERED BY customer_id, event_time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Entity clustering first, time ordering inside each entity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Joins on a stable key&lt;/td&gt;
&lt;td&gt;Sort by the join key, or &lt;code&gt;bucket()&lt;/code&gt; partition it&lt;/td&gt;
&lt;td&gt;Enables merge joins and storage-partitioned joins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2–4 filter columns used in varying combinations&lt;/td&gt;
&lt;td&gt;Z-order&lt;/td&gt;
&lt;td&gt;Balanced pruning across dimensions instead of one good dimension and three bad ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More than 4 filter columns&lt;/td&gt;
&lt;td&gt;Pick the top 2–3&lt;/td&gt;
&lt;td&gt;Curves dilute; the rest is wishful thinking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-cardinality point lookups on a non-sort column&lt;/td&gt;
&lt;td&gt;Bloom filter, not sort&lt;/td&gt;
&lt;td&gt;You only get one leading column; spend it wisely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Z-order, and the gotcha that silently breaks it
&lt;/h3&gt;

&lt;p&gt;Z-order interleaves the bits of several columns into a single Morton code, producing a space-filling curve that clusters data across all of them at once. No single dimension gets bounds as tight as a dedicated linear sort would give it, but every dimension gets usable bounds instead of one winner and several losers.&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;'zorder(customer_id, region, product_id)'&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;'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="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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Z-order has two options of its own, and one of them is a landmine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;var-length-contribution&lt;/code&gt;&lt;/strong&gt; (default &lt;code&gt;8&lt;/code&gt;) is the number of bytes taken from each String or Binary column when building the curve. &lt;strong&gt;Only the first 8 bytes participate.&lt;/strong&gt; If your string keys share a long common prefix, and prefixed UUIDs, hierarchical paths, and &lt;code&gt;evt_2026_03_...&lt;/code&gt; style identifiers all do, then every value contributes identical bits and Z-order degenerates into expensive noise. Raise the value, or truncate the column to its distinguishing portion first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max-output-size&lt;/code&gt;&lt;/strong&gt; (default &lt;code&gt;2147483647&lt;/code&gt;) caps total interleaved bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more properties of curve clustering worth knowing. It is applied per rewrite and is &lt;em&gt;not&lt;/em&gt; persisted as the table's &lt;code&gt;SortOrder&lt;/code&gt;, so subsequent ordinary writes will not maintain it. And because the declared sort order is unchanged, Z-ordered columns do not receive the automatic &lt;code&gt;truncate(16)&lt;/code&gt; metrics promotion described earlier. If your metrics default is restrictive, set explicit per-column metrics on every column you Z-order, or you will do the clustering work and record no bounds to exploit it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Hilbert curves: the accurate status
&lt;/h3&gt;

&lt;p&gt;Hilbert curves are a genuine improvement over Z-order for multi-dimensional locality, because consecutive points on a Hilbert curve are always true spatial neighbors, eliminating the long jumps a Morton curve makes across the value space.&lt;/p&gt;

&lt;p&gt;Be precise about availability, because the internet is not. As of Iceberg 1.11.0, &lt;code&gt;SparkHilbertUDF&lt;/code&gt; exists on the project's &lt;code&gt;main&lt;/code&gt; branch under &lt;code&gt;spark/v4.1&lt;/code&gt; only. It is &lt;strong&gt;not present in any released version&lt;/strong&gt;, it is absent from &lt;code&gt;spark/v4.0&lt;/code&gt; and &lt;code&gt;spark/v3.5&lt;/code&gt;, and it is documented only in the nightly docs, not the release docs. Backports to Spark 4.0 and 3.5 are tracked as an open issue. An earlier 2022 attempt was closed without merging, so blog posts citing it as evidence of support are wrong.&lt;/p&gt;

&lt;p&gt;The nightly documentation records one design difference worth noting for when it does ship: unlike Z-order, every column contributes its full 8-byte primitive width to the Hilbert index, so &lt;code&gt;var-length-contribution&lt;/code&gt; and &lt;code&gt;max-output-size&lt;/code&gt; do not apply. Hilbert and Z-order also cannot be combined with each other or with plain column sorts in a single &lt;code&gt;sort_order&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For production work today, Z-order is the multi-dimensional option. Plan for Hilbert; do not build on it yet.&lt;/p&gt;
&lt;h3&gt;
  
  
  One thing no Iceberg property can tell you
&lt;/h3&gt;

&lt;p&gt;Declaring a sort order is a one-line DDL statement. Knowing &lt;em&gt;which columns to declare&lt;/em&gt; requires knowing what your queries actually filter on, across every engine, every dbt model, every BI tool, and every notebook that touches the table. Most teams choose a sort order at table creation from intuition and never revisit it, while the query mix drifts underneath them for two years.&lt;/p&gt;

&lt;p&gt;That is a telemetry problem, not an Iceberg problem, and it is the first place a control plane earns its keep. Because &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; sits above all connected engines rather than inside one, it collects &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, and &lt;code&gt;GROUP BY&lt;/code&gt; column frequency from Trino, Spark, Snowflake, Athena, DuckDB, and Flink together, and scores candidate layouts against the combined mix rather than against whichever engine an individual team happens to use. The mechanics of how that turns into an applied sort order come later in this guide.&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%2Fzn423tksoytdma91jalz.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%2Fzn423tksoytdma91jalz.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  File Size and Row Group Size
&lt;/h2&gt;

&lt;p&gt;File sizing is usually discussed as a storage-cost topic. It is also a pruning-resolution topic, and the interaction between file size and row group size sets a hard ceiling on how much data any query can skip inside a file.&lt;/p&gt;

&lt;p&gt;The relevant defaults:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.target-file-size-bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;536870912 (512 MB)&lt;/td&gt;
&lt;td&gt;Compaction target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.parquet.row-group-size-bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;134217728 (128 MB)&lt;/td&gt;
&lt;td&gt;Granularity of in-file skipping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.parquet.page-size-bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1048576 (1 MB)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.parquet.page-row-limit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;Matters for engines that use the page index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.delete.target-file-size-bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;67108864 (64 MB)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;read.split.target-size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;134217728 (128 MB)&lt;/td&gt;
&lt;td&gt;Scan parallelism unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;read.split.open-file-cost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4194304 (4 MB)&lt;/td&gt;
&lt;td&gt;Planner's cost model for opening a file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do the arithmetic on the first two together. A 512 MB file at the default 128 MB row group size contains exactly &lt;strong&gt;four row groups&lt;/strong&gt;, so the best possible in-file skip is 75%, and a point lookup that matches one row still reads a quarter of the file. Drop to 32 MB row groups and you get sixteen groups, finer skipping, a larger footer, and slightly worse compression.&lt;/p&gt;

&lt;p&gt;That trade resolves differently by workload, which is the actual guidance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scan-heavy analytical tables&lt;/strong&gt;: keep 128 MB row groups. You are reading most of the file anyway and you want the compression and the low footer overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point-lookup and selective-filter tables&lt;/strong&gt;: 32–64 MB row groups genuinely help, and they compound with bloom filters, since both operate at row-group granularity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming and CDC tables&lt;/strong&gt;: smaller target files (128–256 MB) reduce write amplification during compaction, at the cost of more files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more layers of parallelism control sit on top. &lt;code&gt;read.split.target-size&lt;/code&gt; determines how a large file is divided across tasks, and it uses the &lt;code&gt;split_offsets&lt;/code&gt; recorded in each manifest entry, which are the Parquet row group boundaries, so splitting does not require reading footers at plan time. Iceberg 1.11.0 added adaptive split sizing for Spark 4.1 through &lt;code&gt;spark.sql.iceberg.read.adaptive-split-size.enabled&lt;/code&gt;, plus a session-level &lt;code&gt;spark.sql.iceberg.read.split-size&lt;/code&gt; override. Adaptive sizing fixes the chronic case where a medium scan produces ten splits on a two-hundred-core cluster.&lt;/p&gt;

&lt;p&gt;If you use AQE to control output file sizes during writes, note that &lt;code&gt;spark.sql.adaptive.advisoryPartitionSizeInBytes&lt;/code&gt; is measured as &lt;strong&gt;in-memory row size&lt;/strong&gt;, not on-disk compressed size. It must be set larger than &lt;code&gt;write.target-file-size-bytes&lt;/code&gt; by a data-dependent ratio, and Spark cannot write a file larger than a single task or spanning an Iceberg partition boundary.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bloom Filters: The Point-Lookup Escape Hatch
&lt;/h2&gt;

&lt;p&gt;Bloom filters solve the one case that min/max statistics structurally cannot: equality predicates on a high-cardinality column that is &lt;strong&gt;not&lt;/strong&gt; your sort key. A &lt;code&gt;trace_id&lt;/code&gt; or &lt;code&gt;user_id&lt;/code&gt; column with millions of scattered distinct values has bounds spanning the whole domain in every file. Sorting on it would fix pruning but you only get one leading sort column, and it is usually already spent.&lt;/p&gt;

&lt;p&gt;A Parquet bloom filter is a compact probabilistic structure per row group. It answers "definitely not present" with certainty and "possibly present" with a tunable false-positive rate. False negatives cannot occur, so it is always safe.&lt;/p&gt;
&lt;h3&gt;
  
  
  Configuration
&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;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.trace_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.trace_id'&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'0.01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'write.parquet.bloom-filter-ndv.column.trace_id'&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5000000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'write.parquet.bloom-filter-max-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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;bloom-filter-ndv&lt;/code&gt; is the property most guides omit and the one that decides whether the filter works. Without an expected distinct-value count, Parquet has to infer the bitset size, and you routinely end up either with a filter so small its false-positive rate approaches 100% (making it useless) or one clipped by &lt;code&gt;bloom-filter-max-bytes&lt;/code&gt; (same outcome, wasted bytes). Set it to a realistic per-row-group cardinality estimate. There is also an undocumented &lt;code&gt;write.parquet.bloom-filter-adaptive-enabled&lt;/code&gt; (default &lt;code&gt;false&lt;/code&gt;) present in &lt;code&gt;TableProperties&lt;/code&gt; but absent from the published configuration table.&lt;/p&gt;

&lt;p&gt;These properties affect &lt;strong&gt;newly written files only&lt;/strong&gt;. Existing files need a rewrite pass to gain filters:&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_time &amp;gt;= TIMESTAMP &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;2026-08-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;'rewrite-all'&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  When they help, and when they are pure overhead
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Useful?&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;
&lt;code&gt;WHERE trace_id = 'a7f...'&lt;/code&gt;, high cardinality, unsorted&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Min/max is useless; the bloom filter is the only mechanism left&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;WHERE status = 'active'&lt;/code&gt;, low cardinality&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Dictionary filtering already handles this exactly, with no false positives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHERE amount &amp;gt; 500&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Bloom filters only answer equality and &lt;code&gt;IN&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;WHERE customer_id = 481&lt;/code&gt; on the leading sort column&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Min/max already prunes; the filter is redundant footer bloat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;WHERE user_id IN (...)&lt;/code&gt; from a runtime join filter&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Spark's runtime filtering pushes these down as equality sets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sweet spot is two or three columns. Enabling bloom filters across a wide schema bloats footers, slows every write, and adds a read cost on every row group that the first two filter stages could not already resolve.&lt;/p&gt;
&lt;h3&gt;
  
  
  Which engines read them
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Iceberg's own reader&lt;/strong&gt; (Spark via the Iceberg data source, Flink, generic readers) uses &lt;code&gt;ParquetBloomRowGroupFilter&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trino&lt;/strong&gt; reads them too, which is often assumed otherwise. The catalog property is &lt;code&gt;parquet.use-bloom-filter&lt;/code&gt;, &lt;strong&gt;default &lt;code&gt;true&lt;/code&gt;&lt;/strong&gt;, with session property &lt;code&gt;parquet_use_bloom_filter&lt;/code&gt;. Trino can also write them for Iceberg tables via the &lt;code&gt;parquet_bloom_filter_columns&lt;/code&gt; table property.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify empirically rather than trusting either statement: run &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; on Trino and compare &lt;code&gt;physicalInputBytes&lt;/code&gt; before and after writing filters. If the number does not move, the filter is not being consulted for your predicate shape.&lt;/p&gt;
&lt;h2&gt;
  
  
  Puffin, NDV, and Cost-Based Optimization
&lt;/h2&gt;

&lt;p&gt;Everything above optimizes &lt;em&gt;scanning&lt;/em&gt;. Puffin statistics optimize &lt;em&gt;planning decisions&lt;/em&gt;: join order, join strategy, and aggregation sizing. On a three-way join, the difference between the best and worst join order can be two orders of magnitude in intermediate data volume, and no amount of file skipping recovers from a plan that builds a billion-row hash table first.&lt;/p&gt;
&lt;h3&gt;
  
  
  What is actually in a Puffin file
&lt;/h3&gt;

&lt;p&gt;Correcting a persistent misconception: Puffin does &lt;strong&gt;not&lt;/strong&gt; store column min/max bounds. Those live in manifest entries, as covered above. The &lt;a href="https://iceberg.apache.org/puffin-spec/" rel="noopener noreferrer"&gt;Puffin spec&lt;/a&gt; standardizes exactly two blob types:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apache-datasketches-theta-v1&lt;/code&gt;&lt;/strong&gt; — an Apache DataSketches Theta sketch for NDV (number of distinct values). The estimate is carried in the blob's &lt;code&gt;ndv&lt;/code&gt; property.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;deletion-vector-v1&lt;/code&gt;&lt;/strong&gt; — the format v3 Roaring bitmap deletion vector, discussed in the merge-on-read section below.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Collecting them
&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;-- Spark&lt;/span&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;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;'db.events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'region'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'product_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Trino&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&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;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'region'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  The three ways this silently fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Spark computes the stats and nothing uses them.&lt;/strong&gt; &lt;code&gt;spark.sql.iceberg.report-column-stats&lt;/code&gt; defaults to &lt;code&gt;true&lt;/code&gt;, so Iceberg does report them. But Spark's cost-based optimizer is gated on &lt;code&gt;spark.sql.cbo.enabled&lt;/code&gt;, which defaults to &lt;strong&gt;&lt;code&gt;false&lt;/code&gt;&lt;/strong&gt;. You compute sketches, Iceberg hands them over, and the optimizer ignores them. This is the single most common "statistics don't work" report, and the fix is one config:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spark.sql.cbo.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;spark.sql.cbo.joinReorder.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;2. Trino ignores multi-column sketches.&lt;/strong&gt; Trino's reader filters for the standard theta blob type and requires &lt;code&gt;fields().size() == 1&lt;/code&gt;, so any sketch covering multiple columns is skipped entirely. It is gated by &lt;code&gt;iceberg.table-statistics-enabled&lt;/code&gt; (default &lt;code&gt;true&lt;/code&gt;) and maintained on write by &lt;code&gt;iceberg.extended-statistics.collect-on-write&lt;/code&gt; (default &lt;code&gt;true&lt;/code&gt;). Re-analyzing a subset of columns after a full &lt;code&gt;ANALYZE&lt;/code&gt; requires calling &lt;code&gt;drop_extended_stats&lt;/code&gt; first, or the new run is rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. There is no invalidation mechanism at all.&lt;/strong&gt; This is the design weakness and it deserves emphasis. A Puffin blob pins the &lt;code&gt;snapshot-id&lt;/code&gt; it was computed from, and engines walk the snapshot ancestry to find the most recent statistics file. Nothing marks it stale. A sketch computed at snapshot N is served unchanged at snapshot N+10,000, so if the table doubled in size the optimizer is planning against fiction. Worse, a statistics file whose snapshot is no longer an ancestor, after a rollback or a branch rewrite, is simply &lt;strong&gt;not found&lt;/strong&gt;, and the table silently drops to no statistics with no error.&lt;/p&gt;

&lt;p&gt;Compaction makes this acute: rewriting files changes the cardinality landscape the sketches describe. Treat statistics refresh as a mandatory step in your maintenance sequence, not an occasional chore. In Trino, &lt;code&gt;SHOW STATS FOR db.events&lt;/code&gt; tells you what the optimizer currently believes; compare it against reality before you debug a bad plan. The &lt;a href="https://lakeops.dev/blog/iceberg-puffin-statistics-guide" rel="noopener noreferrer"&gt;Puffin statistics lifecycle&lt;/a&gt; has more on incremental sketch computation, which matters once full recomputation costs more than the compaction that triggered it.&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%2F48civi8g3oks35sqogwr.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%2F48civi8g3oks35sqogwr.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Manifest Health and Planning Latency
&lt;/h2&gt;

&lt;p&gt;Manifest fragmentation is the most overlooked cause of slow Iceberg queries, because the symptom does not look like a data problem. Queries that should plan in milliseconds take seconds, with perfect partitioning and a good sort order, and the time disappears before any data file is touched.&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%2Fuadlrvspfoorlanpfud4.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%2Fuadlrvspfoorlanpfud4.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  How fragmentation happens
&lt;/h3&gt;

&lt;p&gt;Every commit, whether append, overwrite, delete, or compaction, produces at least one new manifest. Streaming, micro-batch, and CDC tables accumulate thousands of them, each tracking a handful of files. The planner must open and evaluate every manifest in the snapshot. More manifests means more metadata round trips to object storage, more Avro parsing, and a longer serial-ish critical path before execution begins.&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;partition_spec_id&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;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_files_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;existing_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;files&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="k"&gt;length&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_manifest_mb&lt;/span&gt;
&lt;span class="k"&gt;FROM&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="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;partition_spec_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Manifests well under &lt;code&gt;commit.manifest.target-size-bytes&lt;/code&gt; (8 MB) mean fragmentation. Hundreds or thousands of manifests on a table with moderate file counts means your planning time is dominated by metadata I/O.&lt;/p&gt;
&lt;h3&gt;
  
  
  Rewriting manifests, and the &lt;code&gt;sort_by&lt;/code&gt; argument almost nobody uses
&lt;/h3&gt;

&lt;p&gt;Manifest rewriting is metadata-only. It does not read or write data files, which makes it the cheapest maintenance operation available with the largest effect on planning latency.&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;span class="n"&gt;sort_by&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'event_time_day'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;use_caching&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;sort_by&lt;/code&gt; arrived in Iceberg 1.11.0 and takes an array of &lt;strong&gt;partition transform names&lt;/strong&gt;, as documented in the &lt;a href="https://iceberg.apache.org/docs/latest/spark-procedures/" rel="noopener noreferrer"&gt;Spark procedures reference&lt;/a&gt;. It is arguably the highest-leverage planning knob in Iceberg and it is barely known, mainly because the default behavior sounds harmless and is not.&lt;/p&gt;

&lt;p&gt;The default sorts manifests &lt;strong&gt;by all partition transforms in spec order&lt;/strong&gt;. Consider a table partitioned by &lt;code&gt;(region, day(ts))&lt;/code&gt; where every production query filters on time and no query filters on region. The default clusters manifests by &lt;code&gt;region&lt;/code&gt; first, so each manifest ends up containing files spanning the full time range. The manifest-list &lt;code&gt;field_summary&lt;/code&gt; bounds on &lt;code&gt;ts_day&lt;/code&gt; overlap across every manifest, and &lt;code&gt;ManifestEvaluator&lt;/code&gt; prunes nothing at Stage 1. Passing &lt;code&gt;sort_by =&amp;gt; array('ts_day')&lt;/code&gt; reorders the clustering to match how the table is actually queried, and Stage 1 starts working.&lt;/p&gt;

&lt;p&gt;You can see the effect directly in the &lt;code&gt;ScanReport&lt;/code&gt;: &lt;code&gt;skippedDataManifests&lt;/code&gt; goes from near zero to most of the manifest count.&lt;/p&gt;

&lt;p&gt;Trino's equivalent is &lt;code&gt;ALTER TABLE db.events EXECUTE optimize_manifests&lt;/code&gt;, which clusters manifests by partitioning columns and reports &lt;code&gt;rewritten_manifests_count&lt;/code&gt;, &lt;code&gt;added_manifests_count&lt;/code&gt;, &lt;code&gt;kept_manifests_count&lt;/code&gt;, and &lt;code&gt;processed_manifest_entries_count&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Sequencing, and why it is not arbitrary
&lt;/h3&gt;

&lt;p&gt;Maintenance operations feed each other, and running them in the wrong order wastes compute or produces metadata describing files that no longer exist. The correct sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expire snapshots&lt;/strong&gt; beyond your retention window. This shrinks the file set every later step has to consider, and it is why expiry goes &lt;em&gt;first&lt;/em&gt;: compacting files that are about to be garbage-collected is pure waste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove orphan files&lt;/strong&gt; left by failed writes and by the snapshots you just expired. Use a grace period, never a zero threshold, or you will race in-flight writers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compact data files&lt;/strong&gt;, applying the sort or curve strategy, ideally scoped to the partitions that need it rather than the whole table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite manifests&lt;/strong&gt;, so the metadata tree indexes the layout that now exists rather than the one that did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh Puffin statistics&lt;/strong&gt;, so the optimizer's cardinality estimates match the files it will actually read.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run step 4 before step 3 and your manifests describe a dead file set. Skip step 5 and your CBO plans against a pre-compaction world, which is arguably worse than having no statistics at all, because a confidently wrong estimate produces a confidently wrong plan.&lt;/p&gt;
&lt;h2&gt;
  
  
  Merge-on-Read: The Delete Tax
&lt;/h2&gt;

&lt;p&gt;On merge-on-read tables, deletes are recorded rather than applied, and every reader pays to reconcile them. This is a read-performance topic that gets filed under "write strategy" and then surprises people when a table that was fast last quarter is not.&lt;/p&gt;
&lt;h3&gt;
  
  
  Position deletes and the shape of the cost
&lt;/h3&gt;

&lt;p&gt;The v2 position delete file records &lt;code&gt;(file_path, position)&lt;/code&gt; pairs, sorted by both so readers can push down by file and stream the merge without holding deletes in memory. The critical property is that &lt;strong&gt;cost scales with the number of delete files, not the number of deleted rows&lt;/strong&gt;. A table with 50 uncompacted small delete files means a full scan opens 51 files where it should open one.&lt;/p&gt;

&lt;p&gt;Published benchmarks on position-delete overhead show the cost curve jumping roughly 4× between 0% and 1% deleted rows, then rising only gradually from 1% to 100%. The fixed cost of &lt;em&gt;having any&lt;/em&gt; delete files dominates the marginal cost of having more deleted rows. That shape is the argument for compacting on file count aggressively rather than waiting for a deletion-ratio threshold to trip.&lt;/p&gt;
&lt;h3&gt;
  
  
  Equality deletes, and why they are much worse
&lt;/h3&gt;

&lt;p&gt;An equality delete file records the values of identifier fields rather than positions. That makes writes cheap, which is exactly why Flink's streaming upsert mode defaults to them: locating &lt;code&gt;(file_path, pos)&lt;/code&gt; would require a read-before-write at CDC rates.&lt;/p&gt;

&lt;p&gt;The cost lands entirely on readers, and two scoping rules make it severe. First, an equality delete applies when the data file's sequence number is &lt;strong&gt;strictly less than&lt;/strong&gt; the delete's, regardless of locality. Second, and this is the one that bites, &lt;strong&gt;if the delete file's partition spec is unpartitioned, it is a global delete applied against every data file in the table.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Equality delete files do carry their own metrics, so &lt;code&gt;InclusiveMetricsEvaluator&lt;/code&gt; can prune them by identifier bounds. Most streaming writers do not optimize for tight bounds, which means most real deployments do not get that benefit.&lt;/p&gt;

&lt;p&gt;Check where you stand:&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;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;file_format&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;files&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;record_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;deleted_rows&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;1048576&lt;/span&gt;   &lt;span class="k"&gt;AS&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;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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_format&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- content = 1 -&amp;gt; position deletes or deletion vectors&lt;/span&gt;
&lt;span class="c1"&gt;-- content = 2 -&amp;gt; equality deletes&lt;/span&gt;
&lt;span class="c1"&gt;-- file_format = 'puffin' -&amp;gt; v3 deletion vectors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Any &lt;code&gt;content = 2&lt;/code&gt; rows on a table that is not fed by Flink is a red flag worth chasing down.&lt;/p&gt;
&lt;h3&gt;
  
  
  Controls
&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;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.delete.mode'&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'merge-on-read'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'write.update.mode'&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'merge-on-read'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'write.merge.mode'&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'copy-on-write'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'write.delete.granularity'&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'file'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;All three mode properties default to &lt;code&gt;copy-on-write&lt;/code&gt;. &lt;code&gt;write.delete.granularity&lt;/code&gt; defaults to &lt;code&gt;partition&lt;/code&gt; at the table level; setting it to &lt;code&gt;file&lt;/code&gt; produces one delete file per data file, which is more files but each is precisely scoped and converts to a deletion vector without a merge step.&lt;/p&gt;

&lt;p&gt;For compaction, the two relevant &lt;code&gt;rewrite_data_files&lt;/code&gt; options are &lt;code&gt;delete-file-threshold&lt;/code&gt; (default &lt;code&gt;2147483647&lt;/code&gt;, effectively off) and &lt;code&gt;delete-ratio-threshold&lt;/code&gt; (default &lt;code&gt;0.3&lt;/code&gt;). Given the cost curve above, &lt;code&gt;delete-file-threshold&lt;/code&gt; is the more useful lever. Set it low, around 2 or 3, so any data file accumulating delete files gets rewritten rather than waiting for 30% of its rows to be deleted:&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="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;'2'&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;'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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note that &lt;code&gt;remove-dangling-deletes&lt;/code&gt; costs an extra commit and, per the documentation, works solely from data sequence numbers. It does not cover global equality deletes, invalid equality deletes, or multi-file position delete files.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Format v3 Changes for Reads
&lt;/h2&gt;

&lt;p&gt;Format v3 is complete and adopted. Version 4 is under active development and has not been formally adopted. For query performance specifically, v3 matters in four places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deletion vectors&lt;/strong&gt; replace position delete files. The spec allows &lt;strong&gt;at most one DV per data file per snapshot&lt;/strong&gt;, enforced by writers merging on commit. That converts delete application from "open N delete files and sort-merge by &lt;code&gt;(file_path, pos)&lt;/code&gt;" into "load one Roaring bitmap, test a bit." Manifests carry &lt;code&gt;referenced_data_file&lt;/code&gt;, &lt;code&gt;content_offset&lt;/code&gt;, and &lt;code&gt;content_size_in_bytes&lt;/code&gt;, so the reader issues a byte-range request for exactly the DV blob with no Puffin footer scan. Position delete files are deprecated in v3 and must be folded into a DV when one is created.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variant shredding&lt;/strong&gt; turns semi-structured data into something prunable. With &lt;code&gt;write.parquet.shred-variants&lt;/code&gt; (default &lt;code&gt;false&lt;/code&gt;), variant sub-fields become real Parquet columns with real statistics, and manifest bounds for variant columns are keyed by normalized JSON path such as &lt;code&gt;$['location']['latitude']&lt;/code&gt;. That is file skipping on JSON payloads, which was previously impossible. Bounds are not written for mixed-type fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Geospatial bounding boxes&lt;/strong&gt; give &lt;code&gt;geometry&lt;/code&gt; and &lt;code&gt;geography&lt;/code&gt; columns real lower and upper bounds as X/Y/Z/M points, with intersects checking added in 1.11.0. &lt;code&gt;geography&lt;/code&gt; permits &lt;code&gt;xmin &amp;gt; xmax&lt;/code&gt; to express antimeridian crossing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Row lineage&lt;/strong&gt; (&lt;code&gt;_row_id&lt;/code&gt;, &lt;code&gt;_last_updated_sequence_number&lt;/code&gt;) is always on for v3, not optional. It is inheritance-based, so readers materialize the values from &lt;code&gt;first_row_id + _pos&lt;/code&gt; and the manifest sequence number. Cheap, but it is work engines must implement, and coverage arrived incrementally across 1.9 through 1.11.&lt;/p&gt;
&lt;h3&gt;
  
  
  The engine support matrix, and why it is a one-way door
&lt;/h3&gt;

&lt;p&gt;Upgrading to v3 is a single atomic metadata change with no data rewrite. Rolling back, once deletion vectors have been written, is not practical. Check your entire read path first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;v3&lt;/th&gt;
&lt;th&gt;Deletion vectors&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spark (Iceberg 1.9+)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Can rewrite v2 deletes into v3 DVs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flink&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IcebergSink&lt;/code&gt; writes DVs as of 1.11.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trino 483&lt;/td&gt;
&lt;td&gt;Experimental&lt;/td&gt;
&lt;td&gt;Read only&lt;/td&gt;
&lt;td&gt;Row-level updates, deletes, and &lt;code&gt;OPTIMIZE&lt;/code&gt; are not supported on v3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;Yes (GA May 2026)&lt;/td&gt;
&lt;td&gt;Read and write&lt;/td&gt;
&lt;td&gt;Managed and externally managed tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Athena&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Creates and operates on v2 only; v3 errors out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DuckDB 1.5.3+&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Read and write&lt;/td&gt;
&lt;td&gt;Selects DV vs positional automatically from &lt;code&gt;format-version&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StarRocks 4.1&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supports defaults and row lineage; &lt;strong&gt;fails fast&lt;/strong&gt; on DVs, encryption, and geo types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClickHouse&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;v1 and v2 only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The practical reading: if Athena, StarRocks, or ClickHouse are anywhere in your read path, v3 breaks them. StarRocks' fail-fast behavior is deliberate and was introduced after cached DVs produced silently wrong results, which is a good reminder that "unsupported" is safer than "partially supported."&lt;/p&gt;

&lt;p&gt;One forward-looking note worth tracking. Today's manifest metrics are stored as &lt;code&gt;map&amp;lt;int, binary&amp;gt;&lt;/code&gt;, so every bound is length-prefixed binary that must be deserialized per value and the untyped map prevents Avro from projecting individual columns out of it. Version 4 moves these into a typed &lt;code&gt;content_stats&lt;/code&gt; struct with deterministic field IDs, which would make &lt;strong&gt;column projection over manifest statistics&lt;/strong&gt; possible: a planner filtering on one column could read only that column's stats instead of deserializing every bound in the file. It also adds &lt;code&gt;tight_bounds&lt;/code&gt; (a boolean asserting the bounds are exact, enabling metadata-only &lt;code&gt;MIN&lt;/code&gt;/&lt;code&gt;MAX&lt;/code&gt;) and &lt;code&gt;avg_value_size_in_bytes&lt;/code&gt; for memory estimation. Foundational types for this landed in 1.11.0.&lt;/p&gt;

&lt;p&gt;Related and immediately relevant: Iceberg 1.11.0 &lt;strong&gt;disabled min/max aggregation pushdown for string and binary columns&lt;/strong&gt;. It was a correctness fix, because truncated bounds produced wrong answers. If you documented metadata-only &lt;code&gt;MIN(varchar_col)&lt;/code&gt; as a trick, it now reads data.&lt;/p&gt;
&lt;h2&gt;
  
  
  Engine-Specific Tuning
&lt;/h2&gt;

&lt;p&gt;Table-level optimization benefits every engine that reads the table. Engine tuning is the layer on top, and the defaults are frequently wrong for lakehouse workloads.&lt;/p&gt;
&lt;h3&gt;
  
  
  Trino
&lt;/h3&gt;

&lt;p&gt;These are the properties from the &lt;a href="https://trino.io/docs/current/connector/iceberg.html" rel="noopener noreferrer"&gt;Iceberg connector docs&lt;/a&gt; that actually move query time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Default&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;code&gt;iceberg.dynamic-filtering.wait-timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long split generation waits for join-side filters to arrive. Too short and dynamic filtering silently does nothing on large builds.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iceberg.max-split-size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unset → table's &lt;code&gt;read.split.target-size&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Note the name; it is not &lt;code&gt;iceberg.split-size&lt;/code&gt;. Session override is &lt;code&gt;experimental_split_size&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iceberg.parquet-footer-cache.type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;none&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Off by default. Setting it to &lt;code&gt;memory&lt;/code&gt; is a free latency win on high-file-count tables.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iceberg.metadata-cache.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Coordinator-side. &lt;strong&gt;Deactivated when &lt;code&gt;fs.cache.enabled = true&lt;/code&gt;.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iceberg.query-partition-filter-required&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only enforced for schemas listed in &lt;code&gt;iceberg.query-partition-filter-required-schemas&lt;/code&gt;. Setting the boolean alone does nothing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iceberg.planning-threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2× coordinator cores&lt;/td&gt;
&lt;td&gt;Manifest read parallelism&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iceberg.bucket-execution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Trino's answer to storage-partitioned joins on bucketed tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iceberg.minimum-assigned-split-weight&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.05&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Raise for skewed aggregations; lower for many-small-file tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parquet.use-bloom-filter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bloom filters are consulted by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parquet.ignore-statistics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Escape hatch when a writer produced corrupt stats&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Dynamic filtering&lt;/strong&gt; is Trino's highest-value Iceberg feature and the one most affected by that first timeout. Trino collects values from the build side of a join at runtime and pushes them into the Iceberg scan, pruning files by &lt;em&gt;join key&lt;/em&gt; rather than only by &lt;code&gt;WHERE&lt;/code&gt; predicate. On a star schema, a small dimension filter can eliminate most of the fact table. If the build side takes longer than the wait timeout to materialize, splits are generated without the filter and the benefit vanishes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File caching&lt;/strong&gt; is separate from metadata caching and requires more setup than a single flag: &lt;code&gt;fs.cache.enabled&lt;/code&gt; (default &lt;code&gt;false&lt;/code&gt;), &lt;code&gt;fs.cache.directories&lt;/code&gt; (must exist on the coordinator &lt;em&gt;and&lt;/em&gt; every worker), and one of &lt;code&gt;fs.cache.max-sizes&lt;/code&gt; or &lt;code&gt;fs.cache.max-disk-usage-percentages&lt;/code&gt;. Node affinity comes from &lt;code&gt;node-scheduler.cache-preferred-hosts-count&lt;/code&gt; (default &lt;code&gt;2&lt;/code&gt;) in the coordinator config; raising it spreads load and adds resilience at the cost of effective cache size. Each catalog needs its own cache directory.&lt;/p&gt;
&lt;h3&gt;
  
  
  Spark
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Runtime filtering&lt;/strong&gt; is Iceberg's implementation of dynamic partition pruning, via &lt;code&gt;SupportsRuntimeV2Filtering&lt;/code&gt;. Spark re-plans the scan after the build side materializes, and Iceberg re-runs &lt;code&gt;InclusiveMetricsEvaluator&lt;/code&gt; with the runtime value set. The underappreciated part: this works on &lt;strong&gt;any column with bounds&lt;/strong&gt;, not just partition columns, which makes it strictly more capable than Hive-style DPP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage-partitioned joins&lt;/strong&gt; eliminate the shuffle entirely when both sides are bucketed on the join key. Getting them to actually engage requires more than the one flag most guides cite:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spark.sql.sources.v2.bucketing.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;spark.sql.iceberg.planning.preserve-data-grouping&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;spark.sql.sources.v2.bucketing.pushPartValues.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;spark.sql.requireAllClusterKeysForCoPartition&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;spark.sql.sources.v2.bucketing.partiallyClusteredDistribution.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;spark.sql.autoBroadcastJoinThreshold&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three failure modes from real deployments. &lt;code&gt;preserve-data-grouping&lt;/code&gt; defaults to &lt;code&gt;false&lt;/code&gt; and is the one people forget, so the join silently falls back to a shuffle. Partition columns must appear in the join condition or you get an &lt;code&gt;UnsupportedOperationException&lt;/code&gt; about retrieving values from an empty struct. And &lt;code&gt;MERGE INTO&lt;/code&gt; with a &lt;code&gt;WHEN NOT MATCHED&lt;/code&gt; clause still requires a full shuffle and can OOM under skew; the documented workaround is splitting matched and not-matched into two statements.&lt;/p&gt;

&lt;p&gt;Other Spark settings with real read impact:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.sql.iceberg.vectorization.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;table default (Parquet: &lt;code&gt;true&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Vectorized Parquet reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.sql.iceberg.data-planning-mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AUTO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LOCAL&lt;/code&gt; / &lt;code&gt;DISTRIBUTED&lt;/code&gt; manifest scanning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.sql.iceberg.executor-cache.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Caches delete files on executors so the same DV is not re-read by every task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.sql.iceberg.executor-cache.max-entry-size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;64 MB&lt;/td&gt;
&lt;td&gt;Silently excludes large equality delete files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.sql.iceberg.aggregate-push-down.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Metadata-only aggregates where bounds permit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.sql.iceberg.report-column-stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reports Puffin stats; needs &lt;code&gt;spark.sql.cbo.enabled&lt;/code&gt; to matter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The executor cache is the quietly valuable one on merge-on-read tables with concentrated deletes, and the 64 MB per-entry cap is the reason it sometimes appears not to work.&lt;/p&gt;
&lt;h3&gt;
  
  
  Everything else
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake&lt;/strong&gt; reached v3 GA in May 2026 with DV read and write on both managed and externally managed tables. Its internal pruning and caching behavior is proprietary and unpublished, so treat claims about how it layers on Iceberg statistics with suspicion, including this one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Athena&lt;/strong&gt; is v2-only. Its documentation cites an Iceberg &lt;em&gt;library&lt;/em&gt; version, which is frequently misread as a format version; do not conflate them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DuckDB&lt;/strong&gt; 1.5.3+ has full v3 read and write support including DVs, variant, and row lineage, and it is genuinely excellent for single-node work on well-compacted tables. &lt;code&gt;iceberg_metadata()&lt;/code&gt; is a fast way to inspect layout without a cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;StarRocks 4.1&lt;/strong&gt; adds native Iceberg &lt;code&gt;DELETE&lt;/code&gt;, global late materialization for v3, and a metadata cache covering tables, partition names, data files, and delete files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ClickHouse&lt;/strong&gt; does not enable &lt;code&gt;use_iceberg_partition_pruning&lt;/code&gt; by default. Setting it to &lt;code&gt;1&lt;/code&gt; is an easy and commonly missed win.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point that survives all of this: table-level layout, statistics, and manifest health benefit every engine simultaneously. Engine tuning benefits one. Fix the table first.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Runbook for One Slow Query
&lt;/h2&gt;

&lt;p&gt;When a specific query is slower than it should be, work down the funnel in order. Each step either finds the problem or rules out a stage.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Split planning from execution
&lt;/h3&gt;

&lt;p&gt;In Trino, check &lt;code&gt;planning_time&lt;/code&gt; against &lt;code&gt;execution_time&lt;/code&gt; in &lt;code&gt;system.runtime.queries&lt;/code&gt;, or read the query detail page. In Spark, look at the gap between job submission and first task launch. If planning dominates, skip to step 4. If execution dominates, the problem is how much data you are reading, so continue.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Confirm the predicate is reaching the scan
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;EXPLAIN&lt;/code&gt; and look at where your predicate landed. In Trino, a predicate absorbed into the scan's &lt;code&gt;constraint&lt;/code&gt; is enforced by Iceberg; one sitting in a &lt;code&gt;Filter&lt;/code&gt; node above the scan is evaluated per row after the data is read. A filter wrapped in a function the engine cannot push down, an implicit cast between timestamp precisions, or a comparison against a non-deterministic expression will all quietly demote your predicate to a post-scan filter.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Check whether the filter column has usable bounds
&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;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;files&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="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower_bound&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;files_with_bounds&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;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;readable_metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower_bound&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;distinct_lowers&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;&lt;code&gt;files_with_bounds&lt;/code&gt; well below &lt;code&gt;files&lt;/code&gt; means a metrics configuration problem: fix it with &lt;code&gt;write.metadata.metrics.column.&amp;lt;col&amp;gt;&lt;/code&gt; and a rewrite. &lt;code&gt;distinct_lowers&lt;/code&gt; well below &lt;code&gt;files&lt;/code&gt; means a clustering problem: fix it with a sorted compaction.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Check manifest count and clustering
&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;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;manifests&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="k"&gt;length&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="k"&gt;FROM&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;Hundreds of manifests averaging well under 8 MB means fragmentation. Run &lt;code&gt;rewrite_manifests&lt;/code&gt;, and pass &lt;code&gt;sort_by&lt;/code&gt; naming the transform your queries actually filter on rather than accepting the spec-order default.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Check the delete burden
&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;content&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;files&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;record_count&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;rows&lt;/span&gt;
&lt;span class="k"&gt;FROM&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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;High delete file counts, or any equality deletes outside a Flink pipeline, mean every scan is paying reconciliation cost. Compact with a low &lt;code&gt;delete-file-threshold&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  6. Check partition and file size distribution
&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;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;files&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="k"&gt;FROM&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;files&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;Thousands of tiny files in a few partitions means compaction lag. Tiny files uniformly across all partitions means the partition spec is too fine.&lt;/p&gt;
&lt;h3&gt;
  
  
  7. Only now, tune the engine
&lt;/h3&gt;

&lt;p&gt;Dynamic filtering timeouts, footer caches, split sizes, and join configs are real wins, but they are multipliers on a healthy table. Applying them first means tuning the constant factor on an exponential problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  How a Control Plane Runs This Loop
&lt;/h2&gt;

&lt;p&gt;Everything above is tractable by hand on ten tables. The structure of the problem changes as the lake grows, and it changes in a specific way worth naming: each individual decision stays easy, but the number of decisions grows with tables × engines × time, and every one of them expires.&lt;/p&gt;

&lt;p&gt;Consider what "keep sort orders correct" actually requires at a few hundred tables. You need query telemetry from every engine, because the optimal layout for a table depends on how Trino queries it, how Spark ETL writes to it, and how a Snowflake dashboard reads it, simultaneously, and no single engine has that view. You need to score candidate layouts against the combined mix. You need to validate a proposed layout before rewriting terabytes, because a sort order change is expensive to reverse. You need to sequence expiry, cleanup, compaction, manifest rewrite, and statistics refresh correctly per table. You need to avoid compacting a partition with an active writer, and handle optimistic-concurrency conflicts when you lose the race anyway. And you need to redo all of it when the query mix shifts.&lt;/p&gt;

&lt;p&gt;That is a control loop, and LakeOps exists to run it as one. Here is what that means concretely for the specific problems in this guide.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/platform" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Fplatform%2Fopengraph-image%3Fa200bfb0d0a82b37" height="" class="m-0" width=""&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer" class="c-link"&gt;
            Apache Iceberg Lakehouse Control Plane | LakeOps
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Manage your Iceberg lakehouse from one control plane — health-driven compaction, table maintenance, multi-engine routing, and observability.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  Sort order selected from telemetry, not from intuition
&lt;/h3&gt;

&lt;p&gt;The hardest question in this guide is which columns to cluster on. LakeOps answers it from observation: it collects &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, and &lt;code&gt;GROUP BY&lt;/code&gt; column frequency across the connected engines, builds a field-access profile per table, and evaluates single-column sort, multi-column sort, and Z-order strategies independently against that profile rather than applying one policy lake-wide. When a new dashboard ships that filters on a different column, the profile shifts and the next compaction cycle applies the updated layout. The &lt;a href="https://lakeops.dev/solutions/query-performance" rel="noopener noreferrer"&gt;query performance results&lt;/a&gt; published for this are 51% less data scanned per query, 12× faster queries, and 76% less CPU.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/solutions/governance" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Fsolutions%2Fgovernance%2Fopengraph-image%3F562eff4bd1d963f2" height="630" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/solutions/governance" rel="noopener noreferrer" class="c-link"&gt;
            Iceberg Lakehouse Governance | LakeOps
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Iceberg lakehouse governance — compliance automation, declarative policies, rollback safety, and audit trails across every catalog and engine.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&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%2Fmj79fcox1o6tbwi7xps8.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%2Fmj79fcox1o6tbwi7xps8.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Layout changes validated on a branch before production files move
&lt;/h3&gt;

&lt;p&gt;This is the part that makes automated layout changes defensible rather than reckless. Rewriting every data file in scope is expensive and awkward to undo, so the decision needs evidence before it is applied, not after.&lt;/p&gt;

&lt;p&gt;LakeOps runs &lt;strong&gt;layout simulations on Iceberg branches&lt;/strong&gt;: it branches from the latest snapshot, applies a candidate layout, replays observed production query patterns against it, compares scan reduction and file distribution to the baseline, and discards the branch. Only the strategy that measurably wins gets applied to production. Iceberg's branching model is what makes this possible at all, and it is a good example of a format capability that most teams never operationalize because wiring it up is more work than the layout question seemed to be worth.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/solutions/observability" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Fsolutions%2Fobservability%2Fopengraph-image%3Ffb36772da39164e6" height="630" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/solutions/observability" rel="noopener noreferrer" class="c-link"&gt;
            Iceberg Lakehouse Observability | LakeOps
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Iceberg table health scores, cross-engine monitoring, and proactive alerts for your data lakehouse. Track file layout, snapshot depth, and partition skew.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h3&gt;
  
  
  Compaction on a dedicated engine rather than a Spark cluster
&lt;/h3&gt;

&lt;p&gt;Sorted compaction is the expensive half of everything above, and running it on Spark means cluster startup, idle capacity, GC pressure, and OOM risk on large rewrites. LakeOps runs &lt;a href="https://lakeops.dev/solutions/compaction" rel="noopener noreferrer"&gt;compaction on a Rust and DataFusion engine&lt;/a&gt; instead, with conflict-aware Iceberg commits, bounded memory with disk spill, and partitions that have active writers excluded from the rewrite set. Its published benchmark compacts 200 GB in 221 seconds against 1,612 seconds for Spark on the same hardware, at a peak throughput of 2,522 MB/s. The &lt;a href="https://lakeops.dev/blog/iceberg-lake-compaction" rel="noopener noreferrer"&gt;compaction strategy deep dive&lt;/a&gt; covers how binpack, sort, and Z-order selection differ per table.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/solutions/compaction" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Fsolutions%2Fcompaction%2Fopengraph-image%3F42b435acb522a3af" height="630" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/solutions/compaction" rel="noopener noreferrer" class="c-link"&gt;
            Apache Iceberg Lakehouse Compaction | LakeOps
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Query-aware Iceberg compaction on a Rust engine. Sorts Parquet files by production query patterns. 95% faster than Spark. Lakehouse ready.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h3&gt;
  
  
  The maintenance sequence, executed in dependency order
&lt;/h3&gt;

&lt;p&gt;The five-step sequence earlier in this guide is not hard to understand and is hard to hold correct across hundreds of tables with different retention windows, write patterns, and SLAs. LakeOps sequences expiry, orphan cleanup, sorted compaction, manifest rewrite, and statistics refresh automatically, per table, with each step's output feeding the next. Puffin NDV sketches are recomputed after the file layout changes rather than on a fixed schedule, which is the only way the staleness problem described earlier stays solved rather than solved once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Health classification, so you know which tables to care about
&lt;/h3&gt;

&lt;p&gt;Rather than a schedule, maintenance triggers on structural signals: file count and size distribution, manifest depth, snapshot accumulation, delete-file ratio, partition skew, and &lt;strong&gt;sort-order alignment with observed query patterns&lt;/strong&gt;. That last signal is the one that has no equivalent in schedule-driven maintenance, because it is not a property of the table at all. It is a property of the relationship between the table and its workload, and it can degrade without a single byte being written. &lt;a href="https://lakeops.dev/solutions/observability" rel="noopener noreferrer"&gt;Lake-wide observability&lt;/a&gt; surfaces it alongside the structural signals so a table whose layout has drifted out of alignment shows up before users file a ticket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routing, which closes the loop
&lt;/h3&gt;

&lt;p&gt;Physical layout determines which engines are viable for a query. A well-clustered table with tight bounds makes a DuckDB point lookup faster and cheaper than the same query on a distributed engine; a table needing a full scan does not. LakeOps routes queries from a single SQL endpoint to the engine best suited to each query shape, and those routing decisions feed back into the same telemetry that drives layout, so agent and application query patterns inform the next sort-order decision. The &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;platform overview&lt;/a&gt; covers how the observe, maintain, compact, govern, and route capabilities compose.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/blog/routing-multiple-query-engines-with-iceberg" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fapi.getpronto.io%2Fv1%2Ffile%2Fuser-cmosmjqho103245978n6h8tqsbi%2Ficeberg-multi-engine-query-routing-sql-1779536259863-e3dfcb25b1d7513d.png" height="450" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/blog/routing-multiple-query-engines-with-iceberg" rel="noopener noreferrer" class="c-link"&gt;
            Routing Multiple Query Engines with Iceberg - LakeOps Blog
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Route queries across Trino, Spark, DuckDB, Athena, and more on shared Iceberg tables. Routing architecture, strategies, dialect translation, and patterns.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/solutions/query-routing" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Fsolutions%2Fquery-routing%2Fopengraph-image%3Fc74ccc615bce41e0" height="630" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/solutions/query-routing" rel="noopener noreferrer" class="c-link"&gt;
            Iceberg Lakehouse Multi-Engine Query Routing | LakeOps
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            One SQL endpoint for your lakehouse. Route Iceberg queries to Trino, Spark, DuckDB, Snowflake, or Athena by cost, latency, or workload.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;None of this is a substitute for understanding the mechanics. It is a substitute for running the loop by hand, on a schedule you set once and never revisited, across a lake that changed underneath you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lever&lt;/th&gt;
&lt;th&gt;Typical impact&lt;/th&gt;
&lt;th&gt;When to apply&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Verify metrics exist on filter columns&lt;/td&gt;
&lt;td&gt;Enables Stage 2 at all&lt;/td&gt;
&lt;td&gt;First. Before any layout work.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partition on time, plus one high-value dimension&lt;/td&gt;
&lt;td&gt;90–99% coarse elimination&lt;/td&gt;
&lt;td&gt;Table creation; evolve as query patterns shift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global sort on the dominant filter column&lt;/td&gt;
&lt;td&gt;Large improvement in file-level skipping&lt;/td&gt;
&lt;td&gt;Once you know the dominant predicate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.distribution-mode = range&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turns a declared sort into real disjoint bounds&lt;/td&gt;
&lt;td&gt;Whenever a sort order is declared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Z-order on 2–4 columns&lt;/td&gt;
&lt;td&gt;Balanced pruning across dimensions&lt;/td&gt;
&lt;td&gt;Varied multi-column filters; check &lt;code&gt;var-length-contribution&lt;/code&gt; on string keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bloom filters on 2–3 high-cardinality keys&lt;/td&gt;
&lt;td&gt;Decisive for point lookups&lt;/td&gt;
&lt;td&gt;Equality predicates on non-sort-key columns; always set &lt;code&gt;bloom-filter-ndv&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row group size 32–64 MB&lt;/td&gt;
&lt;td&gt;Finer in-file skipping&lt;/td&gt;
&lt;td&gt;Selective and point-lookup workloads only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rewrite_manifests&lt;/code&gt; with &lt;code&gt;sort_by&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Seconds to milliseconds of planning&lt;/td&gt;
&lt;td&gt;Manifest count in the hundreds, or &lt;code&gt;skippedDataManifests&lt;/code&gt; near zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compact with low &lt;code&gt;delete-file-threshold&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Removes merge-on-read reconciliation cost&lt;/td&gt;
&lt;td&gt;Any MoR table; urgently if equality deletes are present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh Puffin stats after compaction&lt;/td&gt;
&lt;td&gt;Correct join orders&lt;/td&gt;
&lt;td&gt;Every time the file layout changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable &lt;code&gt;spark.sql.cbo.enabled&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Makes collected stats actually count&lt;/td&gt;
&lt;td&gt;If you compute stats and see no plan change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project only needed columns&lt;/td&gt;
&lt;td&gt;Linear I/O reduction&lt;/td&gt;
&lt;td&gt;Always on wide tables&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Iceberg query performance is not an engine property. It is a function of how much data the planner is able to discard, and every layer of that decision is something you control at write and maintenance time: whether the statistics exist, whether the bounds are tight enough to be useful, whether the manifests are clustered the way your queries filter, whether the deletes have been applied, and whether the optimizer's cardinality estimates describe the table that exists today.&lt;/p&gt;

&lt;p&gt;Those layers compound rather than add. A table with sensible partitioning, globally sorted data, bloom filters on its lookup keys, consolidated manifests sorted on the hot transform, resolved deletes, and current statistics hands the engine everything it needs to eliminate 99% of the work before execution starts. The same table missing any one of them can lose most of that benefit, which is why the diagnostic order in the runbook matters more than any individual knob.&lt;/p&gt;

&lt;p&gt;The engineering is the same whether you run it with carefully sequenced Spark procedures or hand the loop to a control plane like &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; that senses query patterns, plans layout per table, validates on a branch, and measures the result. Make data skipping work at every level, keep the metadata honest, and make sure the statistics still describe reality.&lt;/p&gt;

</description>
      <category>iceberg</category>
      <category>dataengineering</category>
      <category>snowflake</category>
      <category>sql</category>
    </item>
    <item>
      <title>Apache Iceberg Governance: Access Control, Policies, and Audit for Open Lakehouses</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Wed, 23 Sep 2026 09:45:20 +0000</pubDate>
      <link>https://dev.to/jonisar/apache-iceberg-governance-tools-access-control-policies-and-audit-for-open-lakehouses-f61</link>
      <guid>https://dev.to/jonisar/apache-iceberg-governance-tools-access-control-policies-and-audit-for-open-lakehouses-f61</guid>
      <description>&lt;p&gt;Governance in an Apache Iceberg lakehouse is not a feature you enable. It is an architecture you assemble.&lt;/p&gt;

&lt;p&gt;In a data warehouse — Snowflake, BigQuery, Redshift — governance is built in. The vendor controls access, enforces policies, manages retention, and produces audit trails. You configure roles and trust the system. The trade-off is lock-in: your data lives in a proprietary format, governed by a single vendor's rules.&lt;/p&gt;

&lt;p&gt;Iceberg inverts this. The table format is open, engine-agnostic, and deliberately governance-unaware. Any engine can read any table. No single system owns access control. That openness creates a governance vacuum that you have to fill with a stack of purpose-built tools — and filling it correctly is one of the hardest operational problems in production lakehouses.&lt;/p&gt;

&lt;p&gt;This guide covers the governance tools, patterns, and architectural layers that production Iceberg lakehouses need — from catalog-level RBAC and credential vending to the new Read Restrictions spec, policy engines, compliance patterns, and the operational governance layer that most teams overlook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Iceberg Governance Works Differently
&lt;/h2&gt;

&lt;p&gt;The Iceberg community made a deliberate choice: the table format is responsible for data portability, not governance. Governance is someone else's job.&lt;/p&gt;

&lt;p&gt;Three reasons this is the right boundary:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portability would be destroyed.&lt;/strong&gt; If Iceberg metadata included ACLs and masking rules, every engine would need to implement the same governance model — the same RBAC structure, the same masking functions, the same row-filter syntax. Any engine that doesn't implement the full model either breaks compatibility or silently bypasses security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evolution would be frozen.&lt;/strong&gt; Governance requirements change faster than storage formats. GDPR, CCPA, DORA, the AI Act — regulatory regimes evolve, classification taxonomies expand, organizational structures shift. Encoding governance in metadata means every policy change requires metadata migration across potentially thousands of tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enforcement would be unverifiable.&lt;/strong&gt; When rules live in the format, enforcement depends on the reader. A well-behaved Spark job respects the ACL. A misconfigured Trino deployment ignores it. A custom Parquet reader bypasses it entirely. There is no enforcement authority — only conventions that cooperative engines follow and adversarial engines ignore.&lt;/p&gt;

&lt;p&gt;The result: governance in an Iceberg lakehouse must be decomposed into separate, independently evolvable layers. The ecosystem has converged on three.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three-Layer Governance Model
&lt;/h2&gt;

&lt;p&gt;Every production Iceberg governance architecture decomposes into three layers:&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;Responsibility&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Table format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Data portability and structural integrity&lt;/td&gt;
&lt;td&gt;Apache Iceberg (metadata, snapshots, schema)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Catalog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enforcement and coordination&lt;/td&gt;
&lt;td&gt;Apache Polaris, Lakekeeper, Nessie, Gravitino, AWS Glue, Unity Catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Policy engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rules, classification, and context&lt;/td&gt;
&lt;td&gt;OPA, Cedar, OpenFGA, Apache Ranger, cloud IAM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The format&lt;/strong&gt; defines what the data looks like — how data files, manifests, snapshots, and metadata are organized. It carries structural metadata that governance systems can leverage (column names for classification, snapshot history for audit, table properties for labels), but it never interprets these as rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The catalog&lt;/strong&gt; enforces governance. Every operation — table creation, schema alteration, data read, snapshot commit — routes through the catalog. This makes the catalog the mandatory intermediary between engines and data. If the catalog denies access, the engine never sees the metadata, let alone the data files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The policy engine&lt;/strong&gt; defines the rules the catalog enforces. This separation is critical: the catalog is the enforcement mechanism, but it should not be the rule-authoring system. Policy engines are purpose-built for defining, versioning, auditing, and distributing access policies.&lt;/p&gt;

&lt;p&gt;The three-layer model means you can swap Ranger for OPA or Cedar without changing the catalog. You can migrate from Glue to Polaris without rewriting your policies. You can upgrade from Iceberg v2 to v3 without touching your governance model. Each layer evolves independently — which is exactly the modularity that makes open lakehouses viable in the first place.&lt;/p&gt;

&lt;p&gt;But governance alone does not guarantee that data is actually usable. A table that passes every access control check but takes 45 minutes to query because of 200,000 small files is technically governed but operationally useless. That is where operational governance — table health, maintenance policies, and lifecycle management — becomes essential. &lt;/p&gt;

&lt;p&gt;A control plane like &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; fills this gap: it handles autonomous table maintenance, structural health monitoring, and compaction across all engines — ensuring that governed data is also operationally sound. &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%2Fdh0kzmszdytaihbg0jyy.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%2Fdh0kzmszdytaihbg0jyy.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For teams designing governance architectures, understanding how access governance and operational governance interact is a recurring theme — and one we will return to.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Layer 1: The Catalog as Enforcement Point
&lt;/h2&gt;

&lt;p&gt;The catalog is where governance gets teeth. Without a governance-capable catalog, no amount of policy sophistication upstream will matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apache Polaris: The Reference Implementation
&lt;/h3&gt;

&lt;p&gt;Apache Polaris (graduated from incubation in early 2026) is the most fully-realized governance catalog for Iceberg. It implements a four-level RBAC model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Privileges&lt;/strong&gt; are granted to &lt;strong&gt;catalog roles&lt;/strong&gt; (e.g., &lt;code&gt;finance_reader&lt;/code&gt; grants &lt;code&gt;TABLE_READ_DATA&lt;/code&gt; on the &lt;code&gt;finance&lt;/code&gt; namespace)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catalog roles&lt;/strong&gt; are granted to &lt;strong&gt;principal roles&lt;/strong&gt; (e.g., &lt;code&gt;data_engineer_role&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Principal roles&lt;/strong&gt; are assigned to &lt;strong&gt;service principals&lt;/strong&gt; (e.g., the Spark ingestion service, the Trino BI cluster)&lt;/li&gt;
&lt;li&gt;Privileges never attach directly to a service principal — the two-level role indirection means identity management and permission management evolve independently&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The privilege model is granular: &lt;code&gt;TABLE_READ_DATA&lt;/code&gt;, &lt;code&gt;TABLE_WRITE_DATA&lt;/code&gt;, &lt;code&gt;TABLE_CREATE&lt;/code&gt;, &lt;code&gt;TABLE_DROP&lt;/code&gt;, &lt;code&gt;TABLE_LIST&lt;/code&gt;, &lt;code&gt;NAMESPACE_CREATE&lt;/code&gt;, &lt;code&gt;NAMESPACE_LIST&lt;/code&gt;, &lt;code&gt;CATALOG_MANAGE&lt;/code&gt;, plus administrative privileges for managing grants. Privileges scope to the namespace hierarchy — granting &lt;code&gt;TABLE_READ_DATA&lt;/code&gt; on &lt;code&gt;analytics&lt;/code&gt; implicitly covers &lt;code&gt;analytics.clickstream&lt;/code&gt; and &lt;code&gt;analytics.sessions&lt;/code&gt;. This enables the principle of least privilege: a data scientist's service principal can read tables in &lt;code&gt;analytics&lt;/code&gt; without being able to create tables, alter schemas, or access the &lt;code&gt;raw&lt;/code&gt; namespace.&lt;/p&gt;

&lt;p&gt;Polaris 1.7.0 also introduced a &lt;strong&gt;Policy framework&lt;/strong&gt; for defining lifecycle and operational rules directly in the catalog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/api/catalog/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;/namespaces/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;/policies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production-snapshot-retention"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system.snapshot-expiry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max-snapshot-age-ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;604800000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"min-snapshots-to-keep"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Policies attach at catalog, namespace, or table level, with inheritance flowing downward. A table-level policy overrides namespace defaults, which override catalog defaults. This gives teams declarative, auditable lifecycle governance without per-table manual configuration.&lt;/p&gt;
&lt;h3&gt;
  
  
  Credential Vending: Zero-Trust Storage Access
&lt;/h3&gt;

&lt;p&gt;Credential vending is the foundation of zero-trust lakehouse security, and it solves one of the hardest governance problems.&lt;/p&gt;

&lt;p&gt;Without credential vending, engines need direct access to the storage bucket — which means they can read any file in the bucket regardless of table-level access controls. With credential vending, the catalog vends short-lived, scoped storage credentials (AWS STS temporary credentials, GCS signed URLs, Azure SAS tokens) that only allow access to the specific data files in the specific table the engine is authorized to read.&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Engine authenticates to Polaris with OAuth2 credentials&lt;/li&gt;
&lt;li&gt;Polaris evaluates the request against its RBAC model&lt;/li&gt;
&lt;li&gt;If authorized, Polaris calls AWS STS &lt;code&gt;AssumeRole&lt;/code&gt; with an inline session policy scoped to the exact table locations&lt;/li&gt;
&lt;li&gt;Engine receives temporary credentials valid for minutes — read-only, specific path prefix, short-lived&lt;/li&gt;
&lt;li&gt;Engine accesses storage directly with scoped credentials&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No long-lived storage credentials are distributed. A compromised engine token is scoped to one table and expires quickly. The catalog is the single chokepoint through which all storage access flows.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Catalog-Level Enforcement Matters for Multi-Engine
&lt;/h3&gt;

&lt;p&gt;This is where catalog-level governance pays for itself. When Spark, Trino, Flink, DuckDB, and Snowflake all access the same Iceberg tables, the alternative is configuring access control separately in each engine. Consider what that looks like in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spark has its own ACL model via &lt;code&gt;TableCatalog&lt;/code&gt; security&lt;/li&gt;
&lt;li&gt;Trino has &lt;code&gt;access-control.type&lt;/code&gt; in its catalog configuration, with its own rule syntax&lt;/li&gt;
&lt;li&gt;Flink relies on whatever identity the job was submitted with&lt;/li&gt;
&lt;li&gt;DuckDB has no native access control model at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any drift — a permission granted in Spark but not in Trino — creates a bypass path. A data engineer discovers they can read &lt;code&gt;restricted.financial_transactions&lt;/code&gt; through DuckDB even though Trino denies the same query. The policy was configured in Trino's access control file but not in the DuckDB deployment, because DuckDB has no mechanism to enforce it. This is not hypothetical — it is the default failure mode in multi-engine lakehouses without catalog-level enforcement.&lt;/p&gt;

&lt;p&gt;With catalog-level enforcement, every engine that connects through the REST Catalog protocol inherits the same governance model. A data scientist denied access to &lt;code&gt;financial.transactions&lt;/code&gt; is denied regardless of whether they query through Spark, Trino, or DuckDB — because the catalog never returns the metadata, and credential vending never issues scoped storage credentials. A new engine inherits the full governance model the moment it connects. A policy change propagates immediately because the enforcement point is singular.&lt;/p&gt;

&lt;p&gt;For a detailed comparison of catalog governance capabilities, see the &lt;a href="https://lakeops.dev/blog/best-catalog-for-apache-iceberg" rel="noopener noreferrer"&gt;catalog comparison on LakeOps&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 2: Policy Engines — OPA, Ranger, and Cloud IAM
&lt;/h2&gt;

&lt;p&gt;The catalog enforces decisions. Policy engines make them. This separation matters because production enterprises already have policy systems, and a governance architecture that requires replacing all of them is dead on arrival.&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%2Frmsv2q323bcve5btzl9g.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%2Frmsv2q323bcve5btzl9g.png" alt=" " width="800" height="470"&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%2Fkzo57z6uapoitapd1g21.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%2Fkzo57z6uapoitapd1g21.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Open Policy Agent (OPA)
&lt;/h3&gt;

&lt;p&gt;OPA is the standard for policy-as-code in cloud-native environments. Policies are written in Rego, versioned in Git, tested with unit tests, and deployed through CI/CD — the same workflows that manage application code. This is governance-as-code: auditable, reviewable, reproducible. In 2026, Polaris's OPA integration is maturing, and Lakekeeper includes a native OPA bridge that translates its OpenFGA permissions into OPA format for engines like Trino.&lt;/p&gt;

&lt;p&gt;An OPA policy for Iceberg table access can evaluate rich context beyond identity:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;iceberg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authz&lt;/span&gt;

&lt;span class="ow"&gt;default&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"TABLE_READ_DATA"&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;classification&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"restricted"&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"TABLE_READ_DATA"&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;classification&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"restricted"&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clearance&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"pii_cleared"&lt;/span&gt;
    &lt;span class="n"&gt;is_business_hours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&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 is attribute-based access control (ABAC) — policies that evaluate the requesting principal's team membership and clearance, the table's data classification, the columns being accessed, the time window, and the query pattern. ABAC goes where RBAC cannot: purpose limitation under GDPR (restricting access based on declared query purpose), time-restricted production access, classification-conditional column masking.&lt;/p&gt;
&lt;h3&gt;
  
  
  Lakekeeper: Cedar and OpenFGA
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.lakekeeper.io/" rel="noopener noreferrer"&gt;Lakekeeper&lt;/a&gt; — a Rust-based Iceberg REST catalog — takes a different approach to authorization by supporting two policy backends:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenFGA&lt;/strong&gt; (open-source default) uses relationship tuples for ReBAC — relationship-based access control. Permissions are expressed as relationships between principals and objects, with bi-directional inheritance flowing through the namespace hierarchy. An object owner can grant access to their own tables through the API or UI, combining RBAC with discretionary access control. Lakekeeper requires OpenFGA v1.11+ for idempotent-write semantics and includes an OPA bridge that translates OpenFGA permissions into OPA format for query engines like Trino.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cedar&lt;/strong&gt; (available with Lakekeeper+) embeds authorization directly — no external service required. Policies are versioned, reviewable code with per-decision audit traces. Cedar supports ABAC natively: policies can evaluate resource attributes, tags, time conditions, and request context. A powerful pattern is storing access-control lists as table properties (&lt;code&gt;access-readers&lt;/code&gt;, &lt;code&gt;access-owners&lt;/code&gt;) that Cedar evaluates at query time — access control metadata lives with the data it governs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;OpenFGA&lt;/th&gt;
&lt;th&gt;Cedar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Extra service&lt;/td&gt;
&lt;td&gt;Yes (OpenFGA + database)&lt;/td&gt;
&lt;td&gt;No, built into Lakekeeper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission model&lt;/td&gt;
&lt;td&gt;Relationships stored as data&lt;/td&gt;
&lt;td&gt;Policies deployed as code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ABAC support&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (time, tags, attributes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime changes&lt;/td&gt;
&lt;td&gt;API/UI grants&lt;/td&gt;
&lt;td&gt;Redeploy policy source&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Apache Ranger
&lt;/h3&gt;

&lt;p&gt;Ranger has been the governance standard in Hadoop ecosystems for over a decade. Organizations with existing Ranger deployments — policies, audit logs, classification taxonomies — need a migration path that preserves their investment. Polaris delegates authorization decisions to Ranger's REST API, passing table identity, namespace, operation type, and requesting principal. Ranger evaluates against its policy store and returns the decision. The enforcement point changes; the rules remain.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cloud IAM Integration
&lt;/h3&gt;

&lt;p&gt;AWS IAM, Google Cloud IAM, and Azure RBAC are the default authorization systems for cloud infrastructure. The key principle: the catalog does not replace cloud IAM — it layers table-level governance on top. Cloud IAM controls who can reach the catalog. The catalog controls who can access specific tables. The policy engine defines the rules for both.&lt;/p&gt;
&lt;h2&gt;
  
  
  Iceberg Read Restrictions: Spec-Level Fine-Grained Access Control
&lt;/h2&gt;

&lt;p&gt;The most significant governance development in the Iceberg ecosystem in 2026 is Read Restrictions — an extension to the Iceberg REST Catalog specification that moves column masking and row-level security into the catalog protocol itself.&lt;/p&gt;
&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;When a client loads a table, the catalog now returns the Iceberg metadata along with two additional governance instructions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;required-row-filter&lt;/code&gt;&lt;/strong&gt; — an Iceberg predicate (e.g., &lt;code&gt;country = 'USA'&lt;/code&gt;) that the reader must apply. Any row evaluating to false must not appear in results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;required-column-projections&lt;/code&gt;&lt;/strong&gt; — a list of columns the reader must mask per a specified action.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are scoped to the principal who made the request. The same table returns different restrictions for different callers as policies and roles change.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"read-restrictions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required-row-filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"left"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"right"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"literal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USA"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required-column-projections"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"show-last-4"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha-256-global"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace-with-null"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The spec defines nine standardized masking actions — &lt;code&gt;replace-with-null&lt;/code&gt;, &lt;code&gt;show-last-4&lt;/code&gt;, &lt;code&gt;sha-256-global&lt;/code&gt;, &lt;code&gt;mask-alphanum&lt;/code&gt;, truncation, and others. Two engines implementing the spec independently produce identical masked output, making governance truly interoperable.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Trust Contract
&lt;/h3&gt;

&lt;p&gt;Read Restrictions shift governance from a deny-or-allow model to a nuanced instructions model. The catalog evaluates its policies and returns caller-specific restrictions; the trusted engine is responsible for applying them. Trust is established through mTLS or OAuth delegation — the catalog must know that the engine will actually enforce the restrictions before returning data access.&lt;/p&gt;

&lt;p&gt;This is a pragmatic design. It avoids the impossible requirement that every engine implement the same governance model natively. Instead, the catalog speaks a standard governance protocol, and engines that support it participate in fine-grained access control automatically.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Identity Gap
&lt;/h3&gt;

&lt;p&gt;One practical limitation: Read Restrictions work cleanly for engines with federated identity where the requesting user's identity flows through to the catalog. Spark with connect-style setup and Trino with OAuth pass-through can propagate per-user identity. But the identity models are far from uniform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spark&lt;/strong&gt; authenticates per-application (one identity per Spark session, shared across all users of a notebook or ETL job)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trino&lt;/strong&gt; supports OAuth pass-through for interactive queries but uses service accounts for scheduled jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flink&lt;/strong&gt; connects per-job — a streaming pipeline runs as a single service principal for its entire lifetime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DuckDB, ClickHouse, Postgres&lt;/strong&gt; typically connect with a single set of catalog credentials — no per-query identity pass-through at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For engines without per-user identity, Read Restrictions apply uniformly to the service principal, not to the human who submitted the query. A &lt;code&gt;show-last-4&lt;/code&gt; mask on SSN protects data from the service account — but if that account has full access, every user behind it sees unmasked data. Identity-mapping infrastructure that translates per-engine session identity to catalog-level principals mostly doesn't exist yet.&lt;/p&gt;

&lt;p&gt;This is not a flaw in Read Restrictions — it is a gap in the identity fabric that the ecosystem is still building. For production deployments, it means Read Restrictions are most effective when paired with engines that support identity federation, and least effective for the embedded and lightweight engines where governance gaps tend to be largest.&lt;/p&gt;
&lt;h2&gt;
  
  
  Compliance Patterns: GDPR, Retention, and Audit
&lt;/h2&gt;

&lt;p&gt;Governance in regulated environments goes beyond access control. Compliance requires proving that data lifecycle policies are enforced continuously.&lt;/p&gt;
&lt;h3&gt;
  
  
  GDPR Right to Be Forgotten
&lt;/h3&gt;

&lt;p&gt;Iceberg's immutability model creates a direct tension with GDPR Article 17. When you run &lt;code&gt;DELETE FROM users WHERE user_id = 48213&lt;/code&gt;, Iceberg marks the rows as deleted (via delete files or data file rewrites) but creates a new snapshot while keeping all old snapshots intact. The old data is still accessible through time travel.&lt;/p&gt;

&lt;p&gt;Physical erasure requires a three-step pipeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;What Happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Logically removes the row (creates delete file)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Compaction&lt;/td&gt;
&lt;td&gt;Rewrites data files without the deleted rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Snapshot expiration + orphan cleanup&lt;/td&gt;
&lt;td&gt;Removes old data files that contained the row&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only after all three steps is the personal data physically, irreversibly removed from storage. But the Iceberg table is not the only place the data lives. S3 versioning preserves old object versions even after Iceberg deletes the file. Disaster-recovery replicas, downstream data marts, ML feature stores, BI caches, and backups may all contain copies. A complete erasure pipeline must account for every replication path — not just the source table.&lt;/p&gt;

&lt;p&gt;A table with 14-day snapshot retention cannot complete erasure in less than 14 days without expiring snapshots early — which removes the recovery window for every other row. The practical solution: set retention windows shorter than the regulatory deadline on any table holding personal data. GDPR allows one month; a table with seven-day snapshot retention, a seven-day S3 versioning lifecycle, and weekly maintenance completes the sequence in under three weeks.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cryptographic Shredding: The Alternative
&lt;/h3&gt;

&lt;p&gt;Cryptographic shredding inverts the problem. Instead of finding and destroying every copy of the data across snapshots, replicas, and backups, you encrypt personal data under a key unique to each data subject, store the keys in a small, mutable key store, and delete the key when an erasure request arrives.&lt;/p&gt;

&lt;p&gt;Every copy of the ciphertext — in every snapshot, version, replica, backup, and downstream extract — becomes unreadable at the same instant. The ciphertext is still there. The information is gone.&lt;/p&gt;

&lt;p&gt;The pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each data subject gets a data encryption key (DEK), wrapped by a KMS master key&lt;/li&gt;
&lt;li&gt;PII columns are encrypted with the subject's DEK before being written to the lakehouse&lt;/li&gt;
&lt;li&gt;The key store is a small table (bucketed on &lt;code&gt;user_id&lt;/code&gt;) with aggressive retention — one-day snapshot retention, copy-on-write deletes&lt;/li&gt;
&lt;li&gt;Erasure = delete the subject's row from the key store&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is distinct from Iceberg's native table encryption (which protects entire tables at rest with per-file keys). Crypto-shredding is an application-level pattern for per-subject erasure. The two compose: a table can use Iceberg encryption for at-rest protection &lt;em&gt;and&lt;/em&gt; per-subject keys for erasure.&lt;/p&gt;

&lt;p&gt;The trade-offs are real: encryption adds read/write overhead to every query touching PII columns, the key store becomes a critical dependency, and AWS KMS enforces a 7–30 day waiting period for key deletion. But for large tables where full compaction is expensive, or environments with many downstream copies, crypto-shredding can be the only way to meet erasure deadlines reliably.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Governance and Maintenance Are Inseparable
&lt;/h3&gt;

&lt;p&gt;Without automated, continuous maintenance — compaction running after deletes, snapshot expiration enforced on schedule, orphan cleanup reclaiming storage — GDPR erasure deadlines cannot be met reliably across hundreds of tables. An erasure queue with deadlines and completion tracking is the audit story. Without it, you cannot prove compliance. Manual maintenance means manual compliance, which means eventual non-compliance.&lt;/p&gt;
&lt;h3&gt;
  
  
  Retention Policies
&lt;/h3&gt;

&lt;p&gt;Different data classifications require different retention periods. Financial data under SOX may need seven years of queryable history. Operational data may need 90 days. PII-containing tables may need the shortest retention possible to limit exposure surface.&lt;/p&gt;

&lt;p&gt;Snapshot retention governance should enforce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimum retention periods per classification level&lt;/li&gt;
&lt;li&gt;Maximum retention periods for PII-containing tables&lt;/li&gt;
&lt;li&gt;Approval workflows for expiring snapshots on compliance-sensitive tables&lt;/li&gt;
&lt;li&gt;Audit logging recording which snapshots were expired, by whom, when&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Cross-Engine Audit Trails
&lt;/h3&gt;

&lt;p&gt;With engine-level governance, audit logs are scattered across Spark logs, Trino logs, Flink logs, and engine-specific monitoring systems. Compliance auditors do not want to correlate six different logging systems to reconstruct who accessed what. They want a single, authoritative record.&lt;/p&gt;

&lt;p&gt;Catalog-level governance provides this: every access request — regardless of which engine initiated it — is logged in one place. But access audit is only half the story. Maintenance operations — compaction, snapshot expiration, orphan cleanup, manifest rewriting — are also governance-relevant actions. A compaction job rewrites data files. A snapshot expiration permanently removes time-travel capability. An orphan cleanup deletes files from storage.&lt;/p&gt;

&lt;p&gt;Most governance systems handle read and write access well. Few handle maintenance operations at all. The result is a gap: the governance system controls who can query and who can ingest, but maintenance operations run outside the governance model, often as overprivileged service accounts with blanket access. &lt;/p&gt;

&lt;p&gt;For a deep dive into how the three-layer model addresses this, see the &lt;a href="https://lakeops.dev/blog/iceberg-lakehouse-governance-polaris" rel="noopener noreferrer"&gt;governance separation of concerns guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Schema Evolution Governance: Valid Is Not Safe
&lt;/h2&gt;

&lt;p&gt;Iceberg's schema evolution is mechanically safe — field IDs ensure that column renames, reorders, and drops never corrupt data. A renamed column still maps to the same field ID. A dropped column simply disappears from the current schema while old snapshots retain it. No data files are rewritten.&lt;/p&gt;

&lt;p&gt;But mechanically safe is not operationally safe. A column rename from &lt;code&gt;ship_date&lt;/code&gt; to &lt;code&gt;shipped_at&lt;/code&gt; is a metadata-only operation that preserves the field ID — and simultaneously breaks every Spark job, dbt model, Trino query, and BI dashboard that references the old name. The catalog does not know what consumers have open query plans. The Iceberg spec has no built-in mechanism to enumerate downstream impact before a commit lands.&lt;/p&gt;

&lt;p&gt;Schema governance requires treating schema changes as releases, not commits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Diff&lt;/strong&gt; — generate a field-ID-aware diff (not a string diff) that distinguishes renames from drop-then-add&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classify&lt;/strong&gt; — label each change as non-breaking (column add with default), potentially breaking (rename), or definitely breaking (type narrowing, drop of a non-nullable column)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enumerate impact&lt;/strong&gt; — check against query history, lineage metadata, dbt manifests, and BI connections to identify affected consumers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate&lt;/strong&gt; — require approval for potentially breaking and definitely breaking changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage rollback&lt;/strong&gt; — pin a snapshot tag before the change lands so rollback has a named target, not a guessed snapshot ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; — run smoke tests against the new schema before promoting to production&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Partition evolution carries the same risk profile. A partition-spec change that requires &lt;code&gt;rewrite_data_files&lt;/code&gt; should be flagged before publish, not discovered from the next compute bill.&lt;/p&gt;

&lt;p&gt;Neither Polaris nor any current catalog provides this release-gate workflow natively. Teams either build custom CI checks against the catalog API, use emerging tools like &lt;a href="https://kastordata.io/" rel="noopener noreferrer"&gt;Kastor&lt;/a&gt; for change management, or accept the risk of deploying schema changes without impact analysis.&lt;/p&gt;
&lt;h2&gt;
  
  
  Operational Governance: The Layer Most Teams Miss
&lt;/h2&gt;

&lt;p&gt;Here is the gap that trips up most teams designing Iceberg governance: access governance tells you who can read and write data. It says nothing about who can maintain it — and maintenance operations have direct governance implications.&lt;/p&gt;
&lt;h3&gt;
  
  
  Who Can Compact?
&lt;/h3&gt;

&lt;p&gt;Compaction reads existing data files, merges them, and writes new ones. This requires both read and write access. A malicious or buggy compaction job could introduce data corruption, alter sort order, or fail mid-operation leaving the table degraded. Restricting compaction to a trusted maintenance service — and auditing every compaction operation — is a governance requirement.&lt;/p&gt;
&lt;h3&gt;
  
  
  Who Can Expire Snapshots?
&lt;/h3&gt;

&lt;p&gt;Snapshot expiration permanently removes time-travel capability. In regulated environments, prematurely expiring snapshots on a financial table can violate retention requirements. Governance policies must enforce minimum retention periods per classification, and the expiration service must be auditable.&lt;/p&gt;
&lt;h3&gt;
  
  
  Who Can Delete Orphan Files?
&lt;/h3&gt;

&lt;p&gt;Aggressive orphan cleanup can delete files still needed by in-progress operations. Orphan cleanup requires a grace period, validation against active snapshots, and restriction to service accounts that understand the table's operational state.&lt;/p&gt;
&lt;h3&gt;
  
  
  Closing the Gap with a Dedicated Control Plane
&lt;/h3&gt;

&lt;p&gt;Instead of granting maintenance privileges to multiple engines and hoping they coordinate, a dedicated operational control plane runs all maintenance through a single authority that respects table-level policies, avoids conflicts with active writers, sequences operations correctly, and logs every action.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; fills this role as a control plane for Apache Iceberg lakehouses. Its governance capabilities are specifically designed for operational governance:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Declarative maintenance policies&lt;/strong&gt; define compaction thresholds, snapshot retention windows, orphan cleanup schedules, and manifest rewrite targets at organization, catalog, namespace, or individual table scope. Policies cascade with clear precedence: a table-level override wins over namespace defaults, which override catalog defaults, which override organization baselines. New tables inherit the correct configuration from their namespace automatically.&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%2Fwzzlemy04mt089bs1gsc.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%2Fwzzlemy04mt089bs1gsc.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration policies&lt;/strong&gt; enforce consistent Iceberg table settings — format version, file format, write distribution mode, commit retry behavior — across all tables in scope. Without these, table configurations drift as different teams create tables with different defaults, resulting in inconsistent behavior across the 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8f5e5hv0ze3ze4vnl0e.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%2Fn8f5e5hv0ze3ze4vnl0e.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lake-wide event audit trail&lt;/strong&gt; logs every maintenance operation across all catalogs and namespaces — what ran, when, duration, files before/after, bytes reclaimed, and the signal that triggered it. The same feed powers compliance evidence for SOC 2 and GDPR: proof that retention policies are enforced continuously, not just configured.&lt;/p&gt;

&lt;p&gt;Learn more:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/solutions/agentic-ai" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Fsolutions%2Fagentic-ai%2Fopengraph-image%3F3c1c0c109477c485" height="630" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/solutions/agentic-ai" rel="noopener noreferrer" class="c-link"&gt;
            Agentic AI for Iceberg Lakehouse | LakeOps
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            AI agents query your Iceberg data lakehouse via MCP with read-only, cost-cap, and PII guardrails. Engine routing keeps the lake optimized for AI workloads.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Policy versioning&lt;/strong&gt; tracks every policy change — who modified what, when, and what the previous settings were. The combination of operation logs and policy versioning creates an auditable governance record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI agent guardrails&lt;/strong&gt; address an emerging governance surface: AI agents querying the lakehouse via MCP, Postgres wire protocol, or REST API. LakeOps routes agent queries through layered guardrails — &lt;code&gt;ReadOnly&lt;/code&gt; blocks DDL/DML from agent sessions, &lt;code&gt;CostEstimate&lt;/code&gt; rejects queries exceeding scan thresholds, &lt;code&gt;PIIMask&lt;/code&gt; hashes sensitive columns before results reach the model, and &lt;code&gt;HumanApproval&lt;/code&gt; pauses high-stakes operations for review. Agent query telemetry feeds back into compaction and sort-order decisions, closing the loop between agent access patterns and table optimization.&lt;/p&gt;

&lt;p&gt;Learn more:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/blog/ai-agents-safe-access-iceberg" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fapi.getpronto.io%2Fv1%2Ffile%2Fuser-cmosmjqho103245978n6h8tqsbi%2Fai-agents-safe-access-apache-iceberg-1789482295235-f1053a0495c92a10.png" height="450" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/blog/ai-agents-safe-access-iceberg" rel="noopener noreferrer" class="c-link"&gt;
            Safe AI Agent Access to Apache Iceberg in Production - LakeOps Blog
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            How to give AI agents safe access to Apache Iceberg data — guardrails for ReadOnly, CostEstimate, PIIMask, and HumanApproval with governance and routing.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;For the full operational model, see the &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;LakeOps platform page&lt;/a&gt;. For a deep dive into how operational governance intersects with access governance in the three-layer model, see the &lt;a href="https://lakeops.dev/blog/data-lake-lakehouse-governance" rel="noopener noreferrer"&gt;data lake governance guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Governance Stack: A Practical Path
&lt;/h2&gt;

&lt;p&gt;For teams designing governance for a production Iceberg lakehouse, the three-layer model suggests a clear implementation path:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Choose a Governance-Capable Catalog
&lt;/h3&gt;

&lt;p&gt;The catalog determines your governance ceiling. If it does not support fine-grained access control, credential vending, and policy engine integration, no amount of policy sophistication upstream will help.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Catalog&lt;/th&gt;
&lt;th&gt;RBAC&lt;/th&gt;
&lt;th&gt;Credential Vending&lt;/th&gt;
&lt;th&gt;Policy Engine Integration&lt;/th&gt;
&lt;th&gt;Read Restrictions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apache Polaris&lt;/td&gt;
&lt;td&gt;Full (namespace, table, column)&lt;/td&gt;
&lt;td&gt;AWS, GCS, Azure&lt;/td&gt;
&lt;td&gt;OPA, Ranger, custom&lt;/td&gt;
&lt;td&gt;Yes (spec support)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project Nessie&lt;/td&gt;
&lt;td&gt;No built-in RBAC&lt;/td&gt;
&lt;td&gt;No (engines bring own creds)&lt;/td&gt;
&lt;td&gt;Pair with Polaris or OPA&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lakekeeper&lt;/td&gt;
&lt;td&gt;RBAC, ReBAC, ABAC (OpenFGA + Cedar)&lt;/td&gt;
&lt;td&gt;AWS, GCS, Azure&lt;/td&gt;
&lt;td&gt;OpenFGA, Cedar, OPA bridge&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apache Gravitino&lt;/td&gt;
&lt;td&gt;Namespace/table grants&lt;/td&gt;
&lt;td&gt;Via federated catalogs&lt;/td&gt;
&lt;td&gt;Custom integration&lt;/td&gt;
&lt;td&gt;In progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Glue&lt;/td&gt;
&lt;td&gt;IAM-based&lt;/td&gt;
&lt;td&gt;IAM role assumption&lt;/td&gt;
&lt;td&gt;IAM policies&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unity Catalog&lt;/td&gt;
&lt;td&gt;Full (row, column, table)&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: Establish Namespace and Classification Strategy
&lt;/h3&gt;

&lt;p&gt;Before writing policies, define the organizational structure. A practical starting point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three sensitivity tiers:&lt;/strong&gt; &lt;code&gt;public&lt;/code&gt;, &lt;code&gt;internal&lt;/code&gt;, &lt;code&gt;restricted&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One namespace per data domain per tier:&lt;/strong&gt; e.g., &lt;code&gt;internal.marketing&lt;/code&gt;, &lt;code&gt;restricted.finance&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Column-level classification&lt;/strong&gt; for PII fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This structure is simple enough to implement in a week and comprehensive enough to cover most governance requirements. It also maps naturally to RBAC grants: data engineers get write access to &lt;code&gt;raw.*&lt;/code&gt;, data scientists read &lt;code&gt;curated.*&lt;/code&gt;, analysts read &lt;code&gt;published.*&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Implement RBAC as the Baseline
&lt;/h3&gt;

&lt;p&gt;Start with role-based access control using the catalog's native privilege model. Define roles mapping to organizational functions, grant namespace-level and table-level privileges, enforce through the catalog. This covers 80% of access control requirements and is auditable, understandable, and manageable without external tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Layer ABAC Through a Policy Engine
&lt;/h3&gt;

&lt;p&gt;Once RBAC is in place, add attribute-based policies for requirements roles cannot express — classification-based access, column masking, time restrictions, purpose limitation. Deploy OPA, Cedar, or Ranger depending on your catalog choice (OPA/Ranger for Polaris, Cedar or OpenFGA for Lakekeeper), integrate with the catalog, and version policies in Git alongside application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Automate Maintenance Governance
&lt;/h3&gt;

&lt;p&gt;Governance without operational health is incomplete. A governed table with millions of small files, thousands of accumulated snapshots, and orphaned data consuming storage passes every access control check but fails every performance expectation. And a table without automated maintenance cannot meet GDPR erasure deadlines — compaction, snapshot expiration, and orphan cleanup must run continuously, not manually.&lt;/p&gt;

&lt;p&gt;Close the loop by connecting the governance catalog (who can access) with an operational control plane (ensuring the data is healthy, fast, and compliant). The governance catalog enforces access policies. &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; ensures the underlying tables are structurally healthy, compacted, and query-ready — with full audit trails that satisfy the same compliance requirements as your access governance. For tables holding personal data, the same maintenance automation that keeps query performance healthy also drives the three-step erasure pipeline to completion within regulatory deadlines. For a complete production readiness checklist that includes governance alongside maintenance, see the &lt;a href="https://lakeops.dev/blog/iceberg-production-readiness-checklist" rel="noopener noreferrer"&gt;Iceberg production readiness checklist&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;The open lakehouse disaggregated storage, compute, and metadata into independent layers. Governance must follow the same decomposition — rules separate from enforcement, enforcement separate from the format, operational governance separate from access governance.&lt;/p&gt;

&lt;p&gt;The tooling exists. Apache Polaris provides catalog-level RBAC, credential vending, and policy engine integration. Lakekeeper adds Cedar and OpenFGA for policy-as-code and relationship-based access control. Read Restrictions bring interoperable column masking and row filtering into the REST Catalog spec. OPA and Ranger provide external policy engines with full audit trails. &lt;/p&gt;

&lt;p&gt;LakeOps provides the operational governance layer — declarative maintenance policies, AI agent guardrails, and lake-wide audit trails that ensure governed data is also structurally healthy.&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%2Fwrtfup34guc22xqwq603.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%2Fwrtfup34guc22xqwq603.png" alt=" " width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start with the catalog choice — it determines your governance ceiling. Layer policies incrementally: RBAC first, ABAC when needed. Treat schema changes as releases, not commits. Build an erasure pipeline that accounts for every replication path — or invest in cryptographic shredding for tables where full compaction cannot meet deadlines. And close the operational loop from day one. The teams that treat access governance, schema governance, and operational governance as three facets of the same architecture — each with its own tooling and audit trail — are the ones running multi-engine lakehouses at scale without governance gaps or compliance drift.&lt;/p&gt;

</description>
      <category>iceberg</category>
      <category>dataengineering</category>
      <category>governance</category>
      <category>lakehouse</category>
    </item>
    <item>
      <title>The Real Cost of Apache Iceberg Table Maintenance</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Tue, 22 Sep 2026 09:15:42 +0000</pubDate>
      <link>https://dev.to/jonisar/the-real-cost-of-apache-iceberg-table-maintenance-3kg</link>
      <guid>https://dev.to/jonisar/the-real-cost-of-apache-iceberg-table-maintenance-3kg</guid>
      <description>&lt;p&gt;If you operate Apache Iceberg tables in production, you already know maintenance is not optional. What most teams underestimate is how much it actually costs — and how much of that spend is avoidable.&lt;/p&gt;

&lt;p&gt;Iceberg gives you the primitives: &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;. It does not give you an operational layer that runs them efficiently, in the right order, at the right frequency, on the tables that actually need work. That gap between primitives and practice is where maintenance costs compound silently — across storage, compute, and engineering time.&lt;/p&gt;

&lt;p&gt;This article breaks down exactly where Iceberg maintenance dollars go, how each cost category interacts with the others, and what you can do to cut total maintenance spend without sacrificing table health.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four cost categories that compound
&lt;/h2&gt;

&lt;p&gt;Iceberg maintenance costs are not one line item. They come from four distinct sources that interact with and amplify each other. Fixing one in isolation delivers partial results.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/8qj5VkSjFac" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Storage waste
&lt;/h3&gt;

&lt;p&gt;Every Iceberg snapshot persists until you explicitly expire it. Each snapshot pins references to data files from that point in time, preventing garbage collection of superseded bytes. Failed writes and aborted Spark jobs leave orphan files — storage objects not referenced in any metadata — that accumulate indefinitely.&lt;/p&gt;

&lt;p&gt;The cost math is straightforward. S3 Standard storage costs $0.023/GB/month ($23.55/TB/month) in us-east-1. On mature production lakes with streaming ingestion, unreferenced data from retained snapshots and orphan files routinely represents 25–40% of billable object-storage spend. A table with 30-day snapshot retention and 10-minute commit intervals accumulates over 4,000 snapshots per month. Each holds references to data files the current table state no longer needs.&lt;/p&gt;

&lt;p&gt;The numbers from real deployments are concrete. One production scan found approximately 200 TB of orphan data across 324 tables — roughly 1.8 million unreferenced files from aborted transactions, failed Spark jobs, and stale tables. At S3 Standard rates, that was ~$4,700/month in storage for data no query would ever read. Another team found 120 TB removable from expired snapshots alone — approximately $34,000/year in pure waste. Amazon Ads &lt;a href="https://aws.amazon.com/blogs/storage/how-amazon-ads-uses-iceberg-optimizations-to-accelerate-their-spark-workload-on-amazon-s3/" rel="noopener noreferrer"&gt;reported&lt;/a&gt; a 74% reduction in orphan data and 32% lower storage costs after optimizing their Iceberg file layout — confirming that orphan accumulation is a systemic problem, not an edge case.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Query compute waste
&lt;/h3&gt;

&lt;p&gt;This is the hidden maintenance cost most teams miss entirely. Unmaintained tables are expensive to &lt;em&gt;query&lt;/em&gt;, not just to store.&lt;/p&gt;

&lt;p&gt;Every Parquet file requires at least one S3 GET request to read its footer. S3 GET requests cost $0.0004 per 1,000 — seemingly cheap until you multiply by file count and query frequency. A table with 47,000 small files forces 47,000 metadata round trips before reading a single row. After compacting that table to 280 files, query time dropped from 52 seconds to 5.8 seconds — a 9× improvement that translates directly to 9× less CPU time per execution.&lt;/p&gt;

&lt;p&gt;Sort order has an equally large impact. When data is physically sorted by the columns queries filter on, engines use min/max statistics in Parquet row group metadata to skip irrelevant data without reading it. On scan-priced engines like Athena ($5/TB scanned), the difference is stark: a well-sorted 1 TB table queried with a selective filter might cost $0.25 per query. The same query on an unsorted, fragmented copy costs $5.00 — a 20× difference. For a daily reporting pipeline scanning 5 TB across 20 tables, that is $25/run on Athena ($750/month), versus $3–5/run on a right-sized Trino cluster ($90–150/month).&lt;/p&gt;

&lt;p&gt;Delete files from merge-on-read operations add another layer. Every query must reconcile data files against position delete files at read time. One production table had 23,433 delete files covering 551 million rows of deleted data — every query paid the full cost of reading and filtering that entire history.&lt;/p&gt;

&lt;p&gt;The takeaway: the cost of &lt;em&gt;not&lt;/em&gt; maintaining your tables shows up as inflated query compute bills, not as a maintenance line item. It is the largest single cost most teams fail to attribute to table health.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Compaction compute
&lt;/h3&gt;

&lt;p&gt;Maintenance itself consumes compute. The dominant pattern — Spark-based compaction on scheduled Airflow DAGs — works, but costs significantly more than it needs to.&lt;/p&gt;

&lt;p&gt;Compaction is a narrow, I/O-bound read-merge-write operation. Running it on Spark means paying for JVM startup, garbage collection overhead, executor provisioning, and idle cluster time between runs. EMR Serverless charges $0.052624/vCPU-hour — which adds up fast when compaction runs are over-provisioned (as they usually are, because under-provisioning causes OOM failures, and failures mean 2 AM pages).&lt;/p&gt;

&lt;p&gt;The benchmark comparison on a 200 GB table (~1 TB uncompressed): Spark-based binpack takes 1,612 seconds at ~$1.54 in compute. AWS S3 Tables' built-in compaction takes 6,300 seconds. A purpose-built Rust engine on Apache DataFusion completes the same job in 221 seconds at ~$0.21 — roughly one-tenth the compute cost. Independent benchmarks from &lt;a href="https://docs.risingwave.com/iceberg/compaction-benchmark" rel="noopener noreferrer"&gt;RisingWave&lt;/a&gt; confirm the magnitude: their Rust/DataFusion engine completed the same workload 5.5× faster at 82% lower cost, and succeeded on delete-heavy workloads where Spark failed with OOM errors.&lt;/p&gt;

&lt;p&gt;Directional monthly cost comparison for a mid-size estate (~80 TB, 200+ tables, daily compaction):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Est. monthly compaction cost&lt;/th&gt;
&lt;th&gt;Operational overhead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spark on EMR&lt;/td&gt;
&lt;td&gt;~$15,000&lt;/td&gt;
&lt;td&gt;High (clusters, DAGs, on-call)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark on Glue&lt;/td&gt;
&lt;td&gt;~$12,000&lt;/td&gt;
&lt;td&gt;Medium (job configs, scheduling)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Athena OPTIMIZE&lt;/td&gt;
&lt;td&gt;~$8,000&lt;/td&gt;
&lt;td&gt;Low (SQL only, no sort support)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose-built Rust engine&lt;/td&gt;
&lt;td&gt;~$1,500&lt;/td&gt;
&lt;td&gt;Minimal (policies, autonomous)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Estimates based on $50/TB compaction cost for Spark and $5/TB for purpose-built engines, with daily runs across 200+ tables. Actual costs vary by table size, compaction frequency, and instance pricing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The gap is structural, not incremental. JVM-based engines carry startup, GC, and idle-cluster overhead that compounds across hundreds of daily compaction runs. Native engines built for the specific workload of reading, sorting, and writing Parquet files operate at a fundamentally different cost point.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Engineering time
&lt;/h3&gt;

&lt;p&gt;Someone built those Airflow DAGs. Someone maintains them, monitors them, debugs them when compaction conflicts with a streaming writer at 2 AM, and investigates when snapshot expiration fails silently.&lt;/p&gt;

&lt;p&gt;At 50 tables, this is manageable side work — as Alex Merced's &lt;a href="https://iceberglakehouse.com/posts/table-maintenance-economics/" rel="noopener noreferrer"&gt;table maintenance economics analysis&lt;/a&gt; notes, a small estate (40 tables, 3 TB) needs one DAG and a week of initial setup. At 500 tables, maintenance scripts become a dedicated role. At medium scale (600 tables, 80 TB), the work concentrates on the ~30 streaming tables that generate most maintenance needs, but table discovery, per-table policy, and failure handling stop being a side project. Merced estimates the ongoing cost at roughly a quarter of an engineer indefinitely, rising with table count.&lt;/p&gt;

&lt;p&gt;Fully loaded — salary, on-call burden, opportunity cost — this vector frequently exceeds infrastructure spend on lakes with 300+ tables. It never appears on a cloud bill, which is exactly why it is routinely underestimated.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  How the costs interact
&lt;/h2&gt;

&lt;p&gt;These four categories do not operate independently. Each amplifies the others:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small files inflate both query and compaction costs.&lt;/strong&gt; Every query pays per-file overhead. Compaction has more file handles to manage and more footers to read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipped snapshot expiration inflates storage &lt;em&gt;and&lt;/em&gt; compaction costs.&lt;/strong&gt; Compacting before expiring snapshots rewrites data files that are about to become unreferenced anyway — wasted work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphan cleanup before expiration is a no-op.&lt;/strong&gt; The files are still referenced by unexpired snapshots. Running operations out of order wastes compute and leaves storage unreclaimable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifest fragmentation and small files compound.&lt;/strong&gt; Tables with many small files tend to have many manifests. Every query planner reads every manifest before deciding which files to scan. At 200+ manifests, planning overhead often exceeds the actual scan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implication: sequencing matters as much as execution. Running each operation independently on a fixed schedule yields partial results. Running them as a coordinated loop — in the order Iceberg's architecture requires — is where the full cost reduction materializes.&lt;/p&gt;

&lt;p&gt;Learn more:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/blog/iceberg-lakehouse-optimization-with-lakeops" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fapi.getpronto.io%2Fv1%2Ffile%2Fuser-cmosmjqho103245978n6h8tqsbi%2Flakehouse-control-plane-iceberg-1778757922646-e4057689a9f7ebbe.png" height="450" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/blog/iceberg-lakehouse-optimization-with-lakeops" rel="noopener noreferrer" class="c-link"&gt;
            Iceberg Lakehouse Optimization with LakeOps - LakeOps Blog
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Optimize your Apache Iceberg lakehouse end to end — compaction, snapshots, manifests, routing, and governance. Production results: 80% cost reduction.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/blog/iceberg-cost-optimization-2026" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Fimages%2Fblog-results-cover.png" height="442" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/blog/iceberg-cost-optimization-2026" rel="noopener noreferrer" class="c-link"&gt;
            Apache Iceberg Cost Optimization in 2026 - LakeOps Blog
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Apache Iceberg cost breakdown — small files, snapshot bloat, orphans, and compaction overhead. How autonomous management cuts lakehouse costs up to 80%.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/blog/state-of-iceberg-finops-cost-reduction" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fapi.getpronto.io%2Fv1%2Ffile%2Fuser-cmosmjqho103245978n6h8tqsbi%2Fcloud-bill-iceberg-lakehouse-1779013311998-c3cd5cf690db5779.png" height="1000" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/blog/state-of-iceberg-finops-cost-reduction" rel="noopener noreferrer" class="c-link"&gt;
            State of Iceberg FinOps and Cost Reduction in 2026 - LakeOps Blog
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            State of Iceberg FinOps and Cost Reduction in 2026 — lakehouse spend leaks, chargeback metrics, maintenance economics, and tools from LakeOps to AWS Glue.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/blog/reduce-aws-s3-cost-iceberg" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fapi.getpronto.io%2Fv1%2Ffile%2Fuser-cmosmjqho103245978n6h8tqsbi%2Freduce-s3-aws-cost-iceberg-1779705428329-6edb34451f25d208.png" height="450" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/blog/reduce-aws-s3-cost-iceberg" rel="noopener noreferrer" class="c-link"&gt;
            Reducing AWS S3 Cost with Iceberg: A Guide - LakeOps Blog
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Reduce AWS S3 costs for Iceberg — compaction, snapshot expiration, orphan cleanup, layout optimization, and storage tiering with production numbers.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;The cost-optimal order is not arbitrary. It follows from Iceberg's metadata dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Expire snapshots    → releases references to superseded data files
2. Remove orphan files → deletes unreferenced storage (now including newly dereferenced files)
3. Compact data files  → merges remaining small files into optimal sizes
4. Rewrite manifests   → consolidates metadata against the new layout
5. Refresh statistics  → generates Puffin files for aggressive engine-level pruning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this out of order inflates every cost category. You pay compute to rewrite files that expiration would have freed. You miss reclaimable storage because orphan cleanup runs before expiration dereferences anything. You burn metadata I/O rebuilding manifests that compaction is about to invalidate.&lt;/p&gt;

&lt;p&gt;Most Spark-based maintenance setups run each operation as an independent cron job with no awareness of the others. The operations execute on their own schedules, in whatever order the DAG happens to trigger them. This is where a control plane like &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; becomes relevant — it sequences maintenance operations in dependency order per table, triggered by actual table health signals rather than fixed schedules, so cleanup always precedes compaction and metadata optimization always follows it.&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%2Fjzk7jmk45id4689c0v22.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%2Fjzk7jmk45id4689c0v22.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a real data scnapshot of an enterprise lake with 7k tables runing LakeOps:&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%2Fnpspoqyzhh86fm3oec1z.jpeg" 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%2Fnpspoqyzhh86fm3oec1z.jpeg" alt=" " width="797" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring your actual maintenance cost
&lt;/h2&gt;

&lt;p&gt;Before optimizing, measure. Most teams cannot answer "what does Iceberg maintenance cost us?" because the spend is fragmented across cloud line items.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage audit
&lt;/h3&gt;

&lt;p&gt;Compare logical table size (&lt;code&gt;total-data-files-size-in-bytes&lt;/code&gt; from Iceberg metadata) to actual billable storage for each table prefix. A gap exceeding 20% typically indicates orphans, unexpired snapshot references, or incomplete compaction rewrites. Multiply the gap in TB by $23.55 (S3 Standard monthly rate per TB) for a rough dollar estimate.&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;-- Check snapshot count and age per table (via Spark)&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_snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;max&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;newest_snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;datediff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;current_date&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_snapshot_age_days&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;my_table&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Estimate orphan file waste: dry-run before deleting (via Spark)&lt;/span&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.my_table'&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;Tables with unbounded snapshot growth — streaming tables can accumulate 4,000+ snapshots per month — are your highest-priority storage cost targets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compute audit
&lt;/h3&gt;

&lt;p&gt;Track two metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance compute as percentage of query compute.&lt;/strong&gt; If your compaction clusters cost more than 30% of your query compute, the maintenance strategy needs a faster engine or signal-driven triggers. Alex Merced's &lt;a href="https://iceberglakehouse.com/posts/table-maintenance-economics/" rel="noopener noreferrer"&gt;table maintenance economics analysis&lt;/a&gt; puts the healthy range at 10–25% of query compute for write-heavy tables. Teams that have never measured this are usually surprised: either it is zero (no maintenance running, query costs inflated) or above 40% (naive schedule, wasted runs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per TB compacted.&lt;/strong&gt; This is the unit-economics metric. Spark-based compaction typically runs ~$50/TB on EMR. Purpose-built Rust engines run ~$5/TB. Compare your actual cost across engines on identical tables — synthetic benchmarks understate the JVM overhead at production scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Query waste audit
&lt;/h3&gt;

&lt;p&gt;The most underrated measurement: how much query compute is wasted on poorly maintained 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="c1"&gt;-- Check file count and average size per partition (via Spark)&lt;/span&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="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="p"&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avg_file_size_mb&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="p"&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="p"&gt;)&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="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;my_table&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;HAVING&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;OR&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;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;33554432&lt;/span&gt;  &lt;span class="c1"&gt;-- &amp;lt; 32 MB&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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check delete file overhead (via Spark)&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="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;record_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_delete_records&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;my_table&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tables where &amp;gt;30% of files fall below 32 MB are costing you measurably more on every query. On Athena, you can calculate the exact dollar waste: &lt;code&gt;(bytes scanned on fragmented table - bytes scanned on compacted equivalent) × $5/TB × daily query count&lt;/code&gt;. Tables with delete-file-to-data-file ratios above 1:10 are candidates for immediate compaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing compaction compute cost
&lt;/h2&gt;

&lt;p&gt;Compaction is typically the largest controllable maintenance cost. Three strategies reduce it:&lt;/p&gt;

&lt;h3&gt;
  
  
  Replace the engine
&lt;/h3&gt;

&lt;p&gt;The structural mismatch between Spark and compaction is the single biggest cost lever. Compaction is a narrow I/O operation. Spark is a general-purpose distributed computation framework with JVM overhead, GC pauses, and executor provisioning designed for arbitrary DAG workloads.&lt;/p&gt;

&lt;p&gt;Purpose-built compaction engines built on Rust and Apache DataFusion eliminate that overhead structurally. LakeOps's &lt;a href="https://lakeops.dev/blog/replace-spark-iceberg-compaction" rel="noopener noreferrer"&gt;Rust compaction engine&lt;/a&gt; processes the same 200 GB benchmark in 221 seconds versus 1,612 seconds for Spark — 7.3× faster, at roughly one-tenth the compute cost ($0.21 vs. $1.54). A 1.2 TB table that caused Spark to OOM completed in 11 minutes on the same hardware.&lt;/p&gt;

&lt;p&gt;The cost gap compounds: Spark costs scale linearly with data volume at ~$50/TB. Self-improving engines that skip tables not needing work grow sublinearly — one team cut their per-table compaction cost from $8,400/year to $750.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compact only what needs work
&lt;/h3&gt;

&lt;p&gt;Fixed-schedule compaction (every 4 hours, every night) wastes compute on tables that have not degraded since the last run. Signal-driven triggers — firing when file count, average file size, or delete-file ratio crosses a threshold — skip healthy tables entirely.&lt;/p&gt;

&lt;p&gt;A lake with 600 tables where 30 receive continuous streaming writes generates almost all maintenance work from those 30 tables. The other 570 need snapshot expiration and little else. Cron-based compaction treats all 600 equally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sort during compaction
&lt;/h3&gt;

&lt;p&gt;Sort compaction costs more than binpack per run, but the downstream savings dwarf the upfront investment. Sorted data enables min/max pruning on every subsequent query — unsorted tables scan roughly 50% more data than sorted equivalents in production benchmarks. The ROI calculation: sort compaction cost is paid once per sort-order cycle; the scan reduction applies to every query until the next compaction.&lt;/p&gt;

&lt;p&gt;The challenge is choosing the right sort key. The wrong choice accelerates one query pattern while degrading another. &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; addresses this by validating proposed sort orders against actual query patterns before committing to a full rewrite — ensuring the compute investment produces a net-positive return on query performance.&lt;/p&gt;

&lt;p&gt;For a detailed comparison of compaction strategies and tooling options, the LakeOps blog has a practical guide on &lt;a href="https://lakeops.dev/blog/iceberg-compaction-strategies" rel="noopener noreferrer"&gt;Iceberg compaction strategies&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing storage waste cost
&lt;/h2&gt;

&lt;p&gt;Storage cost reduction is the highest-ROI starting point because it requires no sort-order decisions or parameter tuning. You scan, identify waste, and remove it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snapshot retention policies
&lt;/h3&gt;

&lt;p&gt;Most tables do not need 30 days of snapshots. Define retention based on actual requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time-travel window:&lt;/strong&gt; How far back do queries actually look? Usually 7–14 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback safety window:&lt;/strong&gt; How long before you would detect a bad write? Usually 24–72 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ML reproducibility:&lt;/strong&gt; Use named tags for specific training snapshots instead of retaining all history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A streaming table producing 288 snapshots/day with 30-day retention holds 8,640 snapshots. Each pins references to data files. Reducing retention to 7 days and a minimum of 5 retained snapshots cuts snapshot count to ~2,000 — and releases the storage those 6,600 expired snapshots were preventing from being garbage-collected. On a table where expired snapshots pin 10 TB of superseded data, that is ~$235/month in recoverable S3 Standard storage.&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;-- Expire snapshots older than 7 days, keeping at least 5&lt;/span&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.my_table'&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-09-09 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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Orphan file cleanup
&lt;/h3&gt;

&lt;p&gt;Orphan files accumulate from failed writes, aborted compaction, and dropped tables. They are invisible to query engines but billable on your storage invoice. OOMed compaction jobs are the most prolific source — a run writing 500 new files that fails before the atomic commit leaves 500 orphans per failure.&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;-- First: dry-run to see what would be removed&lt;/span&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.my_table'&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-09-09 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;span class="c1"&gt;-- Then: remove orphan files older than 7 days&lt;/span&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.my_table'&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-09-09 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;Always run orphan cleanup &lt;em&gt;after&lt;/em&gt; snapshot expiration. Expiration dereferences files that then become orphans — if cleanup runs first, it misses them. The &lt;code&gt;older_than&lt;/code&gt; threshold of 3–7 days is critical: it protects files from in-flight writes that have not yet committed.&lt;/p&gt;

&lt;p&gt;At lake scale, the impact is significant. One cleanup pass across 324 tables removed approximately 200 TB of orphan data — ~$56,000/year in S3 Standard storage savings from a single operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metadata bloat
&lt;/h3&gt;

&lt;p&gt;Iceberg's &lt;code&gt;metadata.json&lt;/code&gt; stores all historical schemas and partition specs. Tables with frequent schema evolution accumulate hundreds of schema versions — a table with thousands of columns that has undergone 200+ schema changes stores every complete schema version in full. Production reports show &lt;code&gt;metadata.json&lt;/code&gt; files reaching 10 MB compressed (250 MB uncompressed), consuming approximately 4 GB of heap on load in engines like Trino. Each snapshot entry also adds 200–500 bytes; at 288 commits/day, that is 56–140 KB/day of snapshot metadata alone.&lt;/p&gt;

&lt;p&gt;Mitigations: set &lt;code&gt;write.metadata.delete-after-commit.enabled&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; and &lt;code&gt;write.metadata.previous-versions-max&lt;/code&gt; to a reasonable limit (e.g., 10–20). Keep snapshot expiration aggressive to reduce the snapshot list in &lt;code&gt;metadata.json&lt;/code&gt;. For a deeper analysis, see the LakeOps blog on &lt;a href="https://lakeops.dev/blog/iceberg-metadata-lifecycle-maintenance-optimization" rel="noopener noreferrer"&gt;Iceberg metadata lifecycle maintenance&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing query compute through better table health
&lt;/h2&gt;

&lt;p&gt;The most impactful long-term cost reduction comes not from cheaper maintenance, but from maintaining tables well enough that every query costs less.&lt;/p&gt;

&lt;p&gt;Three levers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File sizing.&lt;/strong&gt; Compact small files to 128–512 MB targets. This alone reduces per-file API call overhead by orders of magnitude. A table going from 47,000 files to 280 files saves ~46,700 S3 GET requests per query — and at $0.0004 per 1,000 GETs, that is ~$0.019 saved per query just in API calls, before accounting for the far larger CPU and scan savings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sort order.&lt;/strong&gt; Physical sort by high-selectivity filter columns enables row-group skipping. Engines use min/max statistics in Parquet row group metadata to skip irrelevant file groups entirely. Sorted data also compresses better (163 GB vs. 178 GB for 1 TB TPC-H — a 9% reduction), lowering storage costs as a side effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete file cleanup.&lt;/strong&gt; Physically apply position deletes through compaction so queries stop paying reconciliation cost at read time. Tables with delete-file-to-data-file ratios above 1:10 are candidates for immediate compaction.&lt;/p&gt;

&lt;p&gt;The compound effect: a table that is compacted to the right file size, sorted by the right columns, with delete files cleaned up and manifests consolidated, delivers dramatically faster queries than the same table in a degraded state. On Athena at $5/TB scanned, the cost difference is direct and measurable.&lt;/p&gt;

&lt;h2&gt;
  
  
  From scripts to policies: cutting engineering time
&lt;/h2&gt;

&lt;p&gt;The engineering time cost of Iceberg maintenance scales linearly with table count when you manage it through scripts. Every table needs a compaction DAG, an expiration schedule, an orphan cleanup job, monitoring, alerting, and on-call coverage.&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%2F7szd1dkg23o5dk3ox3fn.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%2F7szd1dkg23o5dk3ox3fn.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The alternative is policy-based maintenance: define rules at the catalog, namespace, or table level and let the system enforce them.&lt;/p&gt;

&lt;p&gt;A practical policy set for a mid-size estate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All tables:&lt;/strong&gt; Expire snapshots daily (7-day window, minimum 5 retained). Remove orphan files weekly (7-day &lt;code&gt;older_than&lt;/code&gt; threshold).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming tables (&amp;gt;10 commits/hour):&lt;/strong&gt; Compact when average file size drops below 64 MB or small-file ratio exceeds 30%. Rewrite manifests after every compaction cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch tables:&lt;/strong&gt; Compact when file count exceeds 500 per partition. Rewrite manifests monthly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDC tables:&lt;/strong&gt; Compact when delete-file count exceeds threshold. Priority: position-delete rewrite before data-file compaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;LakeOps platform&lt;/a&gt; implements this as declarative policies scoped by catalog, namespace, or individual table — with a specificity hierarchy where table-level overrides take precedence. New tables inherit policies automatically. Every execution is logged with duration, impact, and status for governance and chargeback.&lt;/p&gt;

&lt;p&gt;The engineering time savings at scale are substantial. Instead of maintaining hundreds of Airflow DAGs and Spark scripts, you define policies once and the system handles sequencing, scheduling, conflict avoidance, and adaptation. The on-call burden drops from "compaction failed on table X at 2 AM" to reviewing an event log.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a control plane changes
&lt;/h2&gt;

&lt;p&gt;The underlying problem is that Iceberg gives you maintenance primitives but not an operational system. Each procedure runs in isolation. Nothing sequences them, nothing triggers them from table health signals, nothing coordinates across tables, and nothing tells you which tables are actually costing you money.&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%2Fql49bmmj0chgil6la9hi.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%2Fql49bmmj0chgil6la9hi.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A lakehouse control plane like &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; connects to your catalogs (Glue, REST/Polaris, Nessie, S3 Tables) and query engines (Trino, Spark, Snowflake, Athena, DuckDB) and targets each cost vector through specific mechanisms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage waste → lake-wide observability and automated cleanup.&lt;/strong&gt; Every table assessed for structural health — file fragmentation, snapshot age, manifest bloat, write velocity — with the most degraded tables surfaced first. Snapshot expiration and orphan cleanup run in dependency order automatically — so the orphan cleanup that reclaimed 200 TB at one production lake does not require a manual audit to trigger.&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%2Fd5dgkgaesx1728ci4iye.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%2Fd5dgkgaesx1728ci4iye.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compaction compute → Rust engine replacing Spark.&lt;/strong&gt; Compaction on a purpose-built Rust/DataFusion engine at ~$5/TB instead of Spark at ~$50/TB. No JVM startup, bounded memory, no OOM failures. The engine skips tables that have not degraded since the last run, so costs grow sublinearly with table count.&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%2Fxg4corlypbl5kyhox9w8.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%2Fxg4corlypbl5kyhox9w8.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query compute → query-aware sort optimization.&lt;/strong&gt; Sort decisions driven by cross-engine query telemetry — which columns your Trino, Spark, and Athena queries actually filter on — not guesswork at table-creation time. Layout changes are tested on Iceberg branches before touching 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%2F32mrgtejj7uqgu2qgifs.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%2F32mrgtejj7uqgu2qgifs.png" alt=" " width="799" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Engineering time → policy-driven automation replacing DAGs.&lt;/strong&gt; Maintenance rules defined at the namespace level replace per-table Airflow configurations. New tables are covered from the moment they appear in the catalog. Every execution is logged with duration, impact, and status.&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%2Fj3hho72trh5157ev7jy3.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%2Fj3hho72trh5157ev7jy3.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-engine savings → multi-engine routing.&lt;/strong&gt; Each query routed to the cheapest engine meeting its latency target. A nightly batch pipeline scanning 5 TB drops from $25/run on Athena to $3–5/run on Trino — without changing application code.&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%2Fsszmtn612k9jlf3i380w.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%2Fsszmtn612k9jlf3i380w.png" alt=" " width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;LakeOps platform&lt;/a&gt; integrates with your existing infrastructure through standard catalog and engine APIs — setup is measured in minutes, not sprints.&lt;/p&gt;

&lt;p&gt;For a comprehensive breakdown of all four cost vectors with production numbers, the &lt;a href="https://lakeops.dev/blog/iceberg-cost-optimization-2026" rel="noopener noreferrer"&gt;Apache Iceberg cost optimization guide&lt;/a&gt; covers each in detail. For a broader view of the tooling landscape — cloud-native optimizers, engine-integrated maintenance, and dedicated control planes — the &lt;a href="https://lakeops.dev/blog/state-of-iceberg-finops-cost-reduction" rel="noopener noreferrer"&gt;State of Iceberg FinOps&lt;/a&gt; survey compares approaches across the ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The highest-ROI sequence
&lt;/h2&gt;

&lt;p&gt;For most production lakes, the highest-ROI sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit storage waste (week 1).&lt;/strong&gt; Compare logical table sizes to billable storage. Run &lt;code&gt;remove_orphan_files&lt;/code&gt; with &lt;code&gt;dry_run =&amp;gt; true&lt;/code&gt; on your top 20 tables by object count. Multiply the gap in TB by $23.55 for your monthly waste estimate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run snapshot expiration and orphan cleanup (week 1–2).&lt;/strong&gt; These release storage at near-zero compute cost. No sort-order decisions required. One pass can reclaim tens of thousands of dollars in annual storage spend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark compaction cost per TB (week 2–3).&lt;/strong&gt; Compare your current engine against alternatives on identical tables. The engine choice is a first-order cost decision — $50/TB vs. $5/TB is a 10× difference that compounds across every table and every run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compact and sort your highest-traffic tables (week 3–4).&lt;/strong&gt; Start with the tables that generate the most latency complaints — they are typically your highest-cost tables from a compute perspective.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace cron with health-driven policies (month 2).&lt;/strong&gt; Move from fixed schedules to triggers based on file count, average file size, and delete-file ratio. This is where engineering time savings materialize.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The total reduction from addressing all four cost categories as a coordinated system — not as isolated optimizations — is where production teams consistently report the largest improvements. Storage waste disappears. Query compute drops because every engine scans less. Maintenance compute drops because the engine is cheaper and skips healthy tables. Engineering time drops because policies replace scripts.&lt;/p&gt;

&lt;p&gt;Iceberg's table format is production-ready. The maintenance layer should be too — and it should cost a fraction of what cron-based Spark jobs spend to keep your lake healthy. &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; is one way to get there.&lt;/p&gt;

&lt;p&gt;Thanks for reading :)&lt;/p&gt;

</description>
      <category>iceberg</category>
      <category>dataengineering</category>
      <category>lakehouse</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Apache Iceberg Maintenance Tools: From Spark Procedures to Autonomous Control Planes</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Thu, 17 Sep 2026 11:34:50 +0000</pubDate>
      <link>https://dev.to/jonisar/apache-iceberg-maintenance-tools-from-spark-procedures-to-autonomous-control-planes-lbi</link>
      <guid>https://dev.to/jonisar/apache-iceberg-maintenance-tools-from-spark-procedures-to-autonomous-control-planes-lbi</guid>
      <description>&lt;p&gt;Every write to an Apache Iceberg table creates a new snapshot. Every snapshot pins references to data files. Over weeks of production workloads, this produces thousands of small files, bloated metadata, orphaned storage artifacts, and dangling delete records — all of which silently degrade query performance, inflate storage costs, and slow down every engine connected to the table.&lt;/p&gt;

&lt;p&gt;The fix requires five distinct maintenance operations, executed in the right order, at the right frequency, across every table in the lake. The tooling landscape for this has matured substantially — from raw engine procedures to standalone Rust and Go binaries to fully autonomous control planes. This guide covers the complete spectrum, with enough technical depth to help you choose the right approach for your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five maintenance operations
&lt;/h2&gt;

&lt;p&gt;Before evaluating tools, you need a clear picture of what maintenance actually involves. Iceberg maintenance is not a single operation — it's a coordinated pipeline of five distinct actions, each solving a different structural problem.&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%2F9qfwpb347kovrvshcuyj.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%2F9qfwpb347kovrvshcuyj.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's an example of such operations using an &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;autonomous and intelligent Lakehouse control plane&lt;/a&gt;:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/blog/managed-iceberg-2026" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Fimages%2Fblog-dashboard-cover.png" height="477" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/blog/managed-iceberg-2026" rel="noopener noreferrer" class="c-link"&gt;
            Managed Iceberg in 2026: Autonomous Data Lake - LakeOps Blog
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            A deep dive into nine components of a modern Iceberg optimization platform — full-stack observability, query-aware compaction, snapshot lifecycle management, manifest optimization, orphan file cleanup, organization-wide policies, multi-engine query routing, agentic AI enablement, and layout simulations — with product walkthroughs showing how LakeOps delivers autonomous table maintenance, up to 28x faster compaction, and up to 80% cost reduction.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h3&gt;
  
  
  1. Snapshot expiration
&lt;/h3&gt;

&lt;p&gt;Every commit — write, update, delete, compaction — creates a new snapshot. Snapshots enable time travel and rollback, but they also prevent data files from being garbage-collected. A streaming table with 5-minute commits accumulates over 8,600 snapshots per month.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;expire_snapshots&lt;/code&gt; removes snapshots older than a retention threshold and deletes the manifest files and manifest lists that were exclusively referenced by those expired snapshots. Data files that are no longer referenced by any retained snapshot become eligible for orphan cleanup (they are not automatically deleted by expiration alone on all engines — always pair with &lt;code&gt;remove_orphan_files&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Key parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;older_than&lt;/code&gt;&lt;/strong&gt; — the retention boundary. Set this to at least 2× the duration of your longest-running query. A snapshot expired mid-scan produces a &lt;code&gt;FileNotFoundException&lt;/code&gt; that surfaces minutes later, making root cause analysis painful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;retain_last&lt;/code&gt;&lt;/strong&gt; — the minimum snapshot count regardless of age. Never set this to 1 — that leaves zero rollback targets. For streaming tables, 25–100 is typical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stream_results&lt;/code&gt;&lt;/strong&gt; — set to &lt;code&gt;true&lt;/code&gt; on large tables. Without it, the full list of deleted files is collected in the Spark driver's memory before being returned, which causes driver OOM on tables with millions of expired files.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Orphan file cleanup
&lt;/h3&gt;

&lt;p&gt;Distributed engines produce orphan files — data and metadata files that exist in object storage but are not referenced by any snapshot. They accumulate from failed Spark tasks, aborted compaction jobs, and commit conflicts. On active tables, orphan storage can grow to hundreds of gigabytes without anyone noticing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;remove_orphan_files&lt;/code&gt; lists every file in the table's storage location and compares what exists against what the metadata tree references. Anything unreferenced and older than the retention interval is deleted.&lt;/p&gt;

&lt;p&gt;The default retention is 3 days, but production teams use 7+ days. The reason: a Spark job that writes files at the start of a 4-hour run but commits at the end has unreferenced files for 4 hours. A retention shorter than that window deletes live data mid-write, silently corrupting the table.&lt;/p&gt;

&lt;p&gt;Three less-obvious failure modes to watch for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;URI scheme mismatch&lt;/strong&gt; — if your metadata records paths as &lt;code&gt;s3://&lt;/code&gt; but the listing returns &lt;code&gt;s3a://&lt;/code&gt;, the diff considers every file an orphan and deletes your entire table's data. Always verify schemes match before first run, and use &lt;code&gt;prefix_mismatch_mode =&amp;gt; 'ERROR'&lt;/code&gt; (the default since recent Iceberg versions) to catch this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 rate limiting&lt;/strong&gt; — orphan cleanup issuing thousands of concurrent deletes can trigger S3's 3,500 DELETE/s per-prefix throttle, producing &lt;code&gt;SlowDown&lt;/code&gt; errors that affect production reads. Use &lt;code&gt;max_concurrent_deletes&lt;/code&gt; to cap throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Driver OOM on large tables&lt;/strong&gt; — set &lt;code&gt;stream_results =&amp;gt; true&lt;/code&gt; to stream file lists in partitions rather than collecting them all in the driver.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Data file compaction
&lt;/h3&gt;

&lt;p&gt;Small files are the most visible maintenance problem. Each file adds metadata overhead, and each file open during a scan adds latency. A partition with 500 files averaging 4 MB each is orders of magnitude slower to scan than the same data in 8 files of 256 MB.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rewrite_data_files&lt;/code&gt; reads small files and rewrites them into larger, optimally-sized targets. Three strategies are available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binpack&lt;/strong&gt; — merges files without changing row order. Fast, low-risk, and sufficient when queries scan full partitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort&lt;/strong&gt; — rewrites files with rows physically ordered by specified columns. Maximizes data skipping via Parquet min/max statistics. More expensive (requires a full shuffle) but delivers the largest performance gains for filtered queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Z-order&lt;/strong&gt; — interleaves sort columns for multi-dimensional range queries. Useful when queries filter on multiple independent columns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Target file size depends on workload: 64–128 MB for point-lookup tables, 256–512 MB for analytical scans. Most production tables land at 256 MB as a default.&lt;/p&gt;

&lt;p&gt;Two parameters are critical for avoiding OOM during compaction: &lt;strong&gt;&lt;code&gt;max-file-group-size-bytes&lt;/code&gt;&lt;/strong&gt; controls how much data is processed in a single rewrite group — set it too high on a partition with thousands of small files and your Spark executors run out of memory. &lt;strong&gt;&lt;code&gt;rewrite-job-order&lt;/code&gt;&lt;/strong&gt; (e.g., &lt;code&gt;bytes-asc&lt;/code&gt;) lets you process smallest groups first, catching configuration problems early before committing resources to large rewrites. Note that Spark disables Adaptive Query Execution (AQE) during &lt;code&gt;rewrite_data_files&lt;/code&gt;, so size your shuffle resources explicitly rather than relying on auto-tuning.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Manifest rewriting
&lt;/h3&gt;

&lt;p&gt;Manifests are the metadata index — they map which data files exist in which partitions with which column statistics. When writes arrive in a pattern that doesn't match query patterns (e.g., data written hourly but queried by customer ID), manifests become fragmented. Query planners must scan more manifests than necessary, adding seconds of planning overhead on large tables.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rewrite_manifests&lt;/code&gt; reorganizes manifest files to align with the actual file layout, reducing query planning time. The operation is metadata-only — it rewrites Avro manifest files but never touches data files, making it fast (seconds on most tables) and low-risk. The conflict window with concurrent writers is correspondingly narrow, so run it immediately after compaction before the next batch of writes lands.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Delete file maintenance
&lt;/h3&gt;

&lt;p&gt;Iceberg's merge-on-read mode writes position delete files instead of immediately rewriting data files on row-level deletes. This is efficient for small deletes but accumulates overhead: every scan must reconcile delete files against base data at read time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rewrite_position_delete_files&lt;/code&gt; compacts small delete files and filters out dangling delete records — entries pointing to data files that no longer exist after compaction. This is the most frequently overlooked maintenance operation, and on tables with frequent row-level deletes, it can account for a significant portion of read overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why sequencing matters
&lt;/h3&gt;

&lt;p&gt;These operations have dependencies. Running them in the wrong order wastes compute or misses reclaimable storage:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expire snapshots first&lt;/strong&gt; — dereferences files held exclusively by old snapshots, converting them from "referenced" to "orphan-eligible." Without this step, orphan cleanup misses the largest category of reclaimable storage entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove orphans second&lt;/strong&gt; — catches everything expiration just released, plus any debris from failed writes. Reduces the file set before compaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compact data files third&lt;/strong&gt; — operates on a clean, minimal dataset. Compacting files that expiration would have removed is the single largest source of wasted compute in maintenance pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite manifests last&lt;/strong&gt; — reflects the final compacted layout. Rewriting manifests before compaction produces a metadata alignment that becomes stale the moment compaction runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running these as independent cron jobs on separate schedules is the most common source of maintenance collisions and wasted compute. See &lt;a href="https://lakeops.dev/blog/automating-iceberg-table-maintenance" rel="noopener noreferrer"&gt;Automating Apache Iceberg Table Maintenance&lt;/a&gt; for a detailed treatment of the sequencing rationale.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Commit conflicts during maintenance
&lt;/h3&gt;

&lt;p&gt;Iceberg uses optimistic concurrency control (OCC) for all metadata commits. When a maintenance operation and a writer commit simultaneously, one loses the race and must retry. On streaming tables producing multiple commits per minute, compaction jobs that run for 10+ minutes will almost certainly hit at least one conflict.&lt;/p&gt;

&lt;p&gt;The critical distinction: &lt;strong&gt;catalog commit conflicts&lt;/strong&gt; (two writers race to swap the metadata pointer) are transient and resolve through automatic retries. &lt;strong&gt;Data conflicts&lt;/strong&gt; (compaction rewrites files in a partition that received new writes during the rewrite) cause &lt;code&gt;ValidationException&lt;/code&gt; under serializable isolation and require either custom retry logic or switching to snapshot isolation.&lt;/p&gt;

&lt;p&gt;Production guidance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exclude hot partitions from compaction.&lt;/strong&gt; On time-partitioned tables, compact &lt;code&gt;WHERE event_date &amp;lt; current_date()&lt;/code&gt; — never today's partition while a streaming writer is appending to it. This single filter eliminates the most common source of compaction failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use snapshot isolation&lt;/strong&gt; for tables with concurrent streaming writes and compaction. It allows appends to the same partition that compaction is rewriting, as long as the appended files don't overlap with the rewritten set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Increase &lt;code&gt;commit.retry.num-retries&lt;/code&gt;&lt;/strong&gt; on streaming tables from the default 4 to 10–20. The retry only repeats the metadata commit, not the entire rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for retry storms.&lt;/strong&gt; Contention causes retries, retries increase catalog load, higher catalog load slows commits, which widens the contention window. If your catalog request rate climbs while successful commit rate falls, reduce concurrency rather than increasing retries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Engine-native maintenance: Spark
&lt;/h2&gt;

&lt;p&gt;For most teams, Spark is the first maintenance tool they use with Iceberg. The Iceberg connector exposes stored procedures covering every core operation:&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;-- 1. Expire snapshots older than 7 days, keep at least 10&lt;/span&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-09-09 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;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;stream_results&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="c1"&gt;-- prevents driver OOM on large tables&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Remove orphan files older than 7 days (always dry-run first)&lt;/span&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-09-09 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;span class="n"&gt;stream_results&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;span class="n"&gt;max_concurrent_deletes&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;  &lt;span class="c1"&gt;-- throttle to avoid S3 rate limits&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Compact data files (exclude today's hot partition)&lt;/span&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;lt; current_date()'&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;'5'&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;'3221225472'&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="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Rewrite manifests&lt;/span&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;span class="c1"&gt;-- 5. Clean up dangling position delete files&lt;/span&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_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;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;'rewrite-all'&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;What Spark does well:&lt;/strong&gt; Full control over every parameter. Support for all five operations. The &lt;code&gt;partial-progress.enabled&lt;/code&gt; option is critical for production — it commits compaction incrementally, so a failure at 80% doesn't lose all prior work. The &lt;code&gt;stream_results&lt;/code&gt; option prevents driver OOM on tables with millions of files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Spark doesn't do:&lt;/strong&gt; Spark provides procedures, not a maintenance system. There is no scheduling, no health detection, no multi-table orchestration, no sequencing logic, and no awareness of what other engines or operations are running against the same table. You build all of that yourself — typically as Airflow DAGs, cron jobs, or custom scripts. That works at 10 tables. At 500, it becomes its own infrastructure project.&lt;/p&gt;

&lt;p&gt;The other major limitation is cost: Spark requires a running cluster. Running maintenance on a dedicated Spark cluster that sits idle between jobs, or sharing a cluster with production queries and risking commit conflicts, is a tradeoff every team manages differently.&lt;/p&gt;
&lt;h2&gt;
  
  
  Engine-native maintenance: Trino and Flink
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Trino
&lt;/h3&gt;

&lt;p&gt;Trino exposes maintenance through &lt;code&gt;ALTER TABLE ... EXECUTE&lt;/code&gt;, keeping operations inside the same catalog, permissions model, and deployment pipeline as queries:&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;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;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;optimize&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;current_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt; &lt;span class="k"&gt;DAY&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;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;expire_snapshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retention_threshold&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'7d'&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;EXECUTE&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="n"&gt;retention_threshold&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'7d'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The advantage is simplicity — no separate Spark cluster for maintenance. The disadvantage is that maintenance competes with query resources on the same Trino cluster, and Trino's maintenance surface is narrower than Spark's (no sort compaction, no position delete file rewriting, fewer tuning knobs).&lt;/p&gt;
&lt;h3&gt;
  
  
  Flink
&lt;/h3&gt;

&lt;p&gt;Flink provides native streaming maintenance through the &lt;code&gt;TableMaintenance&lt;/code&gt; API. Maintenance runs as a Flink streaming job — triggered by commit counts or file thresholds rather than external schedules. Since Iceberg 1.12, external lock factories are deprecated; Flink uses its own coordination locks by default:&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;// Standalone maintenance job (Flink-managed coordination lock)&lt;/span&gt;
&lt;span class="nc"&gt;TableMaintenance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forTable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tableLoader&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uidSuffix&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-maintenance-job"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rateLimit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMinutes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lockCheckDelay&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExpireSnapshots&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scheduleOnCommitCount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxSnapshotAge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofDays&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retainLast&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RewriteDataFiles&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scheduleOnDataFileCount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;targetFileSizeBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;partialProgressEnabled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;partialProgressMaxCommits&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DeleteOrphanFiles&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;minAge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofDays&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Flink also supports &lt;strong&gt;post-commit maintenance&lt;/strong&gt; directly in &lt;code&gt;IcebergSink&lt;/code&gt; — compaction, expiration, and orphan cleanup run automatically after each data commit, eliminating external scheduling entirely for Flink-written tables:&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="nc"&gt;IcebergSink&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forRowData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataStream&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tableLoader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableLoader&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rewriteDataFiles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;RewriteDataFilesConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MAX_BYTES&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1073741824"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expireSnapshots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;ExpireSnapshotsConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RETAIN_LAST&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;ExpireSnapshotsConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MAX_SNAPSHOT_AGE_SECONDS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"604800"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteOrphanFiles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;DeleteOrphanFilesConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MIN_AGE_SECONDS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"259200"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For tables with row-level deletes in upsert/CDC workloads, Flink also provides &lt;code&gt;ConvertEqualityDeletes&lt;/code&gt; — a maintenance task that rewrites expensive equality deletes into position deletion vectors (requires Iceberg format v3). This can significantly reduce read amplification on tables with frequent upserts.&lt;/p&gt;
&lt;h3&gt;
  
  
  When engine-native works
&lt;/h3&gt;

&lt;p&gt;Engine-native tools are a good fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a small number of tables (under ~50)&lt;/li&gt;
&lt;li&gt;A single engine handles both writes and maintenance&lt;/li&gt;
&lt;li&gt;Your team has the capacity to build and maintain orchestration (DAGs, cron, monitoring)&lt;/li&gt;
&lt;li&gt;You don't need cross-engine coordination&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They start to break down when multiple engines touch the same tables, when table counts grow beyond what manual orchestration can cover, or when the operational cost of maintaining the maintenance system itself becomes significant.&lt;/p&gt;
&lt;h2&gt;
  
  
  Standalone maintenance tools
&lt;/h2&gt;

&lt;p&gt;A newer category of tooling has emerged: standalone maintenance binaries that run outside any query engine. They connect directly to Iceberg catalogs and object storage, execute maintenance operations, and exit. No Spark cluster, no Trino session, no JVM in some cases.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bergman
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/hupe1980/bergman" rel="noopener noreferrer"&gt;Bergman&lt;/a&gt; is a Rust-native maintenance engine built on &lt;code&gt;iceberg-rust&lt;/code&gt; and Apache DataFusion. One static binary. No JVM.&lt;/p&gt;

&lt;p&gt;It supports all core operations — compaction, snapshot expiration, orphan cleanup, manifest rewriting, and dangling delete removal — driven by declarative TOML policies:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bergman inspect   &lt;span class="c"&gt;# table health: file counts, sizes, delete ratios&lt;/span&gt;
bergman plan      &lt;span class="c"&gt;# what maintenance would do, and why&lt;/span&gt;
bergman run       &lt;span class="c"&gt;# execute the plan (same code path as plan)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Bergman's design is library-first: the binary is a thin wrapper around a public Rust API, so catalogs and control planes can embed the engine in-process. Destructive operations (orphan deletion, compaction, file cleanup) are off by default and must be explicitly opted into via policy. It currently supports REST catalogs and is early-stage — all operations are functional but it has not yet seen large-scale production use.&lt;/p&gt;
&lt;h3&gt;
  
  
  Firn
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/basekick-labs/firn" rel="noopener noreferrer"&gt;Firn&lt;/a&gt; is a single Go binary that provides writer-agnostic maintenance — compaction (binpack, sort, and z-order via a DuckDB subprocess), snapshot expiration, and orphan cleanup. It supports AWS Glue, Lakekeeper, Apache Polaris, and Nessie catalogs, with S3-compatible, GCS, and Azure storage backends. Firn positions itself as an open-source alternative to AWS S3 Tables' automatic maintenance — the same operations, without AWS lock-in or the cost premium. Pre-v1.0.&lt;/p&gt;
&lt;h3&gt;
  
  
  iceberg-zamboni
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/iceberg-zamboni/" rel="noopener noreferrer"&gt;iceberg-zamboni&lt;/a&gt; uses PyIceberg for metadata and DuckDB for sorting, providing all six maintenance operations as a single Python process. It supports Z-order clustering, partition evolution, dangling-delete removal, and manifest rewriting — locally by default, with optional Trino or Spark Connect backends for scale. Install with &lt;code&gt;pipx install iceberg-zamboni&lt;/code&gt;. Declarative table configs describe the target layout; every mutating command requires &lt;code&gt;--yes&lt;/code&gt; to execute.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ice-keeper
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/CybercentreCanada/ice-keeper" rel="noopener noreferrer"&gt;Ice-keeper&lt;/a&gt; wraps Iceberg's Spark procedures and orchestrates them across many tables using rules stored in Iceberg &lt;code&gt;tblproperties&lt;/code&gt;. It handles table discovery, parallelizes work across large inventories, and records every action in a journal for auditability. It runs wherever Spark runs — the benefit is multi-table orchestration, not engine independence.&lt;/p&gt;
&lt;h3&gt;
  
  
  Floe
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/nssalian/floe" rel="noopener noreferrer"&gt;Floe&lt;/a&gt; is a policy-based maintenance system that adds declarative policies over Spark or Trino. Define rules matching tables by glob or regex pattern, set cron schedules or manual triggers, and Floe orchestrates compaction, snapshot expiration, orphan cleanup, and manifest optimization across multiple catalogs (REST, Polaris, Lakekeeper, Gravitino, DataHub, Nessie, Hive). Policies support priority-based resolution when multiple patterns match the same table and optional health-based trigger conditions. Apache 2.0 licensed.&lt;/p&gt;
&lt;h3&gt;
  
  
  What standalone tools add
&lt;/h3&gt;

&lt;p&gt;The common thread: these tools treat maintenance as a first-class concern separate from query execution. They add table discovery, policy-driven configuration, multi-table orchestration, and operational logging — the layer that raw engine procedures leave entirely to you.&lt;/p&gt;

&lt;p&gt;What they generally don't provide: health-driven triggering (knowing &lt;em&gt;when&lt;/em&gt; a table needs maintenance based on its actual state), cross-engine awareness, intelligent sort-order selection based on query patterns, or the ability to adapt cadence to write velocity.&lt;/p&gt;
&lt;h2&gt;
  
  
  Platform maintenance layers
&lt;/h2&gt;

&lt;p&gt;If you run within a managed platform, maintenance tooling is increasingly built in:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Maintenance approach&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Key characteristics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudera CLO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Policy-based, schedule or event triggers&lt;/td&gt;
&lt;td&gt;Spark&lt;/td&gt;
&lt;td&gt;Compaction, expiration, orphan cleanup. REST API for policy management.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Starburst/Galaxy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trino-native scheduled maintenance&lt;/td&gt;
&lt;td&gt;Trino&lt;/td&gt;
&lt;td&gt;No separate Spark cluster. Scope-based scheduling. Limited to Trino's Iceberg connector surface.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dremio Open Catalog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Autonomous compaction and cleanup&lt;/td&gt;
&lt;td&gt;Polaris-based&lt;/td&gt;
&lt;td&gt;Built on Apache Polaris. Auto-compaction, auto-vacuum, Iceberg Clustering (Z-order).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Glue Table Optimizers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed compaction and retention&lt;/td&gt;
&lt;td&gt;Spark (managed)&lt;/td&gt;
&lt;td&gt;Automatic compaction and snapshot management for Glue Catalog tables. AWS-only.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Athena&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;VACUUM&lt;/code&gt; and &lt;code&gt;OPTIMIZE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Athena&lt;/td&gt;
&lt;td&gt;Serverless maintenance within Athena sessions. Simple but narrow.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snowflake&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatic for managed Iceberg tables&lt;/td&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;Invisible for Snowflake-managed tables. Open Catalog tables require external maintenance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Databricks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OPTIMIZE&lt;/code&gt; and &lt;code&gt;VACUUM&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Spark (managed)&lt;/td&gt;
&lt;td&gt;Deeply integrated with Delta, Iceberg support through UniForm. Tied to the Databricks runtime.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IOMETE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Health-driven detect → evaluate → execute&lt;/td&gt;
&lt;td&gt;Spark&lt;/td&gt;
&lt;td&gt;Threshold-based triggers for internal REST catalog tables. Table-level overrides.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Platform tools excel when you're fully committed to one ecosystem. They break down when your data lake spans multiple engines, catalogs, or clouds — which is increasingly common in production architectures that use Iceberg specifically to avoid vendor lock-in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lakehouse control planes
&lt;/h2&gt;

&lt;p&gt;The gap in all the approaches above is the same: none of them treat maintenance as a lake-wide coordination problem. Engine procedures don't know about other engines. Standalone tools orchestrate operations but don't understand query patterns. Platform layers are confined to their ecosystem.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;lakehouse control plane&lt;/a&gt;&lt;br&gt;
 bridges your catalogs, engines, and storage into a single operational layer — the coordination fabric that individual components were never designed to provide. &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; is built specifically for this role.&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%2F4cqulzhls2kltkw2en6p.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%2F4cqulzhls2kltkw2en6p.png" alt=" " width="799" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Learn more:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://lakeops.dev/blog/what-is-data-lakehouse-control-plane" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fapi.getpronto.io%2Fv1%2Ffile%2Fuser-cmosmjqho103245978n6h8tqsbi%2Fmodern-iceberg-lakehouse-1778503762167-12423c3fb892aacf.png" height="512" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://lakeops.dev/blog/what-is-data-lakehouse-control-plane" rel="noopener noreferrer" class="c-link"&gt;
            What Is a Data Lakehouse Control Plane? - LakeOps Blog
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            What is a data lakehouse control plane? An intelligent layer that autonomously maintains, optimizes, and governs Apache Iceberg tables across all engines.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flakeops.dev%2Ffavicon-32.png" width="32" height="32"&gt;
          lakeops.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  Health-driven vs. schedule-driven
&lt;/h3&gt;

&lt;p&gt;The fundamental architectural difference between a control plane and a cron-based approach is what triggers maintenance.&lt;/p&gt;

&lt;p&gt;Scheduled maintenance runs on fixed intervals regardless of table state. A table that receives no writes still gets compacted on Tuesday. A streaming table that accumulated 10,000 small files since the last run waits until the next scheduled window.&lt;/p&gt;

&lt;p&gt;Health-driven maintenance evaluates each table's structural signals continuously — file count per partition, average file size relative to target, snapshot count, manifest depth, delete-file ratio — and triggers operations only when indicators cross thresholds. Streaming tables may be maintained hourly. Batch tables daily. Idle tables are skipped entirely. The cadence adapts to write velocity, not wall-clock time.&lt;/p&gt;

&lt;h3&gt;
  
  
  How LakeOps approaches the problem
&lt;/h3&gt;

&lt;p&gt;LakeOps plugs into your existing Iceberg catalogs (REST, Glue, Polaris, Nessie, S3 Tables) and engines (Spark, Trino, Flink, Snowflake, Athena, DuckDB) through their standard APIs. No infrastructure changes, no code changes — only metadata is processed, and 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%2Fl65lskunzvcbjq85apfg.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%2Fl65lskunzvcbjq85apfg.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For maintenance specifically, the system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuously assesses every table's structural condition&lt;/strong&gt; — file layout, snapshot accumulation, manifest depth, write velocity — and flags degradation before it reaches queries. The &lt;a href="https://lakeops.dev/blog/iceberg-table-health-maintenance" rel="noopener noreferrer"&gt;table health scoring model&lt;/a&gt; evaluates signals that individual engine procedures have no visibility into, surfacing the tables that need maintenance most urgently.&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%2Ff7wvmrv9sj0plxxjk0gb.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%2Ff7wvmrv9sj0plxxjk0gb.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Triggers Operations based on tresholds or events and sequences operations in dependency order&lt;/strong&gt; per table: snapshot expiration → orphan cleanup → compaction → manifest rewriting. Each step runs to completion before the next begins, eliminating the conflict scenarios and wasted-compute problems described above. There is no possibility of expiration conflicting with in-flight compaction, because they never run concurrently on the same table.&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%2Fplrriwzv1iq82o4iy4sx.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%2Fplrriwzv1iq82o4iy4sx.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Applies query-aware sort orders&lt;/strong&gt; during compaction. Rather than blindly merging small files, LakeOps watches which columns your queries actually filter, join, and group on across all connected engines — then physically re-sorts data to match. Engines skip entire file groups via min/max pruning instead of scanning everything. The sort strategy adapts as query patterns evolve, and proposed layout changes are validated on Iceberg branches before being applied.&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%2F18spcr36z427nq2f70rq.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%2F18spcr36z427nq2f70rq.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runs compaction on a purpose-built engine&lt;/strong&gt; (Rust + Apache DataFusion) rather than requiring a Spark cluster. This eliminates JVM startup latency, executor provisioning, and resource contention with production queries — a significant operational and cost reduction.&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%2Ftebplzoeuj81mwwjlris.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%2Ftebplzoeuj81mwwjlris.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Governs maintenance through policies&lt;/strong&gt; rather than per-table configuration. Thresholds, retention windows, and sort strategies propagate through a namespace hierarchy, so new tables comply automatically. Execution modes range from fully autonomous to manual approval.&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%2F4rpcoxganpru6oi69hn6.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%2F4rpcoxganpru6oi69hn6.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;platform page&lt;/a&gt; shows the full maintenance pipeline in detail — from health scoring through coordinated execution to the event trail that logs every operation's duration, file impact, and bytes reclaimed.&lt;/p&gt;

&lt;p&gt;Real example from a 7k tables Iceberg Lakehouse:&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%2Ffqtr2v11o9aliynf2th3.jpeg" 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%2Ffqtr2v11o9aliynf2th3.jpeg" alt=" " width="797" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For teams dealing specifically with &lt;a href="https://lakeops.dev/blog/iceberg-small-files-guide" rel="noopener noreferrer"&gt;small file accumulation&lt;/a&gt; or the operational burden of &lt;a href="https://lakeops.dev/blog/iceberg-table-cleanup" rel="noopener noreferrer"&gt;table cleanup at scale&lt;/a&gt;, the control plane approach replaces per-table cron jobs with a single system that handles the entire maintenance lifecycle across the lake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the right approach
&lt;/h2&gt;

&lt;p&gt;The right tool depends on three variables: how many tables you manage, how many engines touch them, and how much operational overhead your team can absorb.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Recommended approach&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;&lt;strong&gt;&amp;lt; 20 tables, single engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Engine-native procedures + cron/Airflow&lt;/td&gt;
&lt;td&gt;Simple, direct, full control. The orchestration overhead is manageable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;20–100 tables, single engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standalone tool (Floe, Ice-keeper) or platform layer&lt;/td&gt;
&lt;td&gt;Multi-table orchestration is necessary. Manual per-table configuration stops scaling.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;100+ tables, single engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Platform layer or control plane&lt;/td&gt;
&lt;td&gt;Health-driven triggering matters. You can't manually monitor 100+ tables for maintenance signals.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Any count, multiple engines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Control plane (&lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Cross-engine coordination is the critical requirement. No single engine has visibility into what the others are doing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fully managed platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built-in platform tools&lt;/td&gt;
&lt;td&gt;If you're all-in on Databricks, Snowflake, or Cloudera, use what's already there.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No-JVM requirement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bergman, Firn, iceberg-zamboni, or LakeOps&lt;/td&gt;
&lt;td&gt;Bergman (Rust), Firn (Go), and iceberg-zamboni (Python+DuckDB) are open-source standalone tools. LakeOps is a managed control plane with a Rust/DataFusion engine.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There's no single correct answer. The common mistake is underestimating how quickly the operational surface area grows. Ten tables with four operations each, sequenced correctly, monitored for failures, adapted to write patterns, across two engines — that's already a non-trivial system to build and operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical implementation checklist
&lt;/h2&gt;

&lt;p&gt;Regardless of which tool you choose, these principles apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always dry-run orphan cleanup&lt;/strong&gt; before executing. A misconfigured &lt;code&gt;older_than&lt;/code&gt; or a URI scheme mismatch can delete live data. Verify the count is reasonable — if it's close to your total file count, something is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expire before compact.&lt;/strong&gt; This is the single most impactful sequencing rule. Compacting files that expiration would have removed wastes the most compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable &lt;code&gt;partial-progress&lt;/code&gt;&lt;/strong&gt; for compaction on any table larger than a few gigabytes. Without it, a single OCC conflict at 90% completion loses all prior work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exclude hot partitions from compaction.&lt;/strong&gt; Use &lt;code&gt;where =&amp;gt; 'event_date &amp;lt; current_date()'&lt;/code&gt; or equivalent. Compacting partitions with active streaming writers is the primary cause of commit conflicts and wasted compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;retain_last&lt;/code&gt; to at least 2.&lt;/strong&gt; Setting it to 1 leaves zero rollback targets after the current snapshot. For streaming tables, 25–100 is appropriate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;stream_results =&amp;gt; true&lt;/code&gt;&lt;/strong&gt; for &lt;code&gt;expire_snapshots&lt;/code&gt; and &lt;code&gt;remove_orphan_files&lt;/code&gt; on any table with more than a few thousand files. Without it, the full file list is collected in the Spark driver's memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a 7+ day orphan retention.&lt;/strong&gt; The 3-day default is too aggressive for environments with long-running Spark jobs or Flink recovery scenarios. Verify URI schemes match before first run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor compaction output, not just completion.&lt;/strong&gt; A compaction job that succeeds but produces files 10× larger than your target is masking a configuration problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;rewrite_position_delete_files&lt;/code&gt; regularly&lt;/strong&gt; on tables with row-level deletes. Dangling deletes are invisible in most monitoring but accumulate meaningful read overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the ecosystem is heading
&lt;/h2&gt;

&lt;p&gt;The Iceberg maintenance ecosystem is converging on a clear pattern: from manual procedures toward automated, health-driven systems. Flink's native &lt;code&gt;TableMaintenance&lt;/code&gt; API builds maintenance into the streaming pipeline. Standalone tools like Bergman, Firn, and iceberg-zamboni remove the JVM dependency entirely. Control planes like &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; close the loop between table health signals and maintenance execution across the full lake.&lt;/p&gt;

&lt;p&gt;The underlying insight across all of these is the same: maintenance is a continuous structural concern, not an ad-hoc operational task. The teams that treat it as infrastructure — with proper sequencing, conflict awareness, adaptive cadence, and per-table tuning — avoid the silent degradation that catches everyone else by surprise six months into production.&lt;/p&gt;

</description>
      <category>iceberg</category>
      <category>dataengineering</category>
      <category>lakehouse</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Amazon S3 Tables Alternatives for Production Iceberg</title>
      <dc:creator>joni sar</dc:creator>
      <pubDate>Wed, 16 Sep 2026 12:30:56 +0000</pubDate>
      <link>https://dev.to/jonisar/amazon-s3-tables-alternatives-for-production-iceberg-3ibp</link>
      <guid>https://dev.to/jonisar/amazon-s3-tables-alternatives-for-production-iceberg-3ibp</guid>
      <description>&lt;p&gt;Amazon S3 Tables solves a real problem. If you have ever spent a weekend debugging a failed compaction job, or watched a table accumulate 40,000 small files because nobody remembered to schedule &lt;code&gt;rewrite_data_files&lt;/code&gt;, the pitch is compelling: create a table bucket, write data, and AWS handles compaction, snapshot expiration, and orphan cleanup automatically. No Spark clusters. No Airflow DAGs. No on-call for maintenance failures.&lt;/p&gt;

&lt;p&gt;For greenfield deployments with straightforward requirements — especially AWS-native stacks using Athena, EMR, and Redshift — that trade-off works well and can save weeks of infrastructure setup. But production Iceberg estates tend to outgrow what S3 Tables can deliver, and the constraints become apparent exactly when you need flexibility most.&lt;/p&gt;

&lt;p&gt;This article breaks down what S3 Tables does well, where it falls short, and what alternatives exist for teams that need more from their Iceberg operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What S3 Tables actually gives you
&lt;/h2&gt;

&lt;p&gt;S3 Tables, generally available across most AWS regions since 2025, merges Iceberg storage and catalog into a single managed service. You create a "table bucket" (a dedicated S3 bucket type that only stores Iceberg tables), and AWS provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Built-in Iceberg REST Catalog&lt;/strong&gt; — each table bucket exposes a REST endpoint that any Iceberg-compatible engine can connect to. S3 Tables also integrates with the Glue Data Catalog through a federated &lt;code&gt;s3tablescatalog&lt;/code&gt;, making tables discoverable alongside standard Glue-managed tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic compaction&lt;/strong&gt; — background binpack, sort, or z-order compaction with configurable target file sizes (64 MB to 512 MB). As of July 2025, compaction costs were reduced by up to 90%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot expiration and orphan cleanup&lt;/strong&gt; — managed automatically without any user-side scheduling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher write throughput&lt;/strong&gt; — AWS reports up to 10x higher transactions per second compared to general-purpose S3 buckets. For high-concurrency streaming ingestion pipelines, this can be the deciding factor — Glue Catalog limits concurrent &lt;code&gt;UPDATE TABLE&lt;/code&gt; calls to roughly 10 per second per table by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent-Tiering&lt;/strong&gt; — automatic storage cost optimization based on access patterns. Compaction runs only on files in the Frequent Access tier, reducing maintenance costs on cold data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SageMaker Lakehouse integration&lt;/strong&gt; — S3 Tables integrates with SageMaker Lakehouse for unified analytics across S3 Tables, Redshift, and third-party sources through a single management experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-region replication&lt;/strong&gt; — read-only replicas of tables across regions and accounts, preserving full snapshot history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a team of two or three engineers standing up their first Iceberg lakehouse on AWS, this eliminates weeks of infrastructure work. Credit where it is due: zero-configuration maintenance is a genuine step forward for the ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where S3 Tables falls short
&lt;/h2&gt;

&lt;p&gt;The constraints surface as soon as requirements move beyond the simple case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Black-box maintenance
&lt;/h3&gt;

&lt;p&gt;S3 Tables handles compaction, but you cannot see what it did. There are no detailed compaction logs, no before/after file counts, and no way to correlate a compaction run with a change in query latency. The trigger conditions for auto-compaction are undocumented — understanding when and why compaction fired (or didn't) requires an AWS support case.&lt;/p&gt;

&lt;p&gt;For streaming workloads with freshness SLAs, this opacity is a production risk. Compaction that runs during a peak query window can temporarily increase latency. Without visibility into the schedule, you cannot coordinate maintenance with critical workloads.&lt;/p&gt;

&lt;p&gt;There is also a subtler issue: &lt;strong&gt;commit contention&lt;/strong&gt;. S3 Tables runs maintenance operations continuously in the background, which can advance table metadata and conflict with your writes. Pipelines see &lt;code&gt;CommitFailedException: branch main has changed&lt;/code&gt; — a retry-and-backoff situation that requires explicit handling in your ingestion code. AWS documents that maintenance jobs themselves can fail due to "out of date metadata" and get retried. For high-frequency writers, this contention is an operational reality that the "zero-ops" pitch does not surface.&lt;/p&gt;

&lt;p&gt;Snapshot management has its own gotchas. If any user-defined tag or branch exists on a table, snapshot management fails for the entire table — no snapshots are expired or removed until you delete all tags and branches. Setting Iceberg snapshot retention properties (&lt;code&gt;history.expire.max-snapshot-age-ms&lt;/code&gt; or &lt;code&gt;history.expire.min-snapshots-to-keep&lt;/code&gt;) as table properties also causes snapshot management to fail silently. These are not edge cases for teams using Iceberg's branching features for testing or auditing.&lt;/p&gt;

&lt;p&gt;The maintenance API lets you configure target file size and compaction strategy per table. That is the extent of your control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engine compatibility gaps
&lt;/h3&gt;

&lt;p&gt;S3 Tables works natively with Athena, EMR, and Redshift. Through the REST Catalog API and the Glue Data Catalog federation, AWS also lists Spark, Trino, Flink, Snowflake, and DuckDB as compatible. Engine breadth has improved significantly since launch. But practical gaps remain:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dbt&lt;/strong&gt;: dbt-athena 1.11.0 (July 2026) shipped end-to-end S3 Tables support via &lt;code&gt;catalogs.yml v2&lt;/code&gt;. This is real progress. However, S3 Tables does not support &lt;code&gt;ALTER TABLE RENAME&lt;/code&gt;, which breaks dbt's standard near-zero-downtime table swap. The materialization uses drop-and-recreate on every run, S3 cleanup is skipped (since S3 Tables manages storage), and tables must be lowercase-only. For teams where dbt is the primary transformation framework — which is most modern data teams — this is workable but not equivalent to standard Glue-backed Iceberg.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Views&lt;/strong&gt;: &lt;code&gt;CREATE VIEW&lt;/code&gt; is not supported against the S3 Tables catalog endpoint. Views are fundamental to most transformation and BI layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DDL restrictions&lt;/strong&gt;: The REST endpoint does not support &lt;code&gt;stage-create&lt;/code&gt; (blocking CTAS in some engine configurations), multi-level namespaces, or OAuth-based authentication. Metadata files over 50 MB return a &lt;code&gt;400 Bad Request&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catalog separation&lt;/strong&gt;: S3 table bucket catalogs surface as a federated &lt;code&gt;s3tablescatalog&lt;/code&gt; in the Glue Data Catalog, which is an improvement over full isolation. But the hierarchical mapping (table bucket → child catalog → database → table) means cross-catalog joins between standard Glue tables and S3 Tables still require fully qualified references. It is more integrated than at launch, but not seamless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permission failure modes&lt;/strong&gt;: A notable gotcha from production PoCs: when Lake Formation permissions are missing, &lt;code&gt;SHOW TABLES&lt;/code&gt; returns empty results rather than an access-denied error. There is no way to distinguish between "table doesn't exist" and "you lack permissions." This was &lt;a href="https://nanta-data.dev/en/posts/s3-table-buckets-poc/" rel="noopener noreferrer"&gt;reported as the single biggest time sink&lt;/a&gt; in at least one published S3 Tables evaluation.&lt;/p&gt;

&lt;p&gt;The structural concern remains: new tools in the data ecosystem integrate against the open Iceberg spec first. When a query engine ships Iceberg support, it works against a standard Glue-registered table on day one. S3 Tables has narrowed the gap significantly through its REST endpoint and Glue federation, but some tools still need specific adapter work for the &lt;code&gt;s3tablescatalog&lt;/code&gt; path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security: Lake Formation support with caveats
&lt;/h3&gt;

&lt;p&gt;S3 Tables now integrates with Lake Formation through the Glue Data Catalog federation, supporting column-level, row-level, and cell-level security. This is a significant improvement over the table-level-only IAM policies available at launch.&lt;/p&gt;

&lt;p&gt;The caveat: when you enable "full table access" for external engines (third-party engines outside the AWS analytics stack), Lake Formation returns credentials without enforcing column or row filters. Fine-grained access control only applies when engines use Lake Formation credential vending without the full-table-access flag. For teams running mixed engine stacks — which is common in production — this means security enforcement depends on how each engine connects, not just what permissions are defined. Standard AWS engines (Athena, EMR, Redshift) enforce Lake Formation grants; external engines may bypass them depending on configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS-only, exit requires rewrite
&lt;/h3&gt;

&lt;p&gt;To be clear about what is and is not locked in: the data itself is standard Apache Iceberg on Parquet. The format is open. The lock-in is at the &lt;strong&gt;storage and catalog layer&lt;/strong&gt;. S3 table buckets are not accessible via standard S3 object APIs — you cannot &lt;code&gt;aws s3 ls&lt;/code&gt; the underlying files or point another catalog at the same path. Moving data out of S3 Tables requires a full data export (typically via Spark CTAS), not a catalog pointer swap.&lt;/p&gt;

&lt;p&gt;For a small table, this is a manageable migration. For a lake at scale, it is a significant project. &lt;a href="https://awstip.com/s3-tables-vs-iceberg-on-aws-which-wins-d73b6c316e94" rel="noopener noreferrer"&gt;As one detailed comparison put it&lt;/a&gt;: "With self-managed Iceberg, your files are on standard S3, accessible via standard S3 APIs. The catalog is a pointer. Switching to any other catalog means re-registering the table path. Nothing moves. S3 Tables is a different commitment."&lt;/p&gt;

&lt;p&gt;There is also a subtler ecosystem portability dimension. When your catalog is proprietary, ecosystem support depends on AWS's roadmap and release schedule. On the self-managed Iceberg path, integrations happen in the open, on the community's timeline. That structural difference matters when the ecosystem is moving as fast as it is now.&lt;/p&gt;

&lt;h3&gt;
  
  
  No cross-estate observability
&lt;/h3&gt;

&lt;p&gt;S3 Tables provides basic CloudWatch metrics per table — object count, storage size, maintenance job status. There is no cross-table health dashboard, no severity-ranked alerting, and no way to see which tables are degrading before they affect query performance or cost.&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%2Fr60x53kqk6qwycmtzz5r.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%2Fr60x53kqk6qwycmtzz5r.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At scale, the question is not "is compaction running?" — it is "which of my 500 tables has a small-file ratio above 40%, and what is the impact on query latency across the three engines that read it?"&lt;/p&gt;

&lt;p&gt;Here's a preview of table health observability with a control plane like &lt;a href="https://lakeops.dev" rel="noopener noreferrer"&gt;LakeOps&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%2Fpazov02yj275i86k2yfg.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%2Fpazov02yj275i86k2yfg.png" alt=" " width="799" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Incomplete maintenance coverage
&lt;/h3&gt;

&lt;p&gt;S3 Tables runs compaction, snapshot expiration, and orphan cleanup. It does not handle manifest rewriting or Puffin column-level statistics computation. Both directly affect query planning performance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manifest bloat&lt;/strong&gt;: Frequent commits accumulate hundreds of small manifest files. Each one must be opened during query planning, adding seconds of latency before any data is read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing statistics&lt;/strong&gt;: Without fresh Puffin NDV, min/max, and null-count statistics, engines cannot prune files effectively. They scan more data than necessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not niche operations. They are part of the standard Iceberg maintenance lifecycle, and their absence means S3 Tables' "fully managed" maintenance is, in practice, partial.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alternatives
&lt;/h2&gt;

&lt;p&gt;When engineers search for S3 Tables alternatives, they are typically looking for one of three things: more engine flexibility, more operational control, or vendor neutrality. The answer depends on which constraint is binding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-managed Iceberg on standard S3
&lt;/h3&gt;

&lt;p&gt;The traditional approach: write Iceberg tables to standard S3 buckets, register them in AWS Glue Data Catalog (or a REST catalog like Apache Polaris or Nessie), and run maintenance yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you gain:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every Iceberg-compatible engine works on day one — Trino, Spark, Flink, DuckDB, Snowflake, Databricks, ClickHouse, dbt.&lt;/li&gt;
&lt;li&gt;Full Lake Formation support for column-level security and row-level filtering without the external-engine caveats that affect S3 Tables.&lt;/li&gt;
&lt;li&gt;Standard S3 object access — no proprietary storage layer, no exit rewrite. Switching catalogs means re-registering the table path; nothing moves.&lt;/li&gt;
&lt;li&gt;Complete maintenance control — you decide when compaction runs, what sort order to use, and how aggressively to expire snapshots.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it costs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You own the entire maintenance lifecycle. Compaction is a Spark job or Athena &lt;code&gt;OPTIMIZE&lt;/code&gt; call. Snapshot expiration and orphan cleanup are separate procedures. Getting the sequence wrong — for example, running orphan cleanup before snapshot expiration finishes, or skipping manifest rewrites after compaction — wastes compute or risks deleting files still referenced by active snapshots.&lt;/li&gt;
&lt;li&gt;At 10–50 tables, this is manageable with a few DAGs. At 500+ tables with different ingestion rates, it becomes the primary source of operational incidents.&lt;/li&gt;
&lt;li&gt;No built-in observability across the estate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Self-managed Iceberg is the default for most production teams today. It works — but the operational burden scales linearly with the number of tables, and most teams underestimate the ongoing cost until they are deep into it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open-source REST catalogs (Apache Polaris, Lakekeeper, Nessie)
&lt;/h3&gt;

&lt;p&gt;If the concern is catalog lock-in rather than maintenance automation, an open REST catalog can replace S3 Tables' built-in catalog without the proprietary coupling.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://polaris.apache.org/" rel="noopener noreferrer"&gt;Apache Polaris&lt;/a&gt; (graduated to ASF Top-Level Project in 2026) implements the Iceberg REST Catalog spec with RBAC, credential vending, multi-engine support, and external catalog federation. It can be self-hosted on Kubernetes with PostgreSQL, or used through managed offerings like Snowflake Open Catalog or Dremio Catalog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lakekeeper.io/" rel="noopener noreferrer"&gt;Lakekeeper&lt;/a&gt; is a Rust-based REST catalog focused on authorization — OPA-based policies, audit events, and fine-grained governance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://projectnessie.org/" rel="noopener noreferrer"&gt;Nessie&lt;/a&gt; adds Git-like branching and tagging to the catalog layer, enabling table versioning and isolated experimentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you gain:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vendor-neutral catalog that works on any cloud.&lt;/li&gt;
&lt;li&gt;Full Iceberg REST spec compliance — every engine integrates without custom adapters.&lt;/li&gt;
&lt;li&gt;Credential vending, RBAC, and (in Polaris) external catalog federation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What they don't solve:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;None of these catalogs handle compaction, snapshot expiration, or orphan cleanup on their own. They are metadata management services, not maintenance automation. You still need a separate system for the operational lifecycle.&lt;/li&gt;
&lt;li&gt;Self-hosting requires infrastructure: PostgreSQL backend (Polaris) or Rust binary (Lakekeeper), Kubernetes deployment, monitoring, upgrades.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Emerging: open-source maintenance layers.&lt;/strong&gt; &lt;a href="https://polaris.apache.org/blog/2026/02/04/floe-and-apache-polaris-policy-driven-table-maintenance-for-apache-iceberg/" rel="noopener noreferrer"&gt;Floe&lt;/a&gt; connects to Polaris and provides policy-driven maintenance (compaction, snapshot expiration, orphan cleanup, manifest optimization) executed via Spark or Trino. &lt;a href="https://github.com/basekick-labs/firn" rel="noopener noreferrer"&gt;Firn&lt;/a&gt; is a Go-based maintenance daemon that works with Lakekeeper, Polaris, Nessie, or Glue — using DuckDB for compaction instead of Spark. Both are pre-1.0 but represent the open-source ecosystem's response to the maintenance gap.&lt;/p&gt;

&lt;p&gt;An open catalog solves the portability problem. With Floe or Firn, you can start to solve the maintenance problem too — though both require more assembly than managed alternatives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lakehouse control plane (LakeOps)
&lt;/h3&gt;

&lt;p&gt;A control plane complements the storage and catalog layers by handling the operational lifecycle that neither S3 Tables nor open catalogs address on their 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flxrym771u4xfz1s5z8ct.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%2Flxrym771u4xfz1s5z8ct.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; connects to any Iceberg catalog — Glue, S3 Tables, Polaris, Nessie, Gravitino — and provides autonomous maintenance, observability, and optimization across the entire estate. The distinction from S3 Tables is that LakeOps works &lt;em&gt;with&lt;/em&gt; your existing infrastructure rather than replacing it, and provides the visibility and intelligence that managed services abstract away.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What this approach covers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-estate observability&lt;/strong&gt; — a single dashboard covers every table across every catalog (Glue, S3 Tables, REST) with severity-ranked insights. You see which tables are degrading before the impact reaches query dashboards or invoices.&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%2F33p20havfaznyywkifu4.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%2F33p20havfaznyywkifu4.png" alt=" " width="799" 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%2Fhufgakurwv90dk9264uq.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%2Fhufgakurwv90dk9264uq.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full maintenance sequence in correct dependency order&lt;/strong&gt; — snapshot expiration → orphan cleanup → compaction → manifest rewrite → statistics computation. Each step's output feeds the next, ensuring cleanup always precedes compaction and metadata optimization always follows it.&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%2Fdbceblqvwh5jxgh8xbrm.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%2Fdbceblqvwh5jxgh8xbrm.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query-aware compaction&lt;/strong&gt; — instead of blind binpack, the system analyzes which columns your queries actually filter, join, and group on, then physically sorts data to match those patterns. Engines skip entire file groups through min/max pruning, reducing I/O significantly. The sort strategy adapts as query patterns change.&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%2F7niq8bhc58yomnh07pp6.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%2F7niq8bhc58yomnh07pp6.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Purpose-built execution engine&lt;/strong&gt; — compaction runs on a native Rust/DataFusion runtime that bypasses the JVM stack entirely. &lt;a href="https://lakeops.dev/blog/iceberg-lake-compaction" rel="noopener noreferrer"&gt;Production benchmarks&lt;/a&gt; show 7× higher throughput than Spark and over 20× faster than S3 Tables' built-in compaction on equivalent datasets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Health-driven triggers, not cron schedules&lt;/strong&gt; — every table is scored along multiple structural dimensions (file fragmentation, delete-file depth, snapshot age, manifest bloat) and maintenance fires only when indicators cross actionable thresholds. Streaming tables might be compacted hourly, batch tables daily, and well-maintained tables are skipped entirely.&lt;/p&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%2Fk29wm5gj7fah7e0hwdtd.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%2Fk29wm5gj7fah7e0hwdtd.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Policy-driven governance&lt;/strong&gt; — maintenance rules defined once at the namespace or catalog level, with per-table overrides where needed. Every execution is logged with before/after metrics for auditability.&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%2Feweoerslr9e5asb4iupm.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%2Feweoerslr9e5asb4iupm.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a deeper look at how each capability works in practice, the &lt;a href="https://lakeops.dev/platform" rel="noopener noreferrer"&gt;LakeOps platform page&lt;/a&gt; walks through the full system from observability through compaction, routing, and governance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison: S3 Tables vs. alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;S3 Tables&lt;/th&gt;
&lt;th&gt;Self-Managed (Glue + S3)&lt;/th&gt;
&lt;th&gt;Open Catalog (Polaris)&lt;/th&gt;
&lt;th&gt;Control Plane (LakeOps)&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;Automatic, opaque&lt;/td&gt;
&lt;td&gt;Manual (Spark/Athena)&lt;/td&gt;
&lt;td&gt;Not included&lt;/td&gt;
&lt;td&gt;Autonomous, query-aware, Rust engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snapshot / orphan mgmt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatic (fails if tags/branches exist)&lt;/td&gt;
&lt;td&gt;Manual procedures&lt;/td&gt;
&lt;td&gt;Not included&lt;/td&gt;
&lt;td&gt;Autonomous, sequenced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Manifest rewrite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Not included&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Statistics (Puffin)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Not included&lt;/td&gt;
&lt;td&gt;Automatic post-compaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Engine support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Athena, EMR, Redshift, Spark, Trino, Flink, Snowflake, DuckDB via REST&lt;/td&gt;
&lt;td&gt;All Iceberg engines&lt;/td&gt;
&lt;td&gt;All Iceberg engines&lt;/td&gt;
&lt;td&gt;All engines + routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;dbt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supported (dbt-athena 1.11+, drop-recreate)&lt;/td&gt;
&lt;td&gt;Full production support&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lake Formation via Glue (caveats for external engines)&lt;/td&gt;
&lt;td&gt;Lake Formation (column, row, tag)&lt;/td&gt;
&lt;td&gt;RBAC (Polaris-native)&lt;/td&gt;
&lt;td&gt;Works with Lake Formation + operational policies&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;CloudWatch basics&lt;/td&gt;
&lt;td&gt;Whatever you build&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Health classification, insights, audit trails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-cloud&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AWS only&lt;/td&gt;
&lt;td&gt;Any cloud (with catalog choice)&lt;/td&gt;
&lt;td&gt;Any cloud&lt;/td&gt;
&lt;td&gt;Any cloud&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;Open format, proprietary storage layer, exit = data rewrite&lt;/td&gt;
&lt;td&gt;Standard S3, portable&lt;/td&gt;
&lt;td&gt;Fully open&lt;/td&gt;
&lt;td&gt;No lock-in, works with existing stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance control&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Config only (target size, strategy)&lt;/td&gt;
&lt;td&gt;Full control, full burden&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Policy-driven with full visibility&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What matters for your evaluation
&lt;/h2&gt;

&lt;p&gt;The right alternative depends on which S3 Tables constraint is actually blocking you.&lt;/p&gt;

&lt;h3&gt;
  
  
  If engine compatibility is the issue
&lt;/h3&gt;

&lt;p&gt;S3 Tables' engine compatibility has improved significantly — Spark, Trino, Flink, Snowflake, and DuckDB all connect through the REST endpoint. But if you need standard dbt table-swap semantics, full &lt;code&gt;CREATE VIEW&lt;/code&gt; support, or CTAS via &lt;code&gt;stage-create&lt;/code&gt;, self-managed Iceberg on standard S3 with Glue Data Catalog remains the straightforward path. Every engine works without adapter caveats. The trade-off is owning the maintenance lifecycle, which is well-understood and automatable — but requires sustained engineering investment.&lt;/p&gt;

&lt;h3&gt;
  
  
  If vendor lock-in is the concern
&lt;/h3&gt;

&lt;p&gt;An open REST catalog like Apache Polaris gives you a vendor-neutral metadata layer that works on any cloud and any engine. You still need to solve maintenance separately. For teams that already have mature Spark-based maintenance pipelines and just want catalog portability, this can be the right layer to change.&lt;/p&gt;

&lt;h3&gt;
  
  
  If operational complexity is the bottleneck
&lt;/h3&gt;

&lt;p&gt;This is where most teams land after running Iceberg in production for more than a few months. The problem is not any single capability — it is the combinatorial complexity of managing compaction schedules, monitoring health across hundreds of tables, keeping sort orders aligned with evolving query patterns, and ensuring the maintenance sequence runs correctly every time across different ingestion cadences.&lt;/p&gt;

&lt;p&gt;S3 Tables addresses this by abstracting the problem away behind a black box. The control plane approach addresses it by automating the same operations with &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;full visibility and policy governance&lt;/a&gt; — plus the query-aware optimization and cross-engine telemetry that no black-box service can provide, because they require understanding how your tables are actually queried.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you are already using S3 Tables
&lt;/h3&gt;

&lt;p&gt;LakeOps connects to S3 Tables as a catalog source alongside Glue and other catalogs. You do not have to migrate off S3 Tables to get cross-estate observability, health classification, or policy governance. For tables that work well in S3 Tables — high-throughput streaming writes that benefit from managed compaction and higher TPS, AWS-native stacks where the Glue federation provides sufficient engine access — you can keep them there. For tables that need standard dbt semantics, view support, or unrestricted Lake Formation enforcement across external engines, those live in standard S3 with Glue. &lt;a href="https://lakeops.dev/blog/s3-tables-vs-self-managed-iceberg" rel="noopener noreferrer"&gt;LakeOps provides the unified operational layer above both&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The maintenance gap is the real problem
&lt;/h2&gt;

&lt;p&gt;The deeper pattern behind the "S3 Tables alternative" search is not really about S3 Tables at all. It is about the gap between Iceberg's capabilities as a format and what it takes to operate Iceberg well in production.&lt;/p&gt;

&lt;p&gt;Iceberg gives you ACID transactions, time travel, schema evolution, and engine-agnostic access. It does not give you a system that keeps tables healthy as data flows in. That requires compaction, cleanup, manifest optimization, statistics computation, health monitoring, and policy enforcement — coordinated correctly across every table in the estate.&lt;/p&gt;

&lt;p&gt;S3 Tables fills part of this gap — and it fills it well for the use cases it targets. If you are building an AWS-native lakehouse with Athena and Redshift, want zero-ops maintenance, and can work within the DDL and view restrictions, S3 Tables is a strong starting point. The ecosystem has improved steadily since launch: Lake Formation integration, Glue federation, dbt support, and broader engine compatibility have addressed several early limitations.&lt;/p&gt;

&lt;p&gt;But many production estates outgrow the managed-service model. They run multiple engines. They need views and standard dbt semantics. They need to understand what maintenance did and why. They need policies that work across catalogs, and observability that covers the entire lake — not just one storage layer.&lt;/p&gt;

&lt;p&gt;Open catalogs like Apache Polaris fill the metadata portability piece. Emerging tools like Floe and Firn add open-source maintenance automation. Self-managed deployments give maximum control at the cost of operational burden.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://lakeops.dev/blog/managed-iceberg-2026" rel="noopener noreferrer"&gt;lakehouse control plane&lt;/a&gt; fills the gap end to end: the full maintenance lifecycle with correct sequencing, the observability to know what is happening, the policy layer to govern it at scale, and the execution engine to do it efficiently — across every catalog type, every engine, and every cloud. That is the architectural layer the Iceberg ecosystem has been missing, and it is the one most teams end up needing regardless of where their data physically sits.&lt;/p&gt;

&lt;p&gt;If you are evaluating alternatives to S3 Tables, start by clarifying which constraint matters most — portability, engine support, security, or operational maturity. Then pick the combination of storage, catalog, and operational layers that matches. &lt;a href="https://lakeops.dev/" rel="noopener noreferrer"&gt;LakeOps&lt;/a&gt; handles the operational layer above any combination of the others.&lt;/p&gt;

</description>
      <category>iceberg</category>
      <category>aws</category>
      <category>datalakehouse</category>
      <category>dataengineering</category>
    </item>
    <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>
  </channel>
</rss>
