32.9 C
Jakarta
Senin, 9 Juni 2025

Mengoptimalkan tabel Apache Iceberg untuk real time analytics

Apache Iceberg memiliki semua fitur yang dibutuhkan untuk analisis berkinerja tinggi, tetapi keberhasilannya bergantung pada cara Anda menggunakannya.

Meskipun Iceberg unggul dalam beban kerja analitis, mengadaptasinya untuk analisis waktu nyata memerlukan pemahaman tentang karakteristik kinerja dan kelebihannya.

Kesalahan paling umum yang dilakukan teknisi adalah:
– Mengabaikan hal-hal mendasar: Pemartisian dan pengurutan yang tepat dapat mempercepat kueri hingga 1000 kali lipat
– Mengoptimalkan sebelum memahami: Selalu menganalisis pola kueri terlebih dahulu, baru kemudian mengoptimalkan
– Menganggap lebih banyak fitur = performa yang lebih baik: Setiap pengoptimalan memiliki kelebihan dan kekurangan
– Tidak memahami tantangan streaming: File kecil, ledakan metadata, dan beban pemadatan
Mari jelajahi fitur partisi, penyortiran, dan pemadatan Iceberg untuk membangun sistem analitik real-time berkinerja tinggi.

1. Partitioning

Partitioning adalah cara untuk mengatur data Anda ke dalam kelompok-kelompok logis. Anggaplah pemartisian sebagai “Di folder mana saya harus mencarinya?”

Pembagian waktu dasar

-- Partition by day for time-based queries
CREATE TABLE ecommerce_events (
  event_id BIGINT,
  user_id BIGINT,
  event_time TIMESTAMP,
  event_type STRING,
  product_id BIGINT,
  region STRING,
  session_id STRING,
  revenue DECIMAL(10,2)
) USING ICEBERG
PARTITIONED BY (days(event_time));

Struktur direktori

ecommerce_events/
├── event_time_day=2025-01-15/
│   └── data files
├── event_time_day=2025-01-16/
│   └── data files
└── event_time_day=2025-01-17/
    └── data files

Sample query

SELECT COUNT(*) FROM ecommerce_events
WHERE event_time = '2025-01-15';
-- Files read: Only event_time_day=2025-01-15 ✅

Query plan

-- Query plan shows partition filters:
== Physical Plan ==
... PartitionFilters: [isnotnull(event_time_day), (event_time_day = 2025-01-15)]

Multi-dimensional partitioning

-- Partition by time AND region for better pruning
CREATE TABLE ecommerce_events (
  event_id BIGINT,
  user_id BIGINT,
  event_time TIMESTAMP,
  event_type STRING,
  product_id BIGINT,
  region STRING,
  session_id STRING,
  revenue DECIMAL(10,2)
) USING ICEBERG
PARTITIONED BY (
  days(event_time),
  region,
  bucket(32, user_id)  -- Hash bucketing for load distribution
);

Directory structur

ecommerce_events/
├── event_time_day=2025-01-15/
│   ├── region=US/
│   │   ├── user_id_bucket=0/
│   │   │   ├── part-00000.parquet
│   │   │   └── part-00001.parquet
│   │   ├── user_id_bucket=1/
│   │   │   └── part-00002.parquet
│   │   └── ... (buckets 2-31)
│   ├── region=EU/
│   │   ├── user_id_bucket=0/
│   │   └── ... (buckets 1-31)
├── event_time_day=2025-01-16/
│   ├── region=US/
│   │   ├── user_id_bucket=0/
│   │   └── ... (buckets 1-31)
│   ├── region=EU/
│   └── region=APAC/
└── event_time_day=2025-01-17/
    ├── region=US/
    ├── region=EU/
    └── region=APAC/

Sample query

SELECT COUNT(*) FROM ecommerce_events
WHERE event_time = '2025-01-15' AND region = 'US';
-- Files read: Only event_time_day=2025-01-15/region=US/ ✅

Query plan

