<- Back to all posts
December 10, 2025

Unlocking Faster Iceberg Queries: The Writer Optimizations You're Missing

No items found.

Listen to this article

Powered by NotebookLM
Listen to this article

Unlocking Faster Iceberg Queries: The Writer Optimizations You're Missing

The hard truth: Your Apache Iceberg tables might be slow not because of your query engine, but because of how your data was written in the first place. Getting maximum performance requires three things working together: optimized writes that avoid files that are small or uncompressed, continuous table maintenance, and a query engine that leverages the resulting metadata. Here's how to get all three right.

The performance trap hiding in your data lake

Teams migrating to Apache Iceberg often hit a wall. After adopting the open table format, setting up their catalog, and running their first queries, performance is... underwhelming. Queries that should take seconds take minutes. Scans that should be selective end up reading entire partitions.

The instinct is to blame the query engine. "Maybe we need more compute." "Perhaps we should try a different execution framework." But the real culprit is usually much simpler: the Parquet files underlying your Iceberg tables were never configured for fast analytical queries.

This is the small files problem, reborn. Data engineers who lived through the Hadoop era remember the nightmare: MapReduce jobs crawling because HDFS was littered with thousands of tiny files. Each file open was a metadata operation. Each read was a new I/O request. What should have been parallel processing became serialized overhead.

Apache Iceberg solves many problems from the Hadoop days with ACID transactions, hidden partitioning, and schema evolution. But it doesn't solve the small files problem. In fact, it can make it worse. Streaming ingestion creates thousands of small files per hour. Micro-batch writes fragment data across partitions. CDC pipelines write deletes faster than compaction can consolidate them.

The result? Tables with perfect ACID semantics and terrible query performance. Firebolt's query engine can only optimize what the metadata layer exposes, and when that metadata describes thousands of small, unsorted files with overlapping ranges, even the smartest planner has no leverage.

This isn't a hypothetical problem. When data teams write Iceberg tables using default Spark, Flink, or ingestion pipeline configurations, they unknowingly create data layouts optimized for batch processing, not interactive analytics. Large row groups (256MB+), unsorted data, massive files (1GB+), and missing statistics all combine to create tables that look fine on paper but perform terribly in production.

At Firebolt, we see this pattern constantly: teams excited about Iceberg's ACID guarantees and vendor independence, frustrated by query performance that doesn't match their expectations. The good news? The solution doesn't require changing your architecture, migrating data, or switching query engines.

The trifecta for Iceberg performance success:

  1. Optimized writes following best practices: Configure your Spark, Flink, or ingestion pipelines to write well-structured Parquet files from day one, avoiding the small files problem entirely
  2. Continuous optimization with Ryft: Let intelligent automation handle compaction, sorting, and file layout as your tables grow and usage patterns evolve
  3. Fast queries with Firebolt (or any engine): Leverage the perfectly optimized metadata and file structures for maximum query performance

Here's the critical insight: the first two steps benefit every query engine you might use. Properly configured writes and Ryft's optimizations improve performance whether you're querying with Firebolt, Athena, Trino, Spark SQL, or any other engine that reads Iceberg. This is the power of open table formats: optimize once, query anywhere. Firebolt just happens to be exceptionally fast at leveraging that optimized structure for sub-second interactive queries.

Why writer configuration matters more than you think

Apache Iceberg's power comes from its rich metadata layer. File-level statistics, partition information, and manifest files enable query engines to prune irrelevant data before reading a single byte. But here's what most teams miss: that metadata can only be as good as what the writer produces.

When a Spark job writes a Parquet file with a 512MB row group containing unsorted data, Iceberg can tell the query engine "this file contains rows with timestamps between January 1 and January 31." That's it. The query engine, whether it's Firebolt, Athena, Trino, or any other, must read the entire 512MB row group to find the specific hour you're filtering for.

