iceberg-best-practices
Skill2 starsChanged 8 months agoApache-2.0View on GitHub
Apache Iceberg best practices for table design, partitioning, compaction, catalog configuration, and multi-engine lakehouse management
In this repository5 other files it gives its agents.
The file
Show its rules and text
We split this skill into the rules it gives an agent
25 rules. About 0 are common, 20 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈20
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 10
Use hidden partitioning with transforms — never create explicit partition columns
- Distinctive: line 11
Run table maintenance regularly — expire snapshots, compact files, rewrite manifests
- Distinctive: line 12
Use Parquet with ZSTD compression for all data files
- Distinctive: line 14
Use a production catalog (REST, Glue, or Hive Metastore) — never Hadoop catalog
- Distinctive: line 24
Every column gets a unique ID.
- Distinctive: line 55
Add new columns as optional with documented defaults
- Very specific: line 56
Use COMMENT ON COLUMN to track field semantics
- Distinctive: line 58
Test schema changes in a branch or test environment before production
- Distinctive: line 98
Avoid more than ~10,000 total partitions (metadata overhead)
- Distinctive: line 99
Avoid fewer than ~10 partitions (limits parallelism)
- Distinctive: line 100
If partitions produce files < 25 MB, use coarser partitioning or compaction
- Distinctive: line 101
If partitions produce files > 2 GB, use finer partitioning
- Distinctive: line 136
Put low-cardinality filter columns first
- Distinctive: line 235
Never below 32 MB (too many small files) or above 2 GB (too little parallelism)
- Distinctive: line 289
Use explicit equality and range predicates.
- Very specific: line 378
Run binpack daily/hourly. Run sort weekly during off-peak hours.
- Very specific: line 402
Use older_than > max expected write duration to avoid deleting in-progress writes (default: 3 days).
- Distinctive: line 440
Does not delete data files — the rolled-back-to snapshot's files are still present.
- Distinctive: line 486
Critical for MoR: Run compaction regularly to merge delete files into base data files.
- Very specific: line 502
Use Spark for all maintenance procedures (rewrite_data_files, expire_snapshots, etc.)
- Very specific: line 504
Use DataFrame.writeTo("catalog.db.table").append() or .overwritePartitions() for writes
- Distinctive: line 521
Does not support all maintenance procedures — use Spark for full maintenance
- Distinctive: line 528
All engines read/write the same Iceberg format — true interoperability
- Distinctive: line 529
Ensure all engines use compatible Iceberg library versions
- Distinctive: line 530
Avoid engine-specific table properties when multi-engine access is needed
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Apache Iceberg Best Practices
Core Principles
- Use hidden partitioning with transforms — never create explicit partition columns
- Run table maintenance regularly — expire snapshots, compact files, rewrite manifests
- Use Parquet with ZSTD compression for all data files
- Configure sort order on frequently filtered columns
- Use a production catalog (REST, Glue, or Hive Metastore) — never Hadoop catalog
- Set state TTL / snapshot retention to prevent unbounded metadata growth
- Design for schema evolution from day one — Iceberg tracks columns by ID, not name
Table Design and Schema Evolution (CRITICAL)
schema-evolution-safety
Iceberg schema evolution is safe — use it instead of recreating tables.
Every column gets a unique ID. Renaming, reordering, adding, or dropping columns are metadata-only operations that never rewrite data files.
Safe operations (metadata-only):
- Add optional column
- Drop column (data files untouched; column excluded from reads)
- Rename column (tracked by ID — historical data readable under new name)
- Reorder columns
- Widen type (
int->long,float->double,decimal(P,S)->decimal(P',S)where P' > P) - Make required field optional
Not supported:
- Narrowing types (
long->int) - Incompatible type changes (
string->int) - Making optional field required
schema-type-choices
Choose types carefully — they are hard to change later.
| Value | Recommended Type | Notes |
| Event timestamps | timestamptz | Always use timezone; avoids ambiguity across engines |
| IDs | long | Prefer over int for growth headroom |
| Money | decimal(P, S) | Never float/double |
| UUIDs | string | Native uuid type has inconsistent engine support |
| Nested data | struct | Prefer over flattened column names with prefixes |
schema-best-practices
Design schemas for evolution from day one.
- Add new columns as optional with documented defaults
- Use
COMMENT ON COLUMNto track field semantics - Coordinate with downstream consumers before dropping columns
- Test schema changes in a branch or test environment before production
Partitioning (CRITICAL)
partition-hidden
Use hidden partitioning with transforms — never create explicit partition columns.
Iceberg applies partition transforms automatically. Users query raw columns and the engine applies partition pruning transparently. No synthetic partition columns needed.
CREATE TABLE events (
event_id STRING,
event_time TIMESTAMP,
user_id BIGINT,
event_type STRING
) USING iceberg
PARTITIONED BY (day(event_time), bucket(16, user_id));Available transforms: year(ts), month(ts), day(ts), hour(ts), bucket(N, col), truncate(L, col), identity(col).
partition-transform-selection
Choose partition transforms based on data volume.
| Data Volume | Time Transform | Notes |
| < 10 GB/day | month(ts) | ~12 partitions/year |
| 10–100 GB/day | day(ts) | Most common choice |
| > 100 GB/day | hour(ts) | Only for very high volume |
For non-time columns, use bucket(N, col) where N produces partitions of 100 MB–1 GB each.
partition-sizing
Target partition sizes of 100 MB–1 GB per partition.
- Avoid more than ~10,000 total partitions (metadata overhead)
- Avoid fewer than ~10 partitions (limits parallelism)
- If partitions produce files < 25 MB, use coarser partitioning or compaction
- If partitions produce files > 2 GB, use finer partitioning
partition-evolution
Iceberg supports changing partition schemes without rewriting data.
Old data retains the old layout; new data uses the new layout. The query planner handles both transparently.
-- Evolve from monthly to daily as data volume grows
ALTER TABLE db.events SET PARTITION SPEC (day(event_time), bucket(16, user_id));partition-anti-patterns
Avoid these partitioning mistakes.
identity(high_cardinality_column)— creates one partition per unique valuehour(ts)on low-volume tables — thousands of tiny files- Three-level partitioning — almost never needed; keep fanout manageable
- Explicit partition columns (Hive-style) — defeats Iceberg's hidden partitioning
Sort Order (HIGH)
sort-order-configuration
Set sort order on frequently filtered columns to enable min/max metadata filtering.
ALTER TABLE db.events WRITE ORDERED BY (tenant_id ASC NULLS LAST, event_time ASC);- Sort by columns most commonly used in
WHEREclauses (after partition columns) - Put low-cardinality filter columns first
- Sort order applies only to newly written files; run
rewrite_data_fileswithsortstrategy to apply to existing data
sort-order-zorder
Use Z-order when queries filter on multiple columns with roughly equal selectivity.
Z-order interleaves bits of multiple columns for balanced multi-dimensional clustering. Linear sort is better when one column dominates filter patterns.
CALL catalog.system.rewrite_data_files(
table => 'db.events',
strategy => 'zorder',
sort_order => 'tenant_id,event_type'
)File Format (HIGH)
format-parquet-zstd
Use Parquet with ZSTD compression. There is rarely a reason to deviate.
ALTER TABLE db.events SET TBLPROPERTIES (
'write.format.default' = 'parquet',
'write.parquet.compression-codec' = 'zstd'
);Key tuning properties:
write.parquet.compression-codec:zstd(recommended),snappy,gzip,lz4write.parquet.compression-level: 1–3 for ZSTD (good balance)write.parquet.row-group-size-bytes: 134217728 (128 MB default)write.parquet.dict-size-bytes: 2097152 (2 MB; increase for high-cardinality dictionary columns)
| Format | Use For |
| Parquet | All data files (default) |
| ORC | Migrating from Hive ORC tables |
| Avro | Delete files in merge-on-read mode only |
Catalog Configuration (CRITICAL)
catalog-selection
Choose a production catalog. Never use Hadoop catalog in production.
| Catalog | Best For | Notes |
| REST (Polaris, Gravitino) | Multi-engine production | Vendor-neutral, spec-driven, credential vending |
| AWS Glue | AWS-native stacks | Zero infrastructure; watch for API rate limits |
| Hive Metastore | Existing Hive infrastructure | No multi-table atomic commits |
| JDBC | Development, small deployments | Simple, any JDBC database |
| Nessie | Git-like data branching | CI/CD for data, branch/merge semantics |
| Hadoop | Never in production | No atomic commits, no concurrency safety |
catalog-rest
Use REST catalog for multi-engine deployments.
spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.my_catalog.type=rest
spark.sql.catalog.my_catalog.uri=http://catalog-host:port/api/v1
spark.sql.catalog.my_catalog.warehouse=s3://bucket/warehouseREST catalog supports credential vending — engines get short-lived credentials for storage access.
catalog-caching
Enable catalog caching to reduce metadata lookups.
spark.sql.catalog.my_catalog.cache-enabled=true
spark.sql.catalog.my_catalog.cache.expiration-interval-ms=300000Write Optimization (HIGH)
write-target-file-size
Target file sizes of 256 MB–1 GB.
ALTER TABLE db.events SET TBLPROPERTIES (
'write.target-file-size-bytes' = '536870912' -- 512 MB
);- High-query-frequency tables: 256–512 MB (more parallelism)
- Archival/cold tables: 512 MB–1 GB (fewer files, less metadata)
- Never below 32 MB (too many small files) or above 2 GB (too little parallelism)
write-distribution-mode
Choose write distribution mode based on table structure.
| Mode | Behavior | Best For |
none | No shuffle before writing | Append-only unpartitioned tables |
hash | Hash-distribute by partition key | Partitioned tables (recommended) |
range | Range-distribute by sort key | When sort order optimization is critical |
ALTER TABLE db.events SET TBLPROPERTIES (
'write.distribution-mode' = 'hash'
);write-streaming-flink
For Flink streaming, set checkpoint interval to 1–5 minutes and target 128 MB files.
ALTER TABLE db.events SET TBLPROPERTIES (
'write.target-file-size-bytes' = '134217728' -- 128 MB for streaming
);10-second checkpoint intervals create many tiny files. Use longer intervals and rely on compaction.
write-commit-retry
Increase commit retries for high-contention environments.
commit.retry.num-retries=4
commit.retry.min-wait-ms=100
commit.manifest-merge.enabled=true
commit.manifest.target-size-bytes=8388608Read Optimization (HIGH)
read-predicate-pushdown
Always filter on partition columns first, then sorted columns.
Iceberg pushes predicates down to three levels:
- Partition pruning — skips entire partitions
- Manifest filtering — min/max per file from manifest metadata
- Row-group filtering — min/max within Parquet row groups
Use explicit equality and range predicates. Avoid UDFs or complex expressions that prevent pushdown.
read-column-pruning
Select only needed columns — never SELECT * on wide tables.
Iceberg + Parquet skips entire column chunks for unselected columns. Wide tables (100+ columns) benefit enormously.
read-metadata-metrics
Configure column-level metrics for optimal metadata filtering.
ALTER TABLE db.events SET TBLPROPERTIES (
'write.metadata.metrics.default' = 'truncate(16)',
'write.metadata.metrics.column.tenant_id' = 'full',
'write.metadata.metrics.column.raw_payload' = 'none'
);full: complete min/max stats — for columns always filtered ontruncate(N): first N bytes of min/max — default, good for most stringsnone: no stats — for columns never filtered on (reduces metadata size)
read-split-size
Tune split size for read parallelism.
ALTER TABLE db.events SET TBLPROPERTIES (
'read.split.target-size' = '134217728' -- 128 MB default
);Decrease for more parallelism on large clusters; increase for small clusters.
read-vectorized
Enable vectorized reading in Spark.
spark.sql.iceberg.vectorization.enabled=trueProcesses data in columnar batches rather than row-by-row — significant speedup for scan-heavy queries.
Table Maintenance (CRITICAL)
maintenance-expire-snapshots
Expire snapshots daily. This is the most important maintenance task.
CALL catalog.system.expire_snapshots(
table => 'db.events',
older_than => TIMESTAMP '2024-01-01 00:00:00',
retain_last => 10
);Without expiration, metadata files grow unboundedly, slowing down all query planning. Keep enough snapshots for your time travel needs (default retention: 5 days).
maintenance-compaction
Run binpack compaction daily (hourly for streaming tables).
CALL catalog.system.rewrite_data_files(
table => 'db.events',
strategy => 'binpack',
options => map(
'target-file-size-bytes', '536870912',
'min-file-size-bytes', '67108864',
'max-file-size-bytes', '1073741824',
'min-input-files', '5',
'partial-progress.enabled', 'true',
'partial-progress.max-commits', '10'
)
);| Strategy | Cost | Effect |
binpack | Low | Combines small files — routine compaction |
sort | High | Re-sorts data by sort order — better read performance |
zorder | High | Z-order clustering — better multi-column filtering |
Run binpack daily/hourly. Run sort weekly during off-peak hours.
maintenance-rewrite-manifests
Rewrite manifests weekly to improve query planning performance.
CALL catalog.system.rewrite_manifests(table => 'db.events');Consolidates many small manifest files into fewer, larger ones. Run after heavy write operations.
maintenance-orphan-files
Remove orphan files weekly or monthly.
CALL catalog.system.remove_orphan_files(
table => 'db.events',
older_than => TIMESTAMP '2024-06-01 00:00:00',
dry_run => true -- preview first
);Use older_than > max expected write duration to avoid deleting in-progress writes (default: 3 days).
maintenance-schedule
Recommended maintenance schedule.
| Task | Frequency | Priority |
| Expire snapshots | Daily | Critical |
| Binpack compaction | Daily / hourly (streaming) | Critical |
| Sort compaction | Weekly (off-peak) | High |
| Rewrite manifests | Weekly | Medium |
| Remove orphan files | Weekly / monthly | Medium |
Time Travel and Snapshots (MEDIUM)
timetravel-queries
Query historical data by snapshot ID or timestamp.
SELECT * FROM db.events VERSION AS OF 1234567890;
SELECT * FROM db.events TIMESTAMP AS OF '2024-06-15 10:00:00';Time travel only works for snapshots that haven't been expired. Retain enough snapshots for your incident response SLA.
timetravel-rollback
Rollback is a metadata-only operation (instant).
CALL catalog.system.rollback_to_snapshot('db.events', 1234567890);
CALL catalog.system.rollback_to_timestamp('db.events', TIMESTAMP '2024-06-15 10:00:00');Does not delete data files — the rolled-back-to snapshot's files are still present.
timetravel-tags
Use tags to protect important snapshots from expiration.
ALTER TABLE db.events CREATE TAG release_2024_q2 AS OF VERSION 1234567890 RETAIN 365 DAYS;Tagged snapshots are immune from expire_snapshots for the retention period.
Merge-on-Read vs Copy-on-Write (HIGH)
mor-vs-cow
Choose based on read/write ratio.
| Mode | Write Speed | Read Speed | Best For |
| Copy-on-Write | Slow (rewrites files) | Fast (no delete files) | Read-heavy, batch updates |
| Merge-on-Read | Fast (append delete markers) | Slower (merge at read time) | Write-heavy, streaming CDC |
-- Copy-on-Write (default, recommended for most tables)
ALTER TABLE db.events SET TBLPROPERTIES (
'write.delete.mode' = 'copy-on-write',
'write.update.mode' = 'copy-on-write',
'write.merge.mode' = 'copy-on-write'
);
-- Merge-on-Read (for streaming/CDC tables)
ALTER TABLE db.events SET TBLPROPERTIES (
'write.delete.mode' = 'merge-on-read',
'write.update.mode' = 'merge-on-read',
'write.merge.mode' = 'merge-on-read'
);Positional vs equality deletes:
- Positional deletes reference file path + row position — efficient for engines to apply, used by Spark
- Equality deletes reference column values to match — more flexible but more expensive at read time
- Positional deletes are preferred; Iceberg v2 supports both
Critical for MoR: Run compaction regularly to merge delete files into base data files. If a data file has >10 associated delete files, compaction is overdue.
Multi-Engine Integration (HIGH)
integration-spark
Spark has the most mature Iceberg integration.
spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.my_catalog.type=rest
spark.sql.catalog.my_catalog.uri=http://catalog-host:port/api/v1- Use Spark for all maintenance procedures (
rewrite_data_files,expire_snapshots, etc.) - Enable vectorized reads:
spark.sql.iceberg.vectorization.enabled=true - Use
DataFrame.writeTo("catalog.db.table").append()or.overwritePartitions()for writes spark.sql.shuffle.partitionsaffects output file count
integration-flink
Flink is best for streaming ingestion into Iceberg.
- Set checkpoint interval to control commit frequency (1–5 minutes)
write.upsert.enabled=truefor CDC upsert semantics- Flink writes smaller files by nature (streaming) — rely on compaction to consolidate
- Target 128 MB file sizes for streaming
integration-trino
Trino is excellent for interactive analytics and ad-hoc queries.
- Supports reads, writes, schema evolution, time travel, hidden partitioning
- Does not support all maintenance procedures — use Spark for full maintenance
- Set
iceberg.file-format=PARQUETandiceberg.compression-codec=ZSTD
integration-cross-engine
Use a shared catalog for multi-engine access.
- All engines read/write the same Iceberg format — true interoperability
- Ensure all engines use compatible Iceberg library versions
- Avoid engine-specific table properties when multi-engine access is needed
Branching and Tagging (MEDIUM)
branching-wap
Use branches for Write-Audit-Publish (WAP) workflows.
-- Create a branch
ALTER TABLE db.events CREATE BRANCH audit_branch;
-- Write to branch (Spark)
SET spark.wap.branch = audit_branch;
INSERT INTO db.events VALUES (...);
-- Read from branch
SELECT * FROM db.events VERSION AS OF 'audit_branch';
-- Fast-forward main after validation
CALL catalog.system.fast_forward('db.events', 'main', 'audit_branch');branching-nessie
Use Nessie for catalog-level branching across multiple tables.
Nessie provides Git-like branching across the entire catalog. Create a branch, modify multiple tables, merge atomically. Enables data CI/CD with merge conflict detection.
branching-cleanup
Set retention on branches and tags. Clean up stale branches.
ALTER TABLE db.events DROP BRANCH old_branch;Unmanaged branches and tags cause unbounded metadata growth.
Common Anti-Patterns
| Anti-Pattern | Problem | Solution |
identity(high_cardinality_col) | One partition per unique value | Use bucket(N, col) |
hour(ts) on low-volume tables | Thousands of tiny files | Use day(ts) or month(ts) |
| Not expiring snapshots | Metadata grows unboundedly | Expire daily, retain what you need |
| Not compacting (streaming/MoR) | Small files degrade reads | Binpack hourly for streaming |
| Hadoop catalog in production | No atomic commits, unsafe | Use REST, Glue, or Hive catalog |
SELECT * on wide tables | Reads all columns | Project only needed columns |
| No sort order | Min/max filtering ineffective | Sort by frequently filtered columns |
| Delete file accumulation (MoR) | Exponential read degradation | Compact to merge deletes into base |
TIMESTAMP without timezone | Ambiguity across engines/regions | Use timestamptz |
| Tiny streaming commit intervals | Many tiny files | Use 1–5 minute intervals + compaction |
| Incompatible library versions | Metadata corruption | Align versions across engines |
| Explicit partition columns | Defeats hidden partitioning | Use partition transforms |
Skipping rewrite_manifests | Slow query planning | Rewrite weekly |
| Large updates with copy-on-write | Wasteful file rewrites | Switch to merge-on-read |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

