Apache Iceberg + Parquet: Руководство для Data Engineers¶
1. Введение¶
Наша платформа данных использует Apache Iceberg для управления таблицами и Apache Parquet для хранения данных.
Почему Iceberg + Parquet?¶
| Требование | Решение |
|---|---|
| ACID гарантии | Iceberg обеспечивает snapshot isolation |
| Schema evolution | Iceberg позволяет добавлять/удалять колонки без rewrite |
| Partition evolution | Iceberg hidden partitioning - можно менять без rewrite |
| Time travel | Iceberg хранит snapshots для rollback |
| Эффективное хранение | Parquet columnar format + compression |
| Быстрые запросы | Parquet statistics + bloom filters + predicate pushdown |
| Vendor-neutral | Оба формата открытые (Apache Foundation) |
2. Выбор каталога метаданных (Catalog)¶
Iceberg требует каталог метаданных для хранения информации о таблицах (schema, partition spec, snapshots). Выбор каталога критичен для долгосрочной поддержки.
2.1. Рекомендация: Gravitino ⭐¶
Gravitino — универсальный metadata catalog от Datastrato, специально разработанный для работы с Iceberg и другими table formats.
Преимущества: - ✅ Unified API — один интерфейс для Iceberg, Paimon, Delta Lake, Hudi - ✅ REST API — стандартный протокол, не нужно разрабатывать свой - ✅ Активное развитие — проект активно развивается сообществом - ✅ Multi-format support — можно мигрировать между форматами без переписывания - ✅ Open Source — Apache-лицензия, нет vendor lock-in - ✅ Production-ready — используется в production окружениях
Конфигурация:
# contracts/domains/sales/orders/physical_layout.yml
iceberg:
catalog:
type: "gravitino"
uri: "http://gravitino-server:8090"
warehouse: "s3://data-lake/warehouse"
database: "sales"
table: "orders"
Подключение в Spark/Trino:
# Spark
spark.sql.catalog.gravitino=org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.gravitino.type=gravitino
spark.sql.catalog.gravitino.uri=http://gravitino-server:8090
# Trino
connector.name=iceberg
iceberg.catalog.type=gravitino
iceberg.catalog.uri=http://gravitino-server:8090
2.2. Альтернатива: REST Catalog¶
Если нужен быстрый старт без дополнительной инфраструктуры:
Преимущества: - ✅ Простота — минимальная настройка - ✅ Официальная поддержка Iceberg - ✅ PostgreSQL backend — дешёвое хранилище метаданных - ✅ Не требует отдельного сервиса (можно встроить в приложение)
Недостатки: - ⚠️ Нужно поддерживать REST API при обновлениях Iceberg - ⚠️ Нет unified API для других форматов (Paimon, Delta)
Конфигурация:
iceberg:
catalog:
type: "rest"
uri: "http://iceberg-rest:8181"
warehouse: "s3://data-lake/warehouse"
2.3. Сравнение каталогов¶
| Каталог | Статус | Поддержка Iceberg | Multi-format | REST API | Рекомендация |
|---|---|---|---|---|---|
| Gravitino | ✅ Активно | ✅ Native | ✅ Да | ✅ Да | ⭐ Рекомендуется |
| REST Catalog | ✅ Стабильно | ✅ Native | ❌ Нет | ✅ Да | ✅ Для MVP |
| Hive Metastore | ⚠️ Legacy | ⚠️ Через адаптер | ❌ Нет | ❌ Нет | ❌ Не рекомендуется |
| AWS Glue | ⚠️ Proprietary | ✅ Native | ❌ Нет | ❌ Нет | ❌ AWS lock-in |
| Nessie | ❌ Заморожен | ⚠️ Устарело | ❌ Нет | ✅ Да | ❌ Не рекомендуется |
2.4. Почему не другие варианты?¶
Hive Metastore: - ❌ Legacy архитектура, не поддерживает современные фичи Iceberg - ❌ Нет REST API, требует Hive сервис - ❌ Сложная настройка и поддержка
AWS Glue: - ❌ Vendor lock-in (привязка к AWS) - ❌ Дорого при масштабировании - ❌ Проприетарное решение
Nessie: - ❌ Проект заморожен, нет активной поддержки - ❌ Устаревшая архитектура - ❌ Не рекомендуется для новых проектов
Custom REST Catalog: - ❌ Требует разработки и поддержки - ❌ Нужно отслеживать все изменения Iceberg - ❌ Нет ресурсов на поддержку всех фич
3. Apache Iceberg Concepts¶
2.1. Table Format¶
Iceberg таблица состоит из трёх слоёв:
┌─────────────────────────────────────────────────────────────────────┐
│ ICEBERG TABLE LAYERS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ METADATA LAYER (JSON) │
│ └── s3://.../metadata/v123.metadata.json │
│ • Table schema │
│ • Partition spec │
│ • Sort order │
│ • Snapshots list │
│ • Current snapshot │
│ • Table properties │
│ │
│ 2️⃣ MANIFEST LAYER (Avro) │
│ └── s3://.../metadata/snap-*.avro │
│ • Manifest list (list of manifests) │
│ • Manifest files (list of data files) │
│ • Statistics per file (row count, column bounds) │
│ │
│ 3️⃣ DATA LAYER (Parquet) │
│ └── s3://.../data/*.parquet │
│ • Actual data in Parquet files │
│ • Target size: 512 MB per file │
│ │
└─────────────────────────────────────────────────────────────────────┘
2.2. Hidden Partitioning¶
Проблема с традиционным партиционированием:
-- Пользователь должен знать о партициях
SELECT * FROM orders
WHERE year = 2026 AND month = 1 AND day = 23
AND customer_id = 'cust_123';
Iceberg Hidden Partitioning:
-- Iceberg автоматически использует партиции
SELECT * FROM orders
WHERE created_at = '2026-01-23'
AND customer_id = 'cust_123';
Partition transforms:
# contracts/domains/sales/orders/physical_layout.yml
partitioning:
spec:
# Time-based
- source_column: "created_at"
transform: "day" # year | month | day | hour
partition_field: "created_at_day"
# Bucket (hash mod N)
- source_column: "customer_id"
transform: "bucket[16]"
partition_field: "customer_id_bucket"
# Truncate (first N characters for strings)
- source_column: "region"
transform: "truncate[2]"
partition_field: "region_prefix"
Пример структуры на S3:
s3://data-lake/warehouse/sales.db/orders/
└── data/
├── created_at_day=2026-01-23/
│ ├── customer_id_bucket=0/
│ │ ├── 00000-0-a1b2c3d4.parquet
│ │ └── 00001-0-e5f6g7h8.parquet
│ ├── customer_id_bucket=1/
│ └── ...
└── created_at_day=2026-01-24/
2.3. Partition Evolution¶
Можно менять партиционирование без переписывания данных!
-- Изначально: monthly partitioning
ALTER TABLE sales.orders
SET PARTITION SPEC (month(created_at));
-- Трафик вырос, переходим на daily
ALTER TABLE sales.orders
REPLACE PARTITION FIELD month(created_at) WITH day(created_at);
-- Старые данные остаются в monthly партициях
-- Новые данные пишутся в daily партиции
-- Запросы работают корректно с обоими
2.4. Time Travel¶
-- Чтение данных как было вчера
SELECT * FROM sales.orders
TIMESTAMP AS OF '2026-01-22 10:00:00';
-- Чтение конкретного snapshot
SELECT * FROM sales.orders
VERSION AS OF 123456789;
-- Список всех snapshots
SELECT * FROM sales.orders.snapshots
ORDER BY committed_at DESC;
-- Rollback к предыдущему snapshot
CALL system.rollback_to_snapshot('sales.orders', 123456789);
2.5. Schema Evolution¶
-- Добавление колонки (non-breaking)
ALTER TABLE sales.orders
ADD COLUMN discount_percent DOUBLE;
-- Удаление колонки
ALTER TABLE sales.orders
DROP COLUMN old_field;
-- Переименование колонки
ALTER TABLE sales.orders
RENAME COLUMN old_name TO new_name;
-- Изменение типа (widening only)
ALTER TABLE sales.orders
ALTER COLUMN price TYPE DECIMAL(20,2); -- было DECIMAL(10,2)
Что происходит: - Старые Parquet файлы НЕ переписываются - Новые файлы используют новую схему - Query engine автоматически обрабатывает разные схемы
3. Apache Parquet Concepts¶
3.1. Columnar Format¶
Row-oriented (CSV, JSON):
Row 1: id=1, name="Alice", amount=100
Row 2: id=2, name="Bob", amount=200
Row 3: id=3, name="Charlie", amount=300
Чтобы прочитать SUM(amount), нужно прочитать все колонки.
Column-oriented (Parquet):
Column "id": [1, 2, 3]
Column "name": ["Alice", "Bob", "Charlie"]
Column "amount": [100, 200, 300]
Чтобы прочитать SUM(amount), читаем только колонку "amount"!
Преимущества: - Читаем только нужные колонки (меньше I/O) - Лучшая компрессия (одинаковые типы данных в колонке) - Vectorized execution (SIMD)
3.2. Row Groups¶
Parquet файл разбит на row groups (по умолчанию 128 MB):
┌─────────────────────────────────────────────────────────────────┐
│ PARQUET FILE (512 MB) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Row Group 1 (128 MB) │
│ ├── Column "order_id" │
│ │ ├── Page 1 (1 MB) - dictionary encoded │
│ │ ├── Page 2 (1 MB) │
│ │ └── ... │
│ ├── Column "customer_id" │
│ │ └── Pages... │
│ ├── Column "total_amount" │
│ │ └── Pages... │
│ └── Statistics: │
│ • min(order_id) = "ord_000..." │
│ • max(order_id) = "ord_999..." │
│ • null_count = 0 │
│ • row_count = 1,000,000 │
│ │
│ Row Group 2 (128 MB) │
│ └── ... │
│ │
│ Row Group 3 (128 MB) │
│ └── ... │
│ │
│ Row Group 4 (128 MB) │
│ └── ... │
│ │
└─────────────────────────────────────────────────────────────────┘
Data Skipping:
-- Query engine читает statistics из footer
SELECT * FROM orders WHERE total_amount > 100000;
-- Если max(total_amount) в row group < 100000,
-- весь row group пропускается (не читается с диска)!
3.3. Compression¶
Column-level compression в physical_layout.yml:
parquet:
# Default compression для всех колонок
compression:
codec: "zstd"
level: 3
reason: "Баланс compression ratio / speed"
# Специфичная компрессия для отдельных колонок
column_compression:
# Dictionary encoding для enum
- column: "status"
codec: "dictionary"
reason: "6 уникальных значений - dictionary очень эффективен"
# Dictionary для low cardinality
- column: "currency"
codec: "dictionary"
reason: "3 значения (RUB, USD, EUR)"
# Snappy для строк с низкой повторяемостью
- column: "customer_email"
codec: "snappy"
reason: "Быстрая компрессия для high cardinality"
Codecs comparison:
| Codec | Compression Ratio | Speed | Use Case |
|---|---|---|---|
| ZSTD (level 3) | ~4.5x | Medium | Default - best balance |
| Snappy | ~2.5x | Fast | Real-time ingestion, high write throughput |
| Gzip | ~5x | Slow | Cold storage, архивы |
| Dictionary | ~10x+ | Fast | Enum, low cardinality (<1000 unique values) |
| LZ4 | ~2x | Very Fast | Hot data, frequent reads |
3.4. Bloom Filters¶
Bloom filter ускоряет point lookups (поиск по ID):
parquet:
bloom_filters:
- column: "order_id"
fpp: 0.01 # false positive probability (1%)
reason: "Частые запросы вида WHERE order_id = X"
- column: "customer_id"
fpp: 0.01
reason: "Фильтрация по customer_id"
Как работает:
SELECT * FROM orders WHERE order_id = 'ord_abc123';
-- 1. Query engine проверяет bloom filter для каждого Parquet файла
-- 2. Если bloom filter говорит "NO" - файл пропускается (100% уверенность)
-- 3. Если bloom filter говорит "MAYBE" - файл читается (1% ложных срабатываний)
Overhead: - Bloom filter занимает ~1-2% от размера файла - Время создания: +5-10% при write - Ускорение point lookups: 10-100x
4. Physical Layout Configuration¶
4.1. Полный пример physical_layout.yml¶
# contracts/domains/sales/orders/physical_layout.yml
version: "1.2"
iceberg:
format_version: 2 # v2 поддерживает row-level updates/deletes
catalog:
type: "hive"
warehouse: "s3://data-lake/warehouse"
database: "sales"
table: "orders"
properties:
"write.format.default": "parquet"
"write.parquet.compression-codec": "zstd"
"write.parquet.compression-level": "3"
"write.target-file-size-bytes": "536870912" # 512 MB
"write.delete.mode": "merge-on-read"
"history.expire.min-snapshots-to-keep": "10"
partitioning:
strategy: "iceberg_hidden"
spec:
- source_column: "created_at"
transform: "day"
partition_field: "created_at_day"
reason: "90% запросов фильтруют по дате"
- source_column: "customer_id"
transform: "bucket[16]"
partition_field: "customer_id_bucket"
reason: "Распределение нагрузки для параллелизма"
sort_order:
enabled: true
fields:
- column: "customer_id"
direction: "asc"
null_order: "nulls-first"
reason: "Запросы часто группируют по customer_id"
- column: "created_at"
direction: "desc"
null_order: "nulls-last"
reason: "Последние заказы первыми"
parquet:
row_group_size: 134217728 # 128 MB
page_size: 1048576 # 1 MB
compression:
codec: "zstd"
level: 3
column_compression:
- column: "status"
codec: "dictionary"
- column: "currency"
codec: "dictionary"
bloom_filters:
- column: "order_id"
fpp: 0.01
- column: "customer_id"
fpp: 0.01
compaction:
small_files:
enabled: true
target_file_size_mb: 512
schedule: "0 2 * * *"
z_order:
enabled: true
columns:
- "customer_id"
- "created_at"
reason: "Оптимизация multi-dimensional queries"
snapshots:
retention:
min_snapshots_to_keep: 10
max_age_days: 30
storage:
avg_row_size_bytes: 2048
compression_ratio: 4.5
growth:
daily_rows: 500000
daily_size_compressed_gb: 0.22
tiering:
hot:
duration: "P30D"
storage_class: "S3 Standard"
warm:
duration: "P365D"
storage_class: "S3 Intelligent-Tiering"
cold:
duration: "P7Y"
storage_class: "S3 Glacier"
4.2. Как выбрать Partition Spec?¶
Вопросы: 1. По каким полям чаще всего фильтруют запросы? 2. Какая кардинальность этих полей? 3. Сколько данных в день/час?
Рекомендации:
| Сценарий | Partition Transform | Пример |
|---|---|---|
| Данные с timestamp, запросы по времени | day(timestamp) | day(created_at) |
| Высокая кардинальность ID | bucket[N](id) | bucket[16](customer_id) |
| География (country, city) | identity(field) | identity(country_code) |
| Строки с префиксами | truncate[N](field) | truncate[2](region) |
Антипаттерны: - ❌ Слишком много партиций (>10,000) - slow metadata operations - ❌ Слишком мало партиций (<100) - no parallelism - ❌ Партиции с very low cardinality (<10 values)
4.3. Как выбрать Sort Order?¶
Sort order критичен для: - Range queries (WHERE created_at >= X) - Joins (sort-merge join) - Aggregations (grouping sorted data)
Правило: 1. Первая колонка: frequently filtered (high selectivity) 2. Вторая колонка: frequently joined 3. Третья колонка: primary key (для детерминизма)
Примеры:
# Customer 360 (запросы по customer_id)
sort_order:
fields:
- column: "customer_id" # High selectivity
- column: "created_at" # Time range
- column: "order_id" # PK для детерминизма
# Time-series analytics (запросы по времени)
sort_order:
fields:
- column: "created_at" # Time range
- column: "customer_id" # Grouping
- column: "order_id" # PK
5. Maintenance Tasks¶
5.1. Expire Snapshots¶
Зачем: Iceberg хранит все snapshots, они занимают место в metadata.
Как часто: Weekly
-- Удалить snapshots старше 30 дней, оставить минимум 10
CALL system.expire_snapshots(
table => 'sales.orders',
older_than => TIMESTAMP '2025-12-24',
retain_last => 10
);
Автоматизация (Airflow):
from airflow import DAG
from airflow.providers.trino.operators.trino import TrinoOperator
with DAG('expire_snapshots', schedule_interval='@weekly') as dag:
expire = TrinoOperator(
task_id='expire_orders_snapshots',
sql="""
CALL system.expire_snapshots(
table => 'sales.orders',
older_than => CURRENT_TIMESTAMP - INTERVAL '30' DAY,
retain_last => 10
)
"""
)
5.2. Remove Orphan Files¶
Зачем: При failed writes могут остаться файлы, которые не в metadata.
Как часто: Weekly
-- Удалить orphan files старше 3 дней
CALL system.remove_orphan_files(
table => 'sales.orders',
older_than => TIMESTAMP '2026-01-20'
);
5.3. Compact Small Files¶
Зачем: Streaming writes создают много маленьких файлов, это slow.
Как часто: Daily
-- Объединить small files в последних 7 днях
CALL system.rewrite_data_files(
table => 'sales.orders',
strategy => 'binpack',
where => 'created_at >= current_date() - INTERVAL ''7'' DAY'
);
Проверка:
-- Сколько small files?
SELECT COUNT(*) as small_files_count
FROM sales.orders.files
WHERE file_size_in_bytes < 128 * 1024 * 1024; -- < 128 MB
5.4. Z-Ordering (Multi-dimensional Clustering)¶
Зачем: Оптимизация для запросов с фильтрами по нескольким колонкам.
Как часто: Monthly
-- Z-order rewrite для последних 30 дней
CALL system.rewrite_data_files(
table => 'sales.orders',
strategy => 'sort',
sort_order => 'zorder(customer_id, created_at)',
where => 'created_at >= current_date() - INTERVAL ''30'' DAY'
);
Когда полезно:
-- Z-order полезен для таких запросов:
SELECT * FROM orders
WHERE customer_id = 'cust_123' -- Filter 1
AND created_at >= '2026-01-01' -- Filter 2
AND total_amount > 1000; -- Filter 3
6. Monitoring & Troubleshooting¶
6.1. Iceberg Metadata Tables¶
Iceberg предоставляет system tables для мониторинга:
-- История snapshots
SELECT
snapshot_id,
committed_at,
operation,
summary
FROM sales.orders.snapshots
ORDER BY committed_at DESC
LIMIT 10;
-- Список data files
SELECT
file_path,
file_size_in_bytes / 1024^2 as size_mb,
record_count,
partition
FROM sales.orders.files
ORDER BY file_size_in_bytes DESC
LIMIT 20;
-- Статистика по партициям
SELECT
partition,
COUNT(*) as file_count,
SUM(record_count) as total_records,
SUM(file_size_in_bytes) / 1024^3 as size_gb
FROM sales.orders.files
GROUP BY partition
ORDER BY partition DESC;
-- Список manifests
SELECT
path,
added_data_files_count,
existing_data_files_count,
deleted_data_files_count
FROM sales.orders.manifests
ORDER BY added_snapshot_id DESC;
-- История изменений таблицы
SELECT
made_current_at,
snapshot_id,
operation,
is_current_ancestor
FROM sales.orders.history
ORDER BY made_current_at DESC;
6.2. Performance Metrics¶
Критичные метрики:
-- 1. Table size
SELECT SUM(file_size_in_bytes) / 1024^3 AS table_size_gb
FROM sales.orders.files;
-- 2. Small files ratio
SELECT
COUNT(CASE WHEN file_size_in_bytes < 128*1024^2 THEN 1 END) AS small_files,
COUNT(*) AS total_files,
COUNT(CASE WHEN file_size_in_bytes < 128*1024^2 THEN 1 END) * 100.0 / COUNT(*) AS small_files_pct
FROM sales.orders.files;
-- 3. Snapshot age
SELECT
NOW() - MAX(committed_at) AS oldest_snapshot_age
FROM sales.orders.snapshots;
-- 4. Query performance (Trino)
SELECT
query_id,
query,
state,
elapsed_time_ms,
queued_time_ms,
planning_time_ms,
execution_time_ms,
peak_memory_bytes / 1024^3 AS peak_memory_gb
FROM system.runtime.queries
WHERE query LIKE '%sales.orders%'
AND state = 'FINISHED'
ORDER BY elapsed_time_ms DESC
LIMIT 10;
6.3. Troubleshooting¶
Problem: Slow Queries¶
Diagnosis:
-- 1. Проверить predicate pushdown
EXPLAIN SELECT * FROM orders WHERE created_at >= '2026-01-23';
-- 2. Проверить partition pruning
EXPLAIN ANALYZE SELECT * FROM orders WHERE created_at >= '2026-01-23';
-- 3. Проверить file count
SELECT COUNT(*) FROM sales.orders.files
WHERE partition LIKE '%created_at_day=2026-01-23%';
Solutions: - ✅ Добавить bloom filter для filtered columns - ✅ Compact small files - ✅ Z-order rewrite для multi-column filters - ✅ Проверить partition spec (возможно нужно finer granularity)
Problem: Too Many Small Files¶
Diagnosis:
SELECT
DATE_TRUNC('day', committed_at) as date,
COUNT(*) as files_created
FROM sales.orders.files
GROUP BY DATE_TRUNC('day', committed_at)
ORDER BY date DESC;
Solutions:
-- Daily compaction
CALL system.rewrite_data_files(
table => 'sales.orders',
strategy => 'binpack',
where => 'created_at >= current_date() - 1'
);
Problem: High Metadata Overhead¶
Diagnosis:
-- Проверить количество snapshots
SELECT COUNT(*) FROM sales.orders.snapshots;
-- Проверить количество manifests
SELECT COUNT(*) FROM sales.orders.manifests;
Solutions:
-- Expire старые snapshots
CALL system.expire_snapshots(
table => 'sales.orders',
older_than => CURRENT_TIMESTAMP - INTERVAL '7' DAY,
retain_last => 10
);
7. Best Practices¶
✅ DO¶
- Target file size: 512 MB (balance между parallelism и overhead)
- Compaction: Daily для hot partitions, weekly для warm
- Snapshots: Expire регулярно, держать 10-20 для debugging
- Bloom filters: Добавлять для ID колонок с point lookups
- Sort order: Первая колонка = frequently filtered
- Partition evolution: Начинать с coarse (monthly), переходить на finer (daily) по мере роста
- Z-ordering: Для multi-dimensional queries (monthly rewrite)
❌ DON'T¶
- Слишком много партиций (>10,000) - slow metadata
- Слишком маленькие файлы (<128 MB) - slow queries
- Слишком большие файлы (>1 GB) - low parallelism
- Игнорировать small files - приводит к performance degradation
- Не expire snapshots - metadata растёт indefinitely
- Partition по high cardinality без bucketing - too many partitions
- Over-engineering: Не добавлять bloom filters на все колонки
8. Query Examples¶
Point Lookup¶
-- Оптимизировано с bloom filter
SELECT * FROM sales.orders
WHERE order_id = 'ord_abc123';
-- План запроса:
-- 1. Bloom filter проверка → пропустить 99% файлов
-- 2. Прочитать 1-2 файла
-- Latency: ~50ms p50, ~200ms p99
Range Scan¶
-- Оптимизировано с partition pruning
SELECT * FROM sales.orders
WHERE created_at >= CURRENT_DATE - INTERVAL '7' DAY
ORDER BY created_at DESC;
-- План запроса:
-- 1. Partition pruning → только 7 партиций
-- 2. Sort order → sequential read, no sorting needed
-- Latency: ~500ms p50, ~2s p99
Customer 360¶
-- Оптимизировано с sort order + z-order
SELECT
customer_id,
COUNT(*) as total_orders,
SUM(total_amount) as total_spent,
MAX(created_at) as last_order
FROM sales.orders
WHERE customer_id = 'cust_123'
GROUP BY customer_id;
-- План запроса:
-- 1. Bloom filter на customer_id → skip большинство файлов
-- 2. Sort order → data локализована, sequential read
-- 3. Columnar format → читаем только нужные колонки
-- Latency: ~100ms
Aggregation¶
-- Columnar format shine
SELECT
DATE(created_at) as date,
COUNT(*) as orders,
SUM(total_amount) as revenue,
AVG(total_amount) as avg_order_value
FROM sales.orders
WHERE created_at >= CURRENT_DATE - INTERVAL '30' DAY
GROUP BY DATE(created_at)
ORDER BY date DESC;
-- План запроса:
-- 1. Partition pruning → 30 партиций
-- 2. Columnar read → только created_at и total_amount колонки
-- 3. Vectorized aggregation
-- Latency: ~2s p50, ~10s p99
9. Resources¶
Documentation¶
Tools¶
- Iceberg CLI - Command line tool
- PyIceberg - Python library
- Iceberg REST Catalog
Monitoring¶
- Trino Web UI: Query plans и performance
- Grafana + Prometheus: Table metrics
- S3 Access Logs: Storage costs
Last Updated: 2026-01-23
Owner: Data Platform Team