...
PartitionFilters: [
  isnotnull(event_time_day#999),
  (event_time_day#999 = 2025-01-15),     ← Time partition elimination
  isnotnull(region#789),
  (region#789 = US)                      ← Region partition elimination
]
...
PartitionsRead: 32 (out of 15,360 total partitions)
FilesRead: 64 (out of 245,760 total files)

Selalu tentukan partisi berdasarkan pola kueri aktual. Beberapa contoh:
Untuk kueri deret waktu

PARTITIONED BY (
  days(event_time),        -- Primary: time filtering (days or months depending on volume)
  bucket(8, user_id)       -- Secondary: load balancing only
)

Untuk pertanyaan multi-penyewa

PARTITIONED BY (
  tenant_id,               -- Primary: perfect isolation
  days(event_time),        -- Secondary: time pruning
  bucket(4, user_id)       -- Tertiary: small buckets
)

Untuk pertanyaan yang berfokus pada wilayah:

PARTITIONED BY (
  region,                  -- Primary: geographic filtering
  days(event_time),        -- Secondary: time pruning
  bucket(16, user_id)      -- Tertiary: parallelism
)

Aturan praktis saat mempartisi tabel Iceberg

– Maksimal 2-3 kolom partisi
– 10-100 file per partisi
– Total ukuran 1GB-100GB per partisi
– Pantau dan kembangkan partisi saat diperlukan

Anti-patterns
Terlalu banyak partisi kecil:

-- BAD: Creates tiny partitions
PARTITIONED BY (
  hours(event_time),       -- 24 partitions per day
  region,                  -- × 10 regions
  event_type,              -- × 20 event types
  user_segment             -- × 5 segments
)
-- Result: 24,000 tiny partitions per day

Partisi kardinalitas tinggi:

-- BAD: Partition explosion
PARTITIONED BY (
  user_id,                 -- Millions of partitions
  session_id               -- Even more partitions
)
-- Result: Metadata larger than data

Mengabaikan pola kueri:

-- BAD: Partitioned by write pattern, not read pattern
PARTITIONED BY (
  ingestion_batch_id       -- How data arrives
)
-- But queries filter by:
WHERE event_time > '...' AND region = '...'  -- Different columns!

Kapan harus melakukan partisi ulang

Periksa metadata tabel dan pola kueri Anda secara berkala untuk mengembangkan partisi.

-- Partition health check
WITH partition_stats AS (
  SELECT
    partition,
    COUNT(*) as file_count,
    SUM(file_size_in_bytes) as partition_bytes,
    AVG(file_size_in_bytes) as avg_file_bytes
  FROM table_name.files
  GROUP BY partition
)
SELECT
  COUNT(*) as total_partitions,
  AVG(file_count) as avg_files_per_partition,
  MAX(file_count) as max_files_per_partition,
  AVG(partition_bytes) / (1024*1024*1024) as avg_partition_gb,
  COUNT(CASE WHEN file_count > 1000 THEN 1 END) as problematic_partitions
FROM partition_stats;

– Terlalu banyak file per partisi: > 1.000 file → Tambahkan bucketing
– Terlalu sedikit file per partisi: < 5 file → Kurangi granularitas waktu - Partisi miring: 1 partisi > 10× rata-rata → Tambahkan sub-partisi
– Performa kueri menurun: → Sejajarkan partisi dengan pola kueri

2. Sorting

Sorting adalah cara untuk mengatur data Anda ke dalam urutan tertentu. Anggap sorting sebagai cara untuk “melewati file dalam partisi dan blok dalam file”

Regular sorting

-- Sort by primary access pattern
-- This creates a strict ordering: first by user_id, then by event_time
CALL catalog.system.rewrite_data_files('db.ecommerce_events',
  strategy => 'sort',
  sort_order => 'user_id, event_time'
);

Tata letak berkas setelah penyortiran rutin:

user_id	event_time
1	10:00
1	10:10
1	10:30
2	10:15
2	10:25
2	10:35
3	10:05
3	10:20

Semua catatan user_id disatukan, lalu event_time
– Kunci sortir utama: [■■■■■■■■■■] ← Pengelompokan yang sangat baik
– Kunci sortir sekunder: [■ ■ ■ ■ ■] ← Tersebar di seluruh berkas

Kapan harus menggunakan pengurutan reguler:
Pola akses utama tunggal (misalnya, 90% kueri difilter menurut event_time)

SELECT * FROM ecommerce_events WHERE event_time BETWEEN '2025-01-01' AND '2025-01-02';

Hierarchical access (tenant → date → user)

SELECT * FROM multi_tenant_events
WHERE tenant_id = 'company_a' AND date >= '2025-01-01' AND user_id = 12345;

Rangkaian waktu dengan akses data sebagian besar terkini

sort_order => 'timestamp DESC'  -- Most recent data first

Z-Ordering untuk kueri multidimensi

Z-order untuk pemfilteran kueri pada beberapa dimensi. Data disisipkan untuk mempertahankan lokalitas di kedua dimensi

CALL catalog.system.rewrite_data_files('db.ecommerce_events',
  strategy => 'sort',
  sort_order => 'zorder(user_id, event_time)'
);

Tata letak berkas setelah z-ordering:

user_id	event_time
1	10:00
2	10:15
1	10:10
3	10:05
1	10:30
2	10:25
3	10:20
2	10:35

Semua kunci sortir: [■■■ ■■■ ■■■] ← Pengelompokan seimbang

Kapan menggunakan z-ordering:
Kueri difilter pada 2-4 kolom secara bersamaan

SELECT * FROM ecommerce_events WHERE user_id = 12345 AND event_time > '2025-01-01';
SELECT * FROM ecommerce_events WHERE user_id BETWEEN 1000 AND 2000 AND event_time BETWEEN '2025-01-01' AND '2025-01-31';

Kueri rentang pada beberapa kolom (seperti data geospasial)

SELECT * FROM locations WHERE latitude BETWEEN 40.0 AND 41.0 AND longitude BETWEEN -74.0 AND -73.0;

Kombinasi berkardinalitas tinggi

SELECT * FROM purchases
WHERE user_id IN (1,2,3) AND product_id IN (100,200) AND timestamp > '2025-01-01';

Pengurutan Z mengorbankan sebagian performa satu dimensi untuk performa multidimensi yang lebih baik, sementara pengurutan biasa mengoptimalkan kolom pengurutan utama dengan mengorbankan kolom sekunder.

Baca Juga:
Membuat Replikasi Master to Master di MySQL

File-Level statistics impact

-- Before sorting: user_id randomly distributed
-- File 1: user_id range [1-1000, 5000-6000] (fragmented)
-- File 2: user_id range [2000-3000, 500-800] (fragmented)

-- After sorting: clean ranges
-- File 1: user_id range [1-1000] ← min=1, max=1000
-- File 2: user_id range [1001-2000] ← min=1001, max=2000

SELECT * FROM ecommerce_events
WHERE user_id = 150 AND event_time >= '2025-01-15';
-- Only reads File 1 (150 is between 1-1000) ✅

3. Pemadatan

Partisi Iceberg terdiri dari berkas-berkas yang tidak dapat diubah, penulisan dapat menghasilkan banyak berkas kecil, yang akan mengganggu kinerja kueri.

Pemadatan adalah suatu proses, yang biasanya berjalan di latar belakang, untuk menggabungkan beberapa berkas kecil menjadi berkas yang lebih besar.

Prosesnya mungkin terlihat sederhana; jalankan saja proses pemadatan:

-- Compact all small files
CALL catalog.system.rewrite_data_files('db.ecommerce_events',
  strategy => 'binpack',
  options => map('target-file-size-bytes', '268435456') -- 256MB
);

Namun pada kenyataannya, hal ini mahal. Sebaliknya, Anda ingin menjalankan pemadatan hanya pada file-file yang diperlukan (kecuali jika Anda ingin menulis ulang tabel lengkap karena beberapa evolusi skema):

-- Compact only files from the last hour
CALL catalog.system.rewrite_data_files(
  table => 'db.ecommerce_events',
  strategy => 'binpack',
  where => 'event_time >= current_timestamp() - INTERVAL 1 HOUR',
  options => map(
    'target-file-size-bytes', '268435456',
    'min-input-files', '5',  -- Only compact if at least 5 files
    'max-concurrent-file-group-rewrites', '5' -- Parallelism level
  )
);

Dalam beban kerja dunia nyata, Anda harus lebih cerdas dan menyesuaikan logika pemadatan sehingga hanya berjalan saat diperlukan:

-- Check if compaction is needed
WITH small_files_check AS (
  SELECT
    COUNT(*) as small_file_count,
    AVG(file_size_in_bytes) as avg_size_bytes
  FROM ecommerce_events.files
  WHERE file_size_in_bytes < 67108864  -- < 64MB
    AND partition LIKE '%2025-01-15%'  -- Today's data
)
SELECT
  CASE
    WHEN small_file_count >= 10 THEN 'COMPACT_RECOMMENDED'
    ELSE 'NO_ACTION_NEEDED'
  END as recommendation
FROM small_files_check;

Optimasi metadata

Ledakan file metadata dapat merusak kinerja. Dua praktik yang baik di sini:
– Kedaluwarsa snapshot
– Kompak file manifest

-- Enable snapshot expiration for faster metadata reads
ALTER TABLE ecommerce_events SET TBLPROPERTIES (
  'history.expire.min-snapshots-to-keep' = '5',
  'history.expire.max-snapshot-age-ms' = '86400000' -- 1 day
);

-- Compact manifests regularly
CALL catalog.system.rewrite_manifests('db.ecommerce_events');

Tumpukan Penghapusan Data 6 Tingkat

Saat Anda menggabungkan partisi, penyortiran, dan pemadatan, Iceberg menciptakan sistem penghapusan data 6 tingkat yang canggih.

Mari kita telusuri bagaimana satu kueri dioptimalkan:

SELECT user_id, event_type
FROM ecommerce_events
WHERE event_time = '2025-01-15' AND user_id = 150;

Level 1: Pemangkasan partisi

– Iceberg melewati seluruh direktori berdasarkan kolom partisi
– Hanya membaca partisi yang event_time = ‘2025-01-15’

-- Table structure:
ecommerce_events/
├── event_time_day=2025-01-14/  ← SKIP (date < 2024-01-15)
├── event_time_day=2025-01-15/  ← READ (matches filter)
├── event_time_day=2025-01-16/  ← SKIP (not needed)
└── event_time_day=2025-01-17/  ← SKIP (not needed)

-- Result: Skip entire directories based on partition columns
-- Data eliminated: 75% (3 out of 4 days)

Level 2: Penghapusan berkas

- Dalam event_time_day=2025-01-15/, lewati berkas yang rentang user_id-nya tidak mencakup 150
- Menggunakan statistik tingkat berkas (nilai min/maks) untuk melewati berkas

-- Within event_time_day=2025-01-15/:
├── file_001.parquet  ← user_id range [1-1000]     ← READ (150 is in range)
├── file_002.parquet  ← user_id range [1001-2000]  ← SKIP (150 < 1001)
├── file_003.parquet  ← user_id range [2001-3000]  ← SKIP (150 < 2001)
└── file_004.parquet  ← user_id range [3001-4000]  ← SKIP (150 < 3001)

-- Result: Skip files using min/max statistics
-- Data eliminated: 75% more (3 out of 4 files)

Level 3: Penghapusan grup baris (di dalam parket)

- Di dalam file yang dipilih, lewati grup baris yang user_id min/max-nya tidak menyertakan 150
- Menggunakan statistik kolom Parket

-- Within file_001.parquet:
├── Row Group 1  ← user_id range [1-500]    ← READ (150 is in range)
├── Row Group 2  ← user_id range [501-1000] ← SKIP (150 < 501)
└── Row Group 3  ← user_id range [1001-1500] ← SKIP (150 < 1001)

-- Result: Skip row groups using Parquet statistics
-- Data eliminated: 66% more (2 out of 3 row groups)

Level 4: Penghapusan kolom (pushdown proyeksi)

Hanya baca potongan kolom user_id dan event_type, lewati yang lain

-- Row Group 1 contains columns:
├── user_id column chunk     ← READ (needed for SELECT)
├── event_type column chunk  ← READ (needed for SELECT)
├── event_time column chunk  ← SKIP (only used in WHERE, already filtered)
├── product_id column chunk  ← SKIP (not needed)
├── revenue column chunk     ← SKIP (not needed)
└── session_id column chunk  ← SKIP (not needed)

-- Result: Only deserialize needed columns
-- Data eliminated: 66% more (4 out of 6 columns)

Level 5: Penghapusan halaman (dalam grup baris)

Dalam grup baris, lewati halaman yang rentang user_id-nya tidak mencakup 150

-- Within user_id column chunk:
├── Page 1  ← user_id range [1-100]   ← SKIP (150 > 100)
├── Page 2  ← user_id range [101-200] ← READ (150 is in range)
├── Page 3  ← user_id range [201-300] ← SKIP (150 < 201)
└── Page 4  ← user_id range [301-400] ← SKIP (150 < 301)

-- Result: Skip pages within column chunks
-- Data eliminated: 75% more (3 out of 4 pages)

Level 6: Penyaringan baris (pasca-deserialisasi)

Penting: Tidak seperti 5 level sebelumnya, ini adalah penyaringan, bukan penghapusan.
- Seluruh halaman harus dibaca dan dideserialisasi dari penyimpanan
- Penyaringan tingkat baris terjadi di memori setelah membaca halaman

-- Within Page 2 (user_id range [101-200]):
├── Read entire page from storage ← ALL rows [101-200] deserialized
├── Apply filter in memory: user_id = 150
└── Return only matching row(s)

-- Result: I/O saved = 0% (entire page must be read)
-- Processing saved = ~99% (only matching rows processed further)

Dampak kinerja
Berikut ini adalah dampak kumulatif dari pengoptimalan 6 tingkat:

Level	Elimination Target	Data Remaining	Cumulative I/O Savings
Original Dataset	-	1TB (100%)	-
1. Partition	Skip directories	250GB (25%)	75% saved
2. File	Skip files	62GB (6.25%)	94% saved
3. Row Group	Skip row groups	21GB (2.1%)	98% saved
4. Column	Skip columns	7GB (0.7%)	99.3% saved
5. Page	Skip pages	1.7GB (0.17%)	99.8% saved
6. Row	Filter rows	1.7GB (0.17%)	99.8% I/O saved

Level 1-5: Penghapusan data yang sebenarnya
- Level ini tidak membaca data sama sekali dari penyimpanan
- Memberikan penghematan I/O dan penghematan pemrosesan
- Dari sinilah peningkatan kinerja yang sangat besar berasal

Baca Juga:
Google membuat AlloyDB untuk PostgreSQL Baru tersedia di 16 wilayah

Level 6: Penyaringan dalam memori
- Tetap membaca data dari penyimpanan (tanpa penghematan I/O tambahan)
- Memfilter baris yang tidak diinginkan dalam memori setelah deserialisasi
- Memberikan penghematan CPU/memori tetapi bukan penghematan I/O
- Ini adalah penyaringan tingkat baris SQL tradisional
Hasil Akhir: Membaca 1,7 GB, bukan 1 TB - itu berarti 99,8% lebih sedikit I/O!

Saat Iceberg tidak cukup

Meskipun pengoptimalan di atas berfungsi dengan baik untuk beban kerja analitis, streaming dan analitik waktu nyata memperlihatkan keterbatasan mendasar dalam arsitektur Iceberg.

Penulisan streaming ke Iceberg menimbulkan beberapa tantangan mendasar karena arsitekturnya didasarkan pada snapshot yang tidak dapat diubah.

Tantangan 1: Ledakan file kecil

# Streaming scenario: New ecommerce_events every second
stream.writeStream \
  .format("iceberg") \
  .trigger(processingTime="1 second") \
  .start()

# Results in:
# ecommerce_events/
# ├── file_001.parquet (1MB - 1 second of data)
# ├── file_002.parquet (1MB - next second)
# ├── file_003.parquet (1MB - next second)
# └── ... (86,400 tiny files per day!)

Karena file Iceberg tidak dapat diubah, setiap mikro-batch membuat file baru alih-alih menambahkannya ke file yang sudah ada.

Ukuran file optimal adalah 128MB-1GB, tetapi streaming membuat file berukuran 1-10MB - 100x lebih banyak dari ukuran optimal.

Tantangan 2: Ledakan metadata

File metadata ditulis pada setiap file baru yang ditulis.

// After 1 hour of streaming (3,600 commits):
{
  "snapshots": [
    {"snapshot-id": 1, "manifest-list": "snap-001.avro"},
    {"snapshot-id": 2, "manifest-list": "snap-002.avro"},
    // ... 3,598 more snapshots
    {"snapshot-id": 3600, "manifest-list": "snap-3600.avro"}
  ]
}

Metadata tabel tumbuh secara linear seiring dengan komitmen. Perencanaan kueri menjadi lebih lambat daripada kueri aktual karena memindai ribuan manifes.

Setelah satu jam, kueri sederhana seperti ini harus membuka ribuan file kecil:

SELECT COUNT(*) FROM ecommerce_events WHERE date = '2025-01-15';

Tantangan 3: Jendela kinerja pemadatan

Pemadatan mengatasi masalah ledakan file kecil dan metadata, tetapi menimbulkan masalah lain dalam skenario streaming, seperti jendela kinerja yang menurun.

-- Timeline of degraded performance:
-- 10:00 - Stream starts writing small files
-- 10:30 - 1,800 small files created (30 min × 1/sec)
-- 10:30 - Background compaction starts
-- 10:35 - Compaction completes, creates 2 large files

-- Query at 10:29: Reads 1,800 small files (SLOW - 45 seconds)
-- Query at 10:36: Reads 2 large files (FAST - 3 seconds)
-- Problem: 30-minute window of degraded performance!

Tantangan 4: Konflik penulis serentak

Penulisan skala menyiratkan beberapa pekerja serentak menulis ke tabel Iceberg yang sama.

Karena sifat optimis dari konkurensi di Iceberg, beberapa pekerjaan streaming yang menulis ke tabel yang sama dapat berkonflik, yang dapat menyebabkan percobaan ulang dan penundaan yang eksponensial.

# Job 1: Processing user ecommerce_events
# Job 2: Processing system ecommerce_events
# Job 3: Processing audit ecommerce_events

# All trying to commit simultaneously:
# Commit 1: snapshot-1234 → SUCCESS
# Commit 2: snapshot-1234 → CONFLICT! (optimistic concurrency)
# Commit 3: snapshot-1234 → CONFLICT!

# Jobs 2 & 3 must retry, causing delays and wasted work

Tantangan 5: Agregasi waktu nyata dan beberapa indeks

Aplikasi nyata memiliki beberapa pola kueri yang memerlukan urutan penyortiran yang berbeda:

-- Query Pattern 1: User analytics (90% of queries)
SELECT * FROM ecommerce_events WHERE user_id = 12345 AND event_time > '2025-01-01';
-- Needs: ORDER BY user_id, event_time

-- Query Pattern 2: Product analytics (5% of queries)
SELECT * FROM ecommerce_events WHERE product_id = 789 AND event_time > '2025-01-01';
-- Needs: ORDER BY product_id, event_time

-- Query Pattern 3: Geographic analysis (5% of queries)
SELECT * FROM ecommerce_events WHERE region = 'US' AND event_type = 'purchase';
-- Needs: ORDER BY region, event_type

dan agregasi yang telah dihitung sebelumnya:

-- Users want real-time dashboards showing:
SELECT
  region,
  event_type,
  COUNT(*) as ecommerce_events,
  SUM(revenue) as total_revenue,
  COUNT(DISTINCT user_id) as unique_users
FROM ecommerce_events
WHERE event_time >= current_timestamp() - INTERVAL 1 HOUR
GROUP BY region, event_type;

Pendekatan Iceberg: Jalankan kueri agregasi setiap kali

-- Manual pre-aggregation with Iceberg
CREATE TABLE hourly_region_stats AS
SELECT
  date_trunc('hour', event_time) as hour,
  region,
  event_type,
  COUNT(*) as ecommerce_events,
  SUM(revenue) as total_revenue
FROM ecommerce_events
GROUP BY date_trunc('hour', event_time), region, event_type;

-- But now you need to:
-- 1. Keep it in sync with source table
-- 2. Handle late-arriving data
-- 3. Manage incremental updates
-- 4. Deal with duplicate processing

Platform Analisis Real-Time Khusus

Apache Iceberg menyediakan fitur dasar untuk beban kerja analitik berkinerja tinggi dan dapat diskalakan, tetapi analitik real-time yang berhadapan dengan pengguna memerlukan alat khusus.

Gunakan Iceberg saat
- Analitik batch dengan penulisan yang jarang
- Skenario data lake dengan ETL yang kompleks
- Evolusi skema dan perjalanan waktu sangat penting
- Latensi kueri 5-30 detik dapat diterima

Gunakan platform khusus saat
- Aplikasi real-time memerlukan kueri sub-detik yang konsisten dan konkurensi tinggi
- Penulisan streaming frekuensi tinggi (>1000 peristiwa/detik)
- Beberapa pola kueri memerlukan indeks yang berbeda
- Pra-agregasi harus diperbarui secara bertahap
- Alur kerja yang ramah bagi pengembang

┌─────────────┐    ┌────────────────────────┐    ┌─────────────────────┐
│             │    │                        │    │                     │
│ Events      │    │ Tinybird               │    │ Iceberg             │
│ stream      │--->│ (Real-time analytics)  │<---│ (Long-term storage) │
│             │  │ │                        │  │ │                     │
└─────────────┘  │ └────────────────────────┘  │ └─────────────────────┘
           Kafka ┘             │               └ iceberg() function
                               │ real-time API
                               │
                   ┌────────────────────────┐
                   │                        │
                   │ Real-time              │
                   │ application            │
                   │                        │
                   └────────────────────────┘

Tinybird adalah platform analitik real-time yang dapat menyerap aliran peristiwa dan tabel iceberge. Untuk mempelajari lebih lanjut tentang pola ini, Anda dapat membaca posting blog Iceberg + Redpanda + Tinybird ini, yang menjelaskan cara memanfaatkan penyerapan streaming Kafka asli dan tampilan materialisasi inkremental untuk API publik dalam waktu kurang dari satu detik sambil menggunakan Apache Iceberg sebagai penyimpanan tahan lama untuk beban kerja analitik.






Reporter: Nyoman Artawa Wiguna

Apache Iceberg memiliki semua fitur yang dibutuhkan untuk analisis berkinerja tinggi, tetapi keberhasilannya bergantung pada cara Anda menggunakannya.

Meskipun Iceberg unggul dalam beban kerja analitis, mengadaptasinya untuk analisis waktu nyata memerlukan pemahaman tentang karakteristik kinerja dan kelebihannya.

Kesalahan paling umum yang dilakukan teknisi adalah:
– Mengabaikan hal-hal mendasar: Pemartisian dan pengurutan yang tepat dapat mempercepat kueri hingga 1000 kali lipat
– Mengoptimalkan sebelum memahami: Selalu menganalisis pola kueri terlebih dahulu, baru kemudian mengoptimalkan
– Menganggap lebih banyak fitur = performa yang lebih baik: Setiap pengoptimalan memiliki kelebihan dan kekurangan
– Tidak memahami tantangan streaming: File kecil, ledakan metadata, dan beban pemadatan
Mari jelajahi fitur partisi, penyortiran, dan pemadatan Iceberg untuk membangun sistem analitik real-time berkinerja tinggi.

1. Partitioning

Partitioning adalah cara untuk mengatur data Anda ke dalam kelompok-kelompok logis. Anggaplah pemartisian sebagai “Di folder mana saya harus mencarinya?”

Pembagian waktu dasar

-- Partition by day for time-based queries
CREATE TABLE ecommerce_events (
  event_id BIGINT,
  user_id BIGINT,
  event_time TIMESTAMP,
  event_type STRING,
  product_id BIGINT,
  region STRING,
  session_id STRING,
  revenue DECIMAL(10,2)
) USING ICEBERG
PARTITIONED BY (days(event_time));

Struktur direktori

ecommerce_events/
├── event_time_day=2025-01-15/
│   └── data files
├── event_time_day=2025-01-16/
│   └── data files
└── event_time_day=2025-01-17/
    └── data files

Sample query

SELECT COUNT(*) FROM ecommerce_events
WHERE event_time = '2025-01-15';
-- Files read: Only event_time_day=2025-01-15 ✅

Query plan

-- Query plan shows partition filters:
== Physical Plan ==
... PartitionFilters: [isnotnull(event_time_day), (event_time_day = 2025-01-15)]

Multi-dimensional partitioning

-- Partition by time AND region for better pruning
CREATE TABLE ecommerce_events (
  event_id BIGINT,
  user_id BIGINT,
  event_time TIMESTAMP,
  event_type STRING,
  product_id BIGINT,
  region STRING,
  session_id STRING,
  revenue DECIMAL(10,2)
) USING ICEBERG
PARTITIONED BY (
  days(event_time),
  region,
  bucket(32, user_id)  -- Hash bucketing for load distribution
);

Directory structur

ecommerce_events/
├── event_time_day=2025-01-15/
│   ├── region=US/
│   │   ├── user_id_bucket=0/
│   │   │   ├── part-00000.parquet
│   │   │   └── part-00001.parquet
│   │   ├── user_id_bucket=1/
│   │   │   └── part-00002.parquet
│   │   └── ... (buckets 2-31)
│   ├── region=EU/
│   │   ├── user_id_bucket=0/
│   │   └── ... (buckets 1-31)
├── event_time_day=2025-01-16/
│   ├── region=US/
│   │   ├── user_id_bucket=0/
│   │   └── ... (buckets 1-31)
│   ├── region=EU/
│   └── region=APAC/
└── event_time_day=2025-01-17/
    ├── region=US/
    ├── region=EU/
    └── region=APAC/

Sample query

SELECT COUNT(*) FROM ecommerce_events
WHERE event_time = '2025-01-15' AND region = 'US';
-- Files read: Only event_time_day=2025-01-15/region=US/ ✅

Query plan

...
PartitionFilters: [
  isnotnull(event_time_day#999),
  (event_time_day#999 = 2025-01-15),     ← Time partition elimination
  isnotnull(region#789),
  (region#789 = US)                      ← Region partition elimination
]
...
PartitionsRead: 32 (out of 15,360 total partitions)
FilesRead: 64 (out of 245,760 total files)

Selalu tentukan partisi berdasarkan pola kueri aktual. Beberapa contoh:
Untuk kueri deret waktu

PARTITIONED BY (
  days(event_time),        -- Primary: time filtering (days or months depending on volume)
  bucket(8, user_id)       -- Secondary: load balancing only
)

Untuk pertanyaan multi-penyewa

PARTITIONED BY (
  tenant_id,               -- Primary: perfect isolation
  days(event_time),        -- Secondary: time pruning
  bucket(4, user_id)       -- Tertiary: small buckets
)

Untuk pertanyaan yang berfokus pada wilayah:

PARTITIONED BY (
  region,                  -- Primary: geographic filtering
  days(event_time),        -- Secondary: time pruning
  bucket(16, user_id)      -- Tertiary: parallelism
)

Aturan praktis saat mempartisi tabel Iceberg

– Maksimal 2-3 kolom partisi
– 10-100 file per partisi
– Total ukuran 1GB-100GB per partisi
– Pantau dan kembangkan partisi saat diperlukan

Anti-patterns
Terlalu banyak partisi kecil:

-- BAD: Creates tiny partitions
PARTITIONED BY (
  hours(event_time),       -- 24 partitions per day
  region,                  -- × 10 regions
  event_type,              -- × 20 event types
  user_segment             -- × 5 segments
)
-- Result: 24,000 tiny partitions per day

Partisi kardinalitas tinggi:

-- BAD: Partition explosion
PARTITIONED BY (
  user_id,                 -- Millions of partitions
  session_id               -- Even more partitions
)
-- Result: Metadata larger than data

Mengabaikan pola kueri:

-- BAD: Partitioned by write pattern, not read pattern
PARTITIONED BY (
  ingestion_batch_id       -- How data arrives
)
-- But queries filter by:
WHERE event_time > '...' AND region = '...'  -- Different columns!

Kapan harus melakukan partisi ulang

Periksa metadata tabel dan pola kueri Anda secara berkala untuk mengembangkan partisi.

-- Partition health check
WITH partition_stats AS (
  SELECT
    partition,
    COUNT(*) as file_count,
    SUM(file_size_in_bytes) as partition_bytes,
    AVG(file_size_in_bytes) as avg_file_bytes
  FROM table_name.files
  GROUP BY partition
)
SELECT
  COUNT(*) as total_partitions,
  AVG(file_count) as avg_files_per_partition,
  MAX(file_count) as max_files_per_partition,
  AVG(partition_bytes) / (1024*1024*1024) as avg_partition_gb,
  COUNT(CASE WHEN file_count > 1000 THEN 1 END) as problematic_partitions
FROM partition_stats;

– Terlalu banyak file per partisi: > 1.000 file → Tambahkan bucketing
– Terlalu sedikit file per partisi: < 5 file → Kurangi granularitas waktu - Partisi miring: 1 partisi > 10× rata-rata → Tambahkan sub-partisi
– Performa kueri menurun: → Sejajarkan partisi dengan pola kueri

2. Sorting

Sorting adalah cara untuk mengatur data Anda ke dalam urutan tertentu. Anggap sorting sebagai cara untuk “melewati file dalam partisi dan blok dalam file”

Regular sorting

-- Sort by primary access pattern
-- This creates a strict ordering: first by user_id, then by event_time
CALL catalog.system.rewrite_data_files('db.ecommerce_events',
  strategy => 'sort',
  sort_order => 'user_id, event_time'
);

Tata letak berkas setelah penyortiran rutin:

user_id	event_time
1	10:00
1	10:10
1	10:30
2	10:15
2	10:25
2	10:35
3	10:05
3	10:20

Semua catatan user_id disatukan, lalu event_time
– Kunci sortir utama: [■■■■■■■■■■] ← Pengelompokan yang sangat baik
– Kunci sortir sekunder: [■ ■ ■ ■ ■] ← Tersebar di seluruh berkas

Kapan harus menggunakan pengurutan reguler:
Pola akses utama tunggal (misalnya, 90% kueri difilter menurut event_time)

SELECT * FROM ecommerce_events WHERE event_time BETWEEN '2025-01-01' AND '2025-01-02';

Hierarchical access (tenant → date → user)

SELECT * FROM multi_tenant_events
WHERE tenant_id = 'company_a' AND date >= '2025-01-01' AND user_id = 12345;

Rangkaian waktu dengan akses data sebagian besar terkini

sort_order => 'timestamp DESC'  -- Most recent data first

Z-Ordering untuk kueri multidimensi

Z-order untuk pemfilteran kueri pada beberapa dimensi. Data disisipkan untuk mempertahankan lokalitas di kedua dimensi

CALL catalog.system.rewrite_data_files('db.ecommerce_events',
  strategy => 'sort',
  sort_order => 'zorder(user_id, event_time)'
);

Tata letak berkas setelah z-ordering:

user_id	event_time
1	10:00
2	10:15
1	10:10
3	10:05
1	10:30
2	10:25
3	10:20
2	10:35

Semua kunci sortir: [■■■ ■■■ ■■■] ← Pengelompokan seimbang

Kapan menggunakan z-ordering:
Kueri difilter pada 2-4 kolom secara bersamaan

SELECT * FROM ecommerce_events WHERE user_id = 12345 AND event_time > '2025-01-01';
SELECT * FROM ecommerce_events WHERE user_id BETWEEN 1000 AND 2000 AND event_time BETWEEN '2025-01-01' AND '2025-01-31';

Kueri rentang pada beberapa kolom (seperti data geospasial)

SELECT * FROM locations WHERE latitude BETWEEN 40.0 AND 41.0 AND longitude BETWEEN -74.0 AND -73.0;

Kombinasi berkardinalitas tinggi

SELECT * FROM purchases
WHERE user_id IN (1,2,3) AND product_id IN (100,200) AND timestamp > '2025-01-01';

Pengurutan Z mengorbankan sebagian performa satu dimensi untuk performa multidimensi yang lebih baik, sementara pengurutan biasa mengoptimalkan kolom pengurutan utama dengan mengorbankan kolom sekunder.

Baca Juga:
Instalasi MariaDB Server di MacOS Menggunakan Homebrew

File-Level statistics impact

-- Before sorting: user_id randomly distributed
-- File 1: user_id range [1-1000, 5000-6000] (fragmented)
-- File 2: user_id range [2000-3000, 500-800] (fragmented)

-- After sorting: clean ranges
-- File 1: user_id range [1-1000] ← min=1, max=1000
-- File 2: user_id range [1001-2000] ← min=1001, max=2000

SELECT * FROM ecommerce_events
WHERE user_id = 150 AND event_time >= '2025-01-15';
-- Only reads File 1 (150 is between 1-1000) ✅

3. Pemadatan

Partisi Iceberg terdiri dari berkas-berkas yang tidak dapat diubah, penulisan dapat menghasilkan banyak berkas kecil, yang akan mengganggu kinerja kueri.

Pemadatan adalah suatu proses, yang biasanya berjalan di latar belakang, untuk menggabungkan beberapa berkas kecil menjadi berkas yang lebih besar.

Prosesnya mungkin terlihat sederhana; jalankan saja proses pemadatan:

-- Compact all small files
CALL catalog.system.rewrite_data_files('db.ecommerce_events',
  strategy => 'binpack',
  options => map('target-file-size-bytes', '268435456') -- 256MB
);

Namun pada kenyataannya, hal ini mahal. Sebaliknya, Anda ingin menjalankan pemadatan hanya pada file-file yang diperlukan (kecuali jika Anda ingin menulis ulang tabel lengkap karena beberapa evolusi skema):

-- Compact only files from the last hour
CALL catalog.system.rewrite_data_files(
  table => 'db.ecommerce_events',
  strategy => 'binpack',
  where => 'event_time >= current_timestamp() - INTERVAL 1 HOUR',
  options => map(
    'target-file-size-bytes', '268435456',
    'min-input-files', '5',  -- Only compact if at least 5 files
    'max-concurrent-file-group-rewrites', '5' -- Parallelism level
  )
);

Dalam beban kerja dunia nyata, Anda harus lebih cerdas dan menyesuaikan logika pemadatan sehingga hanya berjalan saat diperlukan:

-- Check if compaction is needed
WITH small_files_check AS (
  SELECT
    COUNT(*) as small_file_count,
    AVG(file_size_in_bytes) as avg_size_bytes
  FROM ecommerce_events.files
  WHERE file_size_in_bytes < 67108864  -- < 64MB
    AND partition LIKE '%2025-01-15%'  -- Today's data
)
SELECT
  CASE
    WHEN small_file_count >= 10 THEN 'COMPACT_RECOMMENDED'
    ELSE 'NO_ACTION_NEEDED'
  END as recommendation
FROM small_files_check;

Optimasi metadata

Ledakan file metadata dapat merusak kinerja. Dua praktik yang baik di sini:
– Kedaluwarsa snapshot
– Kompak file manifest

-- Enable snapshot expiration for faster metadata reads
ALTER TABLE ecommerce_events SET TBLPROPERTIES (
  'history.expire.min-snapshots-to-keep' = '5',
  'history.expire.max-snapshot-age-ms' = '86400000' -- 1 day
);

-- Compact manifests regularly
CALL catalog.system.rewrite_manifests('db.ecommerce_events');

Tumpukan Penghapusan Data 6 Tingkat

Saat Anda menggabungkan partisi, penyortiran, dan pemadatan, Iceberg menciptakan sistem penghapusan data 6 tingkat yang canggih.

Mari kita telusuri bagaimana satu kueri dioptimalkan:

SELECT user_id, event_type
FROM ecommerce_events
WHERE event_time = '2025-01-15' AND user_id = 150;

Level 1: Pemangkasan partisi

– Iceberg melewati seluruh direktori berdasarkan kolom partisi
– Hanya membaca partisi yang event_time = ‘2025-01-15’

-- Table structure:
ecommerce_events/
├── event_time_day=2025-01-14/  ← SKIP (date < 2024-01-15)
├── event_time_day=2025-01-15/  ← READ (matches filter)
├── event_time_day=2025-01-16/  ← SKIP (not needed)
└── event_time_day=2025-01-17/  ← SKIP (not needed)

-- Result: Skip entire directories based on partition columns
-- Data eliminated: 75% (3 out of 4 days)

Level 2: Penghapusan berkas

- Dalam event_time_day=2025-01-15/, lewati berkas yang rentang user_id-nya tidak mencakup 150
- Menggunakan statistik tingkat berkas (nilai min/maks) untuk melewati berkas

-- Within event_time_day=2025-01-15/:
├── file_001.parquet  ← user_id range [1-1000]     ← READ (150 is in range)
├── file_002.parquet  ← user_id range [1001-2000]  ← SKIP (150 < 1001)
├── file_003.parquet  ← user_id range [2001-3000]  ← SKIP (150 < 2001)
└── file_004.parquet  ← user_id range [3001-4000]  ← SKIP (150 < 3001)

-- Result: Skip files using min/max statistics
-- Data eliminated: 75% more (3 out of 4 files)

Level 3: Penghapusan grup baris (di dalam parket)

- Di dalam file yang dipilih, lewati grup baris yang user_id min/max-nya tidak menyertakan 150
- Menggunakan statistik kolom Parket

-- Within file_001.parquet:
├── Row Group 1  ← user_id range [1-500]    ← READ (150 is in range)
├── Row Group 2  ← user_id range [501-1000] ← SKIP (150 < 501)
└── Row Group 3  ← user_id range [1001-1500] ← SKIP (150 < 1001)

-- Result: Skip row groups using Parquet statistics
-- Data eliminated: 66% more (2 out of 3 row groups)

Level 4: Penghapusan kolom (pushdown proyeksi)

Hanya baca potongan kolom user_id dan event_type, lewati yang lain

-- Row Group 1 contains columns:
├── user_id column chunk     ← READ (needed for SELECT)
├── event_type column chunk  ← READ (needed for SELECT)
├── event_time column chunk  ← SKIP (only used in WHERE, already filtered)
├── product_id column chunk  ← SKIP (not needed)
├── revenue column chunk     ← SKIP (not needed)
└── session_id column chunk  ← SKIP (not needed)

-- Result: Only deserialize needed columns
-- Data eliminated: 66% more (4 out of 6 columns)

Level 5: Penghapusan halaman (dalam grup baris)

Dalam grup baris, lewati halaman yang rentang user_id-nya tidak mencakup 150

-- Within user_id column chunk:
├── Page 1  ← user_id range [1-100]   ← SKIP (150 > 100)
├── Page 2  ← user_id range [101-200] ← READ (150 is in range)
├── Page 3  ← user_id range [201-300] ← SKIP (150 < 201)
└── Page 4  ← user_id range [301-400] ← SKIP (150 < 301)

-- Result: Skip pages within column chunks
-- Data eliminated: 75% more (3 out of 4 pages)

Level 6: Penyaringan baris (pasca-deserialisasi)

Penting: Tidak seperti 5 level sebelumnya, ini adalah penyaringan, bukan penghapusan.
- Seluruh halaman harus dibaca dan dideserialisasi dari penyimpanan
- Penyaringan tingkat baris terjadi di memori setelah membaca halaman

-- Within Page 2 (user_id range [101-200]):
├── Read entire page from storage ← ALL rows [101-200] deserialized
├── Apply filter in memory: user_id = 150
└── Return only matching row(s)

-- Result: I/O saved = 0% (entire page must be read)
-- Processing saved = ~99% (only matching rows processed further)

Dampak kinerja
Berikut ini adalah dampak kumulatif dari pengoptimalan 6 tingkat:

Level	Elimination Target	Data Remaining	Cumulative I/O Savings
Original Dataset	-	1TB (100%)	-
1. Partition	Skip directories	250GB (25%)	75% saved
2. File	Skip files	62GB (6.25%)	94% saved
3. Row Group	Skip row groups	21GB (2.1%)	98% saved
4. Column	Skip columns	7GB (0.7%)	99.3% saved
5. Page	Skip pages	1.7GB (0.17%)	99.8% saved
6. Row	Filter rows	1.7GB (0.17%)	99.8% I/O saved

Level 1-5: Penghapusan data yang sebenarnya
- Level ini tidak membaca data sama sekali dari penyimpanan
- Memberikan penghematan I/O dan penghematan pemrosesan
- Dari sinilah peningkatan kinerja yang sangat besar berasal

Baca Juga:
Cara menginstal Nginx, MariaDB dan HHVM pada Debian 8

Level 6: Penyaringan dalam memori
- Tetap membaca data dari penyimpanan (tanpa penghematan I/O tambahan)
- Memfilter baris yang tidak diinginkan dalam memori setelah deserialisasi
- Memberikan penghematan CPU/memori tetapi bukan penghematan I/O
- Ini adalah penyaringan tingkat baris SQL tradisional
Hasil Akhir: Membaca 1,7 GB, bukan 1 TB - itu berarti 99,8% lebih sedikit I/O!

Saat Iceberg tidak cukup

Meskipun pengoptimalan di atas berfungsi dengan baik untuk beban kerja analitis, streaming dan analitik waktu nyata memperlihatkan keterbatasan mendasar dalam arsitektur Iceberg.

Penulisan streaming ke Iceberg menimbulkan beberapa tantangan mendasar karena arsitekturnya didasarkan pada snapshot yang tidak dapat diubah.

Tantangan 1: Ledakan file kecil

# Streaming scenario: New ecommerce_events every second
stream.writeStream \
  .format("iceberg") \
  .trigger(processingTime="1 second") \
  .start()

# Results in:
# ecommerce_events/
# ├── file_001.parquet (1MB - 1 second of data)
# ├── file_002.parquet (1MB - next second)
# ├── file_003.parquet (1MB - next second)
# └── ... (86,400 tiny files per day!)

Karena file Iceberg tidak dapat diubah, setiap mikro-batch membuat file baru alih-alih menambahkannya ke file yang sudah ada.

Ukuran file optimal adalah 128MB-1GB, tetapi streaming membuat file berukuran 1-10MB - 100x lebih banyak dari ukuran optimal.

Tantangan 2: Ledakan metadata

File metadata ditulis pada setiap file baru yang ditulis.

// After 1 hour of streaming (3,600 commits):
{
  "snapshots": [
    {"snapshot-id": 1, "manifest-list": "snap-001.avro"},
    {"snapshot-id": 2, "manifest-list": "snap-002.avro"},
    // ... 3,598 more snapshots
    {"snapshot-id": 3600, "manifest-list": "snap-3600.avro"}
  ]
}

Metadata tabel tumbuh secara linear seiring dengan komitmen. Perencanaan kueri menjadi lebih lambat daripada kueri aktual karena memindai ribuan manifes.

Setelah satu jam, kueri sederhana seperti ini harus membuka ribuan file kecil:

SELECT COUNT(*) FROM ecommerce_events WHERE date = '2025-01-15';

Tantangan 3: Jendela kinerja pemadatan

Pemadatan mengatasi masalah ledakan file kecil dan metadata, tetapi menimbulkan masalah lain dalam skenario streaming, seperti jendela kinerja yang menurun.

-- Timeline of degraded performance:
-- 10:00 - Stream starts writing small files
-- 10:30 - 1,800 small files created (30 min × 1/sec)
-- 10:30 - Background compaction starts
-- 10:35 - Compaction completes, creates 2 large files

-- Query at 10:29: Reads 1,800 small files (SLOW - 45 seconds)
-- Query at 10:36: Reads 2 large files (FAST - 3 seconds)
-- Problem: 30-minute window of degraded performance!

Tantangan 4: Konflik penulis serentak

Penulisan skala menyiratkan beberapa pekerja serentak menulis ke tabel Iceberg yang sama.

Karena sifat optimis dari konkurensi di Iceberg, beberapa pekerjaan streaming yang menulis ke tabel yang sama dapat berkonflik, yang dapat menyebabkan percobaan ulang dan penundaan yang eksponensial.

# Job 1: Processing user ecommerce_events
# Job 2: Processing system ecommerce_events
# Job 3: Processing audit ecommerce_events

# All trying to commit simultaneously:
# Commit 1: snapshot-1234 → SUCCESS
# Commit 2: snapshot-1234 → CONFLICT! (optimistic concurrency)
# Commit 3: snapshot-1234 → CONFLICT!

# Jobs 2 & 3 must retry, causing delays and wasted work

Tantangan 5: Agregasi waktu nyata dan beberapa indeks

Aplikasi nyata memiliki beberapa pola kueri yang memerlukan urutan penyortiran yang berbeda:

-- Query Pattern 1: User analytics (90% of queries)
SELECT * FROM ecommerce_events WHERE user_id = 12345 AND event_time > '2025-01-01';
-- Needs: ORDER BY user_id, event_time

-- Query Pattern 2: Product analytics (5% of queries)
SELECT * FROM ecommerce_events WHERE product_id = 789 AND event_time > '2025-01-01';
-- Needs: ORDER BY product_id, event_time

-- Query Pattern 3: Geographic analysis (5% of queries)
SELECT * FROM ecommerce_events WHERE region = 'US' AND event_type = 'purchase';
-- Needs: ORDER BY region, event_type

dan agregasi yang telah dihitung sebelumnya:

-- Users want real-time dashboards showing:
SELECT
  region,
  event_type,
  COUNT(*) as ecommerce_events,
  SUM(revenue) as total_revenue,
  COUNT(DISTINCT user_id) as unique_users
FROM ecommerce_events
WHERE event_time >= current_timestamp() - INTERVAL 1 HOUR
GROUP BY region, event_type;

Pendekatan Iceberg: Jalankan kueri agregasi setiap kali

-- Manual pre-aggregation with Iceberg
CREATE TABLE hourly_region_stats AS
SELECT
  date_trunc('hour', event_time) as hour,
  region,
  event_type,
  COUNT(*) as ecommerce_events,
  SUM(revenue) as total_revenue
FROM ecommerce_events
GROUP BY date_trunc('hour', event_time), region, event_type;

-- But now you need to:
-- 1. Keep it in sync with source table
-- 2. Handle late-arriving data
-- 3. Manage incremental updates
-- 4. Deal with duplicate processing

Platform Analisis Real-Time Khusus

Apache Iceberg menyediakan fitur dasar untuk beban kerja analitik berkinerja tinggi dan dapat diskalakan, tetapi analitik real-time yang berhadapan dengan pengguna memerlukan alat khusus.

Gunakan Iceberg saat
- Analitik batch dengan penulisan yang jarang
- Skenario data lake dengan ETL yang kompleks
- Evolusi skema dan perjalanan waktu sangat penting
- Latensi kueri 5-30 detik dapat diterima

Gunakan platform khusus saat
- Aplikasi real-time memerlukan kueri sub-detik yang konsisten dan konkurensi tinggi
- Penulisan streaming frekuensi tinggi (>1000 peristiwa/detik)
- Beberapa pola kueri memerlukan indeks yang berbeda
- Pra-agregasi harus diperbarui secara bertahap
- Alur kerja yang ramah bagi pengembang

┌─────────────┐    ┌────────────────────────┐    ┌─────────────────────┐
│             │    │                        │    │                     │
│ Events      │    │ Tinybird               │    │ Iceberg             │
│ stream      │--->│ (Real-time analytics)  │<---│ (Long-term storage) │
│             │  │ │                        │  │ │                     │
└─────────────┘  │ └────────────────────────┘  │ └─────────────────────┘
           Kafka ┘             │               └ iceberg() function
                               │ real-time API
                               │
                   ┌────────────────────────┐
                   │                        │
                   │ Real-time              │
                   │ application            │
                   │                        │
                   └────────────────────────┘

Tinybird adalah platform analitik real-time yang dapat menyerap aliran peristiwa dan tabel iceberge. Untuk mempelajari lebih lanjut tentang pola ini, Anda dapat membaca posting blog Iceberg + Redpanda + Tinybird ini, yang menjelaskan cara memanfaatkan penyerapan streaming Kafka asli dan tampilan materialisasi inkremental untuk API publik dalam waktu kurang dari satu detik sambil menggunakan Apache Iceberg sebagai penyimpanan tahan lama untuk beban kerja analitik.






Reporter: Nyoman Artawa Wiguna

Untuk mendapatkan Berita & Review menarik Saksenengku Network
Google News

Artikel Terkait

Populer

Artikel Terbaru