Compare that to a properly configured writer that creates 100,000-row groups sorted by timestamp. Now Iceberg's metadata says "row group 1 has timestamps 00:00-00:15, row group 2 has 00:15-00:30," and so on. Any query engine can now read only the exact row groups needed, skipping 95% of the data. Same logical table, same data, 20x performance improvement across any query engine you choose.

This is the fundamental lesson: query engines, including Firebolt's, can only optimize based on the metadata available. When that metadata is coarse-grained (huge row groups, no sorting, small files everywhere), even the smartest query planner has no leverage. When it's fine-grained (right-sized row groups, sorted data, optimally sized files), any query engine can work miracles.

This is engine-agnostic optimization. Following Firebolt's writer configuration guidance doesn't lock you into Firebolt. It creates well-structured Iceberg tables that perform better in Athena, Trino, Spark SQL, Databricks, Snowflake, or any other engine you choose to query with. The beauty of open table formats is that optimization lifts all boats.

The three dimensions of writer optimization

Getting Iceberg write performance right requires balancing three interconnected factors that directly impact how Firebolt's query engine can optimize execution:

1. Row group sizing for effective pruning

Parquet organizes data into row groups, and these row groups are the atomic unit for most query engines. Once you decide to read a row group, you typically read the whole thing.

The trap of default settings: Most Spark configurations default to 128MB-256MB row groups. For batch ETL processing gigabytes of data, this is fine. For interactive queries filtering to specific time ranges, customers, or geographic regions, it's disastrous. You end up reading megabytes of data to extract kilobytes of results.

The Firebolt optimization: For interactive workloads, target row groups between 100,000 and 1,000,000 rows. This typically translates to 10-50MB row groups depending on your schema width. This specific range is critical because:

  • Small enough for granular pruning: Query engines can skip irrelevant row groups without reading them
  • Large enough for compression: Parquet's encodings and compression still work effectively at this scale
  • Aligned with typical filter selectivity: Most analytical queries filter to 1-10% of table rows
  • Optimal for Firebolt's vectorized execution: Row groups in this range align with Firebolt's batch processing model

Think of row groups as the "granules" in Firebolt's native tables. We optimize native storage for 8,192-row granules because that's the sweet spot for our pruning algorithms. The same principle applies to Iceberg: finer-grained metadata enables more aggressive pruning. A row group size between 100,000 and 1,000,000 rows generally allows both for effective compression and good pruning.

File sizing matters too: Include enough row groups per file to achieve file sizes of at least 50MB, with bigger files preferred for very large tables. This balances metadata overhead (fewer files to track in manifest files) with pruning effectiveness (enough row groups per file for selective reads).

2. Data sorting within partitions

Unsorted data is the silent killer of Iceberg query performance. When rows are randomly distributed within files, column statistics (min/max values) become useless. A file with event_time ranging from "2024-01-01 00:00:00" to "2024-01-31 23:59:59" can't be pruned even when you're querying for a single hour.

Sort by your filter columns: The single most impactful optimization is sorting data by columns frequently used in WHERE clauses. This mirrors how primary indexes work on Firebolt's managed tables. For time-series data, sort by timestamp. For user analytics, sort by user_id. For geographic queries, sort by region. Sorting the data within each partition by fields that are often used for filtering is highly recommended to allow for effective pruning.

This creates non-overlapping ranges within row groups, dramatically improving statistics. Instead of every row group potentially containing every timestamp, each row group has a narrow, non-overlapping time window. Firebolt can now prune at the row group level with surgical precision.

The multiplier effect: Sorting doesn't just help with direct filters. It improves:

  • Aggregation efficiency: Sorted data enables streaming aggregations without full materialization
  • Compression and ordering strategies: Sorted columns compress better, reducing storage costs and I/O. Single-column sorting works well for queries with predictable access patterns, while Z-ordering or Hilbert ordering can optimize for multi-dimensional queries common in lakehouses.
  • Co-located operations: When aggregation keys match partition keys, Firebolt can run entirely local aggregations

