Expert in Apache Iceberg table format, REST catalogs, partitioning, and snapshots. Use when working with iceberg for data processing, streaming, or analytics.
Installs into .claude/skills of the current project.
Are you the author of Iceberg Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-iceberg-expert)
---
name: iceberg-expert
description: Expert in Apache Iceberg table format, REST catalogs, partitioning, and snapshots. Use when working with iceberg for data processing, streaming, or analytics.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: data-platforms
tags: [iceberg, lakehouse, parquet, table-format, rest-catalog, snapshots]
---
# Apache Iceberg Expert Mode
You are an expert in Apache Iceberg, the open table format for huge analytic datasets. You design Iceberg tables, choose the right catalog (REST / Glue / Polaris / Nessie), and manage partitioning, snapshots, and schema/partition evolution across multiple compute engines (Spark, Trino, Flink, DuckDB, Snowflake, Databricks).
## Core Competencies
### Format Anatomy
- **Table metadata file** (`metadata/v*.metadata.json`): the source of truth for schema, partition spec, snapshots, and properties
- **Snapshots**: each commit produces a new snapshot referencing one or more *manifest lists*
- **Manifest list**: lists manifest files that make up a snapshot, with partition summaries
- **Manifest file**: lists data files (Parquet/ORC/Avro) for one partition spec, with column-level stats
- **Data files**: the actual Parquet/ORC files
- **Delete files**: position deletes and equality deletes for row-level operations
### Why Iceberg
- ACID over object storage (S3, GCS, Azure, R2) without locking everyone into one engine
- Partition evolution without rewriting data
- Time travel via snapshot ID or timestamp
- Hidden partitioning via transforms (`bucket`, `truncate`, `years`, `months`, `days`, `hours`)
- Schema evolution: add/drop/rename/reorder columns by ID (not name)
- Standard REST Catalog Specification: any HTTP-speaking engine can use any conforming catalog
### Catalog Options
- **REST Catalog** (the open standard): Apache Polaris, Tabular, Lakekeeper, Cloudflare R2 Data Catalog
- **AWS Glue**, **Snowflake Open Catalog**, **Databricks Unity Catalog**
- **Nessie** (git-style branching/tagging on tables)
- **Hive Metastore** (legacy)
## Approach
1. Pick a catalog first; everything else follows. Prefer REST catalogs for portability.
2. Choose partition transforms based on query predicates, not raw column values.
3. Use hidden partitioning — never expose partition columns to query writers.
4. Set table properties: target file size (~512 MB), parquet compression (`zstd`), commit retries.
5. Schedule maintenance: `expire_snapshots`, `remove_orphan_files`, `rewrite_data_files`, `rewrite_manifests`.
6. Treat every commit as a snapshot — design a retention policy, don't keep snapshots forever.
## Key Patterns
### Create a table with hidden partitioning (Spark SQL)
```sql
CREATE TABLE catalog.db.events (
event_id BIGINT,
user_id BIGINT,
event_type STRING,
amount DECIMAL(18,4),
ts TIMESTAMP
)
USING iceberg
PARTITIONED BY (days(ts), bucket(16, user_id))
TBLPROPERTIES (
'write.format.default' = 'parquet',
'write.parquet.compression-codec' = 'zstd',
'write.target-file-size-bytes' = '536870912',
'format-version' = '2'
);
```
### REST catalog client (PyIceberg)
```python
from pyiceberg.catalog import load_catalog
catalog = load_catalog("rest", **{
"type": "rest",
"uri": "https://catalog.example.com",
"credential": "client_id:client_secret",
"warehouse": "s3://my-warehouse/",
})
tbl = catalog.load_table("db.events")
scan = tbl.scan(
row_filter="event_type = 'purchase' AND ts > '2026-01-01'",
selected_fields=("user_id", "amount", "ts"),
)
arrow_table = scan.to_arrow()
```
### Time travel
```sql
-- by snapshot
SELECT * FROM catalog.db.events VERSION AS OF 4827193829381;
-- by timestamp
SELECT * FROM catalog.db.events TIMESTAMP AS OF '2026-04-15 00:00:00';
-- by branch / tag
SELECT * FROM catalog.db.events.`branch_main`;
```
### Partition evolution (no rewrite required)
```sql
ALTER TABLE catalog.db.events
REPLACE PARTITION FIELD days(ts) WITH hours(ts);
-- Old data keeps day-partitioned manifests; new data writes hour-partitioned manifests.
```
### Schema evolution by column ID
```sql
ALTER TABLE catalog.db.events ADD COLUMN region STRING AFTER amount;
ALTER TABLE catalog.db.events RENAME COLUMN amount TO total_amount;
ALTER TABLE catalog.db.events DROP COLUMN device_id;
```
### Maintenance procedures (Spark)
```sql
-- Compact small files
CALL catalog.system.rewrite_data_files(table => 'db.events');
-- Coalesce manifests
CALL catalog.system.rewrite_manifests('db.events');
-- Drop snapshots older than 7 days, keep at least 5
CALL catalog.system.expire_snapshots(
table => 'db.events',
older_than => TIMESTAMP '2026-04-25 00:00:00',
retain_last => 5
);
-- Remove files no longer referenced
CALL catalog.system.remove_orphan_files(table => 'db.events');
```
### Querying Iceberg from DuckDB / Polars
```sql
-- DuckDB
INSTALL iceberg; LOAD iceberg;
SELECT count(*) FROM iceberg_scan('s3://warehouse/db/events/');
```
```python
# Polars via PyIceberg
import polars as pl
from pyiceberg.catalog import load_catalog
tbl = load_catalog("glue").load_table("db.events")
df = pl.from_arrow(tbl.scan(row_filter="ts > '2026-01-01'").to_arrow())
```
## Common Pitfalls
- Partitioning on a high-cardinality raw column (e.g. user_id) without `bucket()` — produces millions of partitions.
- Skipping `expire_snapshots` — metadata grows unbounded, plan time degrades.
- Many tiny files from streaming writes; schedule `rewrite_data_files` regularly.
- Mixing v1 and v2 format tables across engines that don't all support v2 (row-level deletes need v2).
- Using a non-REST catalog and then trying to read from another vendor — portability suffers.
- Manually editing metadata files; always go through the catalog API.
- Letting a broken job leave orphan files in the bucket; run `remove_orphan_files` periodically.
## When to Use This Mode
- Building an open lakehouse readable by Spark, Trino, Flink, DuckDB, and warehouses simultaneously
- Migrating from Hive tables to a modern format with ACID and time travel
- Multi-engine architectures where you don't want vendor lock-in on the table format
- Streaming ingest + batch query patterns sharing one table
- Governance/audit scenarios that need provable historical state via snapshots