Перейти к содержанию

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

  1. Target file size: 512 MB (balance между parallelism и overhead)
  2. Compaction: Daily для hot partitions, weekly для warm
  3. Snapshots: Expire регулярно, держать 10-20 для debugging
  4. Bloom filters: Добавлять для ID колонок с point lookups
  5. Sort order: Первая колонка = frequently filtered
  6. Partition evolution: Начинать с coarse (monthly), переходить на finer (daily) по мере роста
  7. Z-ordering: Для multi-dimensional queries (monthly rewrite)

❌ DON'T

  1. Слишком много партиций (>10,000) - slow metadata
  2. Слишком маленькие файлы (<128 MB) - slow queries
  3. Слишком большие файлы (>1 GB) - low parallelism
  4. Игнорировать small files - приводит к performance degradation
  5. Не expire snapshots - metadata растёт indefinitely
  6. Partition по high cardinality без bucketing - too many partitions
  7. 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

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