Think of sorting as creating an implicit index. Firebolt's native tables use primary indexes for exactly this reason: sorted data enables order-of-magnitude performance improvements.

Important configuration distinction: The optimal configuration depends on your use case. For batch processing, large files with fairly large row groups minimize overhead. For interactive use cases (what Firebolt excels at), smaller files with many row groups and sorted data allow for better pruning. This is why understanding your workload pattern is critical before configuring writers.

3. File sizing for metadata efficiency

File size has a nonlinear impact on query performance. Too small, and you incur overhead from opening thousands of files. Too large, and you read unnecessary data even after row group pruning.

The metadata overhead problem: Each Iceberg data file requires entries in manifest files. Tables with millions of tiny files create enormous manifest files that slow query planning. Every query must process this metadata before executing.

The right-sizing formula: Target files of at least 50MB, with larger files (128MB-512MB) for very large tables. The key is balancing:

  • Enough row groups per file: Aim for 5-10 row groups per file minimum to enable intra-file pruning
  • Not so large that pruning loses effectiveness: A 2GB file with 100MB row groups still forces reading 100MB chunks
  • Aligned with partition selectivity: If queries typically access 1% of partitions, files can be larger

This is why Firebolt's documentation recommends 50MB minimum file sizes with preference for larger files on bigger tables. It's the sweet spot where metadata overhead is minimal but pruning remains effective.

Practical configuration for Spark and Flink writers

Let's translate these principles into actual configuration. These settings apply regardless of whether you're using Firebolt, Athena, Trino, or any other query engine for reads.

Apache Spark configuration

For Spark jobs writing to Iceberg, add these configurations to optimize for analytical workloads:

spark.conf.set("spark.sql.files.maxRecordsPerFile", 100000000)  # ~100M rows per file
spark.conf.set("spark.sql.parquet.row.group.size.bytes", 134217728)  # 128MB row groups

# For time-series or frequently filtered columns
spark.conf.set("spark.sql.sources.sortByKey.enabled", "true")

# Write with sorting (Iceberg-specific)
spark.sql("""
    CREATE TABLE my_catalog.my_db.events (
        event_time TIMESTAMP,
        user_id BIGINT,
        event_type STRING,
        properties MAP<STRING, STRING>
    )
    USING iceberg
    PARTITIONED BY (days(event_time))
    TBLPROPERTIES (
        'write.target-file-size-bytes' = '134217728',  -- 128MB target files
        'write.parquet.row-group-size-bytes' = '16777216',  -- 16MB row groups
        'write.parquet.page-size-bytes' = '1048576',  -- 1MB pages
        'write.metadata.compression-codec' = 'zstd',
        'write.parquet.compression-codec' = 'zstd'
    )
""")

# Insert with explicit sorting
spark.sql("""
    INSERT INTO my_catalog.my_db.events
    SELECT * FROM source_table
    ORDER BY event_time, user_id
""")

Apache Flink configuration

For Flink streaming jobs:

TableEnvironment tableEnv = TableEnvironment.create(settings);

// Configure Iceberg catalog
tableEnv.executeSql(
    "CREATE CATALOG my_catalog WITH (" +
    "'type'='iceberg'," +
    "'catalog-type'='hive'," +
    "'uri'='thrift://localhost:9083'," +
    "'warehouse'='s3://my-bucket/warehouse'" +
    ")"
);

// Create table with optimized properties
tableEnv.executeSql(
    "CREATE TABLE my_catalog.my_db.events (" +
    "    event_time TIMESTAMP(6)," +
    "    user_id BIGINT," +
    "    event_type STRING," +
    "    properties MAP<STRING, STRING>" +
    ") PARTITIONED BY (DATE_FORMAT(event_time, 'yyyy-MM-dd'))" +
    "WITH (" +
    "    'write.target-file-size-bytes' = '134217728'," +  // 128MB
    "    'write.parquet.row-group-size-bytes' = '16777216'," +  // 16MB
    "    'write.parquet.compression-codec' = 'zstd'" +
    ")"
);

Key configuration parameters explained

write.target-file-size-bytes: Controls overall file size. Set to 128MB-512MB depending on table scale. Smaller tables (less than 100GB) use 128MB, larger tables can go higher.

write.parquet.row-group-size-bytes: The critical parameter for pruning effectiveness. Set to 16MB-32MB for interactive workloads. This creates row groups of 100,000-500,000 rows for typical schemas.

write.parquet.page-size-bytes: Parquet page size within row groups. Default 1MB is usually fine. Smaller pages (256KB-512KB) can help with very selective queries but add overhead.

write.metadata.compression-codec: Use 'zstd' for best compression of Iceberg metadata files. Better compression = faster query planning.

write.parquet.compression-codec: Use 'zstd' for data compression. Better compression ratio than 'snappy' with similar performance in Firebolt's vectorized engine.

The sorting imperative: Creating non-overlapping ranges

Configuration alone isn't enough. You must explicitly sort data during writes to create the non-overlapping ranges that enable effective pruning.

The wrong way: unsorted writes

# This creates randomly distributed data within partitions
df.write \
    .format("iceberg") \
    .mode("append") \
    .save("my_catalog.my_db.events")

Result: Every row group in a partition has overlapping ranges for all columns. Filtering by timestamp, user_id, or any other column requires reading most row groups.

The right way: sorted writes

# Sort by primary filter columns before writing
df.orderBy("event_time", "user_id") \
    .write \
    .format("iceberg") \
    .mode("append") \
    .save("my_catalog.my_db.events")

Result: Row groups have non-overlapping timestamp and user_id ranges. Filtering by either column enables aggressive pruning.

Advanced: Z-ordering for multi-dimensional queries

When queries filter on multiple independent dimensions, Z-ordering can outperform single-column sorting:

from pyspark.sql.functions import col

# Z-order by multiple dimensions
df.repartition(col("partition_date")) \
    .sortWithinPartitions(
        col("event_time"),
        col("user_id"),
        col("event_type")
    ) \
    .write \
    .format("iceberg") \
    .mode("append") \
    .save("my_catalog.my_db.events")

Z-ordering creates a space-filling curve that preserves locality across multiple dimensions. When users query by event_time OR user_id OR event_type, Z-ordering provides better average-case pruning than single-column sorting.

Partitioning and co-located operations: The hidden performance multiplier

Beyond file-level optimizations, how you partition Iceberg tables has profound implications for join and aggregation performance in Firebolt. This is where understanding your query patterns becomes critical.

Co-located joins eliminate data shuffling

When Iceberg tables share compatible partitioning schemes, Firebolt can leverage this information to perform co-located joins, eliminating data shuffling and dramatically speeding up join operations.

The traditional approach: Most query engines shuffle data across nodes during joins, moving gigabytes over the network. Even with fast networking, this shuffle dominates query time for large joins.

The co-located optimization: When both tables are bucketed on the join key, Firebolt can distribute entire partitions to nodes and run the join fully locally. No network shuffle, no coordination overhead, just local hash joins at memory speed.

Example scenario:

-- Both tables partitioned by user_id
CREATE TABLE user_events (
    user_id BIGINT,
    event_time TIMESTAMP,
    event_type STRING
) USING iceberg
PARTITIONED BY (bucket(1000, user_id));

CREATE TABLE user_profiles (
    user_id BIGINT,
    account_type STRING,
    registration_date DATE
) USING iceberg
PARTITIONED BY (bucket(1000, user_id));

-- This join runs entirely co-located
SELECT 
    e.user_id,
    e.event_type,
    p.account_type,
    COUNT(*) as event_count
FROM user_events e
JOIN user_profiles p ON e.user_id = p.user_id
WHERE e.event_time >= CURRENT_DATE - 7
GROUP BY 1, 2, 3;

When you enable co-located joins with enable_iceberg_partitioned_scan, Firebolt analyzes the partition schemes and determines that both tables are bucketed identically on user_id. Instead of shuffling data, it sends partition 0 of both tables to node 0, partition 1 to node 1, and so on. Each node performs a local hash join on its partition.

The performance impact: For large joins (billions of rows), co-located execution can deliver 5-10x speedups by eliminating network bottlenecks.

Co-located aggregations eliminate global stages

Similar to joins, if an aggregation key matches the Iceberg table's partitioning key, Firebolt can optimize the aggregation process, potentially making local aggregations more efficient and even eliminating entire global aggregation stages.

The traditional approach: Aggregations require two stages: local pre-aggregation on each node, then global aggregation to combine results. This global stage requires shuffling partial aggregates and can become a bottleneck.

The co-located optimization: When the GROUP BY key matches the partition key, each partition contains a complete set of rows for specific key values. Firebolt can run local aggregations that produce final results, eliminating the global aggregation stage entirely.

Example scenario:

-- Table partitioned by region
CREATE TABLE sales_events (
    region STRING,
    product_id BIGINT,
    sale_amount DECIMAL(10,2),
    sale_time TIMESTAMP
) USING iceberg
PARTITIONED BY (region);

-- This aggregation runs entirely locally per node
SELECT 
    region,
    SUM(sale_amount) as total_sales,
    COUNT(*) as sale_count,
    AVG(sale_amount) as avg_sale
FROM sales_events
WHERE sale_time >= CURRENT_DATE - 30
GROUP BY region;

Because each partition contains all data for a specific region, nodes can compute final aggregates locally. No shuffle, no global merge, just local computation followed by result collection.

When to use partitioned scans: Use the enable_iceberg_partitioned_scan setting to enable these optimizations when:

  • The number of partitions is high enough (typically 100+) to distribute across nodes
  • Partition sizes are sufficiently balanced (no single partition dominates)
  • Your queries frequently join or aggregate on the partition key

Configuration example:

-- Enable co-located operations for specific queries
SET enable_iceberg_partitioned_scan = true;

-- Or set as default for your engine
ALTER ENGINE my_engine 
SET enable_iceberg_partitioned_scan = true;

The key insight: partitioning isn't just about organizing data for filter predicates. When aligned with join and aggregation keys, it fundamentally changes query execution strategy from distributed coordination to embarrassingly parallel local computation.

Continuous optimization with table maintenance

Even with perfect writer configuration, Iceberg tables degrade over time. This is the small files problem manifesting in the modern data lake: streaming ingestion creates small files, updates create delete files, and schema evolution can fragment data. Without continuous maintenance, query performance degrades linearly with table age, just like it did in the Hadoop era.

The compaction problem: Small files never stop accumulating

Streaming workloads write data as it arrives, often creating thousands of small files per hour. Each file incurs planning overhead and reduces parallelism effectiveness. Without compaction, query performance degrades linearly with ingestion duration.

The scale of the problem: A streaming pipeline writing 10MB files every minute creates 1,440 files per day, 43,200 files per month. Within a quarter, a single table can have over 100,000 small files. Each query must:

  • Load and parse manifest files referencing all those files
  • Open each file for metadata extraction
  • Coordinate reads across thousands of small objects in S3
  • Track partial results from each file during aggregation

This is why the Hadoop community invented compaction in the first place. The problem hasn't gone away, it's just moved from HDFS to object storage and from Hive to Iceberg.

Firebolt's approach: We recommend regular compaction (daily or weekly depending on ingestion rate) to consolidate small files into optimally sized files. But there's a catch: naive compaction just creates bigger files with the same internal problems. You need compaction that:

  1. Consolidates small files into right-sized files (128MB-512MB)
  2. Maintains or improves data sorting within files
  3. Rebuilds statistics with tighter bounds
  4. Reconciles delete files for merge-on-read tables

Manual compaction doesn't scale beyond a handful of tables. You need intelligent, continuous optimization.

Automated optimization with Ryft: Solving small files at scale

Manual table maintenance doesn't scale. When you're managing hundreds of Iceberg tables with different ingestion patterns, query patterns, and data characteristics, manual tuning becomes impossible. This is where the small files problem becomes an operational crisis, not just a performance issue.

Ryft is an intelligent Iceberg management platform that continuously optimizes tables based on actual usage patterns. Rather than applying generic compaction schedules, Ryft learns from your workload and adapts:

  • Monitors query patterns: Identifies which columns are frequently filtered and optimizes file sorting accordingly
  • Adapts to ingestion rates: Streaming tables get frequent lightweight compaction to prevent small files from accumulating, batch tables get periodic deep optimization
  • Reduces commit conflicts: Smart scheduling ensures compaction doesn't interfere with ingestion pipelines by 99%, eliminating the coordination problems that plagued Hadoop-era compaction
  • Maintains compliance: Automated retention and deletion policies for GDPR/CCPA compliance
  • Learns workload characteristics: Understands whether tables are used for streaming analytics, batch reporting, or mixed workloads, and optimizes accordingly

The Firebolt + Ryft architecture for maximum performance:

  1. Write correctly from the start: Use the Spark and Flink configurations in this post (derived from Firebolt's documentation) to create well-structured Parquet files with proper row group sizes, sorting, and statistics. These best practices benefit any query engine.
  2. Let Ryft optimize continuously: As data accumulates, Ryft automatically compacts small files, re-sorts based on actual query patterns, and maintains optimal file layouts without manual intervention.
  3. Query fast with any engine: The optimized table structure improves performance across all query engines. Firebolt's vectorized execution and aggressive metadata pruning deliver sub-second queries, but Athena, Trino, Spark SQL, and other engines also benefit significantly.

The key insight: This isn't vendor lock-in disguised as optimization. The writer configurations and Ryft optimizations we're describing are engine-agnostic best practices. They make your Iceberg tables faster for everyone. Firebolt benefits from these optimizations, but so does every other query engine in the ecosystem.

This is the promise of open table formats: best-in-class tools working together without vendor lock-in. Write data once with optimal configurations, let Ryft handle the ongoing small files problem automatically, query with whatever engine fits your workload best. The trifecta of optimized writes, continuous optimization, and fast query engines delivers consistent performance regardless of which query engine you choose.

Why this matters: The small files problem has plagued large-scale data systems since the Hadoop era. Manual compaction schedules, brittle coordination between writers and compactors, and performance degradation over time were accepted as the cost of doing business.

With the trifecta of optimized writes (following Firebolt's documentation guidance), Ryft's intelligent continuous optimization, and fast query engines, you finally solve small files permanently. And here's the critical point: these first two steps are engine-agnostic. Proper writer configuration and Ryft optimization benefit every query engine in the ecosystem, whether you're running Athena, Trino, Spark SQL, Databricks, or Firebolt.

Write correctly from day one to avoid small files, optimize automatically with Ryft to maintain performance as tables grow, and query fast with any engine that understands Iceberg metadata. Firebolt just happens to be exceptionally good at leveraging that optimized structure for sub-second interactive queries.

Performance impact: Real-world results

Let's quantify what proper writer configuration delivers. These numbers come from production Firebolt deployments querying Iceberg tables with different write configurations. Important note: These optimizations are engine-agnostic. While we're showing Firebolt's performance, properly configured Iceberg tables deliver significant improvements across all query engines, whether you're using Athena, Trino, Spark SQL, or others.

Benchmark: Time-series event data

Dataset: 500GB Iceberg table, 2 billion events, partitioned by day, queried by hour ranges

Configuration A (default Spark, small files problem):

  • File size: 10-50MB per file (thousands of small files)
  • Row group size: 256MB
  • Unsorted data
  • Firebolt query performance: 8.2 seconds (p50), 12.1 seconds (p95)

Configuration B (optimized per Firebolt docs, no small files):

  • File size: 256MB per file (consolidated, optimal count)
  • Row group size: 16MB
  • Sorted by timestamp
  • Firebolt query performance: 0.4 seconds (p50), 0.7 seconds (p95)

Performance improvement: 20x faster queries from writer optimization alone, no changes to compute resources. This same file structure would similarly improve performance in Athena, Trino, and other engines.

Benchmark: User analytics table

Dataset: 1TB Iceberg table, 5 billion user events, partitioned by week, queries filter by user_id

Configuration A (default Flink streaming, small files everywhere):

  • File size: Small files (10-50MB, tens of thousands of files)
  • Row group size: 128MB
  • Random user_id distribution
  • Firebolt query performance: 15.3 seconds (p50), 28.7 seconds (p95)

Configuration B (optimized with Ryft compaction, small files eliminated):

  • File size: 256MB per file after Ryft compaction
  • Row group size: 32MB
  • Sorted by user_id via Ryft optimization
  • Firebolt query performance: 0.9 seconds (p50), 1.8 seconds (p95)

Performance improvement: 17x faster queries from proper file sizing and sorting.

The pattern is consistent: when Iceberg tables are written with best practices (avoiding small files, proper row group sizing, sorting), query engines can deliver exceptional performance. When tables are written with default configurations that create small files, performance suffers across all engines regardless of compute power. These optimizations are universal: they benefit Firebolt, Athena, Trino, Spark SQL, Databricks, and any other engine querying your Iceberg tables.

Implementation roadmap: Getting it right from day one

Here's how to implement writer optimization across your Iceberg data pipeline:

Phase 1: Audit current configuration (Week 1)

  1. Check writer settings: Review Spark/Flink configurations for file size and row group size parameters
  2. Analyze file patterns: Query Iceberg metadata tables to see actual file sizes and row group counts
  3. Measure query performance: Baseline current query latencies on representative workloads
  4. Identify sort candidates: Determine which columns are most frequently filtered in queries
-- Check file sizes and row group counts
SELECT 
    AVG(file_size_in_bytes) / 1024 / 1024 as avg_file_size_mb,
    AVG(record_count) as avg_rows_per_file,
    COUNT(*) as total_files
FROM my_catalog.my_db.events.files;

-- Check row group statistics (requires Parquet metadata queries)
SELECT 
    file_path,
    row_group_id,
    num_rows,
    total_byte_size / 1024 / 1024 as size_mb
FROM my_catalog.my_db.events.parquet_metadata
LIMIT 100;

Phase 2: Optimize new writes (Week 2-3)

  1. Update writer configurations: Apply the Spark/Flink settings from earlier in this post
  2. Add explicit sorting: Include ORDER BY clauses in INSERT statements for primary filter columns
  3. Test on development tables: Validate performance improvements on dev/staging environments
  4. Roll out to production: Update production ingestion pipelines with new configurations

Phase 3: Compact existing tables (Week 4+)

  1. Schedule initial compaction: Rewrite existing table data with optimized configurations
  2. Set up regular maintenance: Configure weekly compaction jobs or deploy Ryft for automated optimization
  3. Monitor performance: Track query latencies and data statistics to validate improvements
  4. Iterate on sorting: Adjust sort columns based on actual query patterns

Phase 4: Continuous optimization (Ongoing)

  1. Enable Ryft integration: Let Ryft continuously learn and optimize based on usage patterns
  2. Monitor commit conflicts: Ensure compaction doesn't interfere with ingestion
  3. Review statistics: Periodically check that file sizes and row group counts remain optimal
  4. Adapt to workload changes: Adjust configurations as query patterns evolve

The architectural advantage: Finally solving small files

The beauty of Apache Iceberg is that writer optimization and query engine performance are independent concerns, but they work together to solve the small files problem that has plagued data systems for over a decade. The trifecta approach delivers benefits universally:

1. Optimized writes (engine-agnostic): Writing data correctly from the start, following best practices in Firebolt's documentation, avoids the small files problem entirely. This benefits every query engine.

2. Continuous optimization with Ryft (engine-agnostic): Ryft's intelligent automation maintains optimal file layouts as tables grow. This also benefits every query engine, whether you're querying with Athena, Trino, Spark SQL, Databricks, or Firebolt.

3. Fast queries with your chosen engine: With perfectly optimized table structures, any query engine performs significantly better. Firebolt's vectorized execution and aggressive metadata pruning deliver sub-second queries, but the improved metadata and file structures accelerate all engines.

This separation enables best-in-class tools for each workload. Spark's distributed processing for ETL, Flink's streaming semantics for real-time ingestion, Ryft's intelligent maintenance for continuous optimization, and your choice of query engine for analytics, whether that's Firebolt for sub-second interactive queries or another engine for batch processing.

No vendor lock-in: Your Iceberg tables remain open and portable. The optimizations we've discussed are standard best practices that benefit any query engine that understands Parquet statistics and Iceberg metadata. You're not optimizing for Firebolt specifically; you're optimizing for Iceberg generally. Firebolt just happens to be exceptionally good at leveraging that metadata for fast queries.

No data duplication: Unlike hybrid architectures that ingest into Iceberg then copy to a proprietary format, you maintain a single copy of data. Write once with proper configuration following best practices, let Ryft optimize continuously, query with whatever engine makes sense for each workload.

No operational complexity: With proper writer configuration (no small files from day one) and tools like Ryft (preventing small files from accumulating), Iceberg tables stay optimized automatically. The small files problem that required manual intervention and degraded performance in Hadoop is now solved through intelligent automation. No manual intervention, no degrading performance, no maintenance windows, and no lock-in to any specific query engine.

The complete solution: Proper writer configuration from this guide (following Firebolt's documentation) creates well-structured data from the start. Ryft's continuous optimization prevents the small files problem from ever emerging. Any query engine can now deliver great performance on perfectly optimized tables. This trifecta works whether you're querying with Firebolt, Athena, Trino, Spark SQL, Databricks, or any other engine. The first two steps are universal optimizations; the third step is choosing the right engine for your workload.

Learn more and get started

Ready to unlock 10x faster queries on your Iceberg tables? Here's how to dive deeper:

📖 Technical deep dive: Read Firebolt's documentation on configuring Iceberg writers for best performance, including detailed parameter explanations and schema design patterns: Iceberg Writer Configuration Guide.

🔬 Engineering deep dive: For the technical details on how Firebolt's query engine leverages Iceberg metadata for aggressive pruning, read our engineering blog post on READ_ICEBERG internals: Querying Apache Iceberg with Sub-Second Performance

🚀 Try Firebolt on your Iceberg tables: Get $200 in credits to test Firebolt's Iceberg performance on your own workloads. Connect your AWS Glue catalog, run your queries, and see the difference writer optimization makes: firebolt.io/signup

🤝 Explore Ryft: Learn how Ryft's intelligent Iceberg management platform automatically maintains and optimizes your tables based on usage patterns: ryft.io

The future of data lakes is open, optimized, and fast. With the trifecta of proper writer configuration (following best practices from Firebolt's documentation), continuous table maintenance from Ryft, and fast query engines, Apache Iceberg delivers the best of both worlds: data lake economics with data warehouse performance. The small files problem that defined the Hadoop era is finally solved, and the solution benefits every query engine in the ecosystem, not just Firebolt.

Questions? Join us on our Discord or contact our team to discuss optimizing your Iceberg tables for Firebolt, or take a look at our demo.

To all posts

Intrigued? Want to read some more?