Tutorial Apache Iceberg: Open Table Format untuk Data Lakehouse

# Apache Iceberg: Format Tabel Lintas-Engine untuk Lakehouse Apache Iceberg adalah format tabel terbuka yang mengubah sekumpulan file Parquet (atau ORC/Avro) di object storage menjadi tabel sungguhan...

By Ruby Abdullah · · tutorial
Apache IcebergLakehouseTable FormatData EngineeringPyIcebergSpark

Apache Iceberg: Format Tabel Lintas-Engine untuk Lakehouse

Apache Iceberg adalah format tabel terbuka yang mengubah sekumpulan file Parquet (atau ORC/Avro) di object storage menjadi tabel sungguhan dengan snapshot, evolusi skema, dan penulisan konkuren yang aman. Yang membedakannya dari format lakehouse lain adalah desain yang berpusat pada katalog serta hidden partitioning: tabel didefinisikan oleh penunjuk katalog ke sebuah pohon metadata, dan partisi merupakan properti tabel, bukan sesuatu yang harus disebutkan setiap kueri. Tutorial ini membahas Iceberg dari dasar dengan PyIceberg sebagai jalur utama praktik, lalu menunjukkan tabel yang sama dari Spark SQL serta catatan bagaimana DuckDB dan Trino membacanya.

Apa Itu Iceberg dan Mengapa Format Tabel Penting

Data lake biasa hanyalah file di dalam bucket. Parquet memberi penyimpanan kolumnar yang efisien, tetapi sebuah folder berisi file Parquet bukanlah tabel: tidak ada commit atomik, tidak ada skema yang disepakati, tidak ada cara mengetahui file mana yang aktif, dan tidak ada cara aman bagi dua job untuk menulis bersamaan. Pembaca yang memindai direktori saat penulis sedang bekerja bisa mengambil data yang setengah jadi.

Sebuah format tabel menyelesaikan ini dengan menambahkan lapisan metadata di atas file data. Metadata itu menjawab tiga pertanyaan secara presisi:

  • File mana saja yang membentuk tabel saat ini (dan pada titik waktu mana pun di masa lalu)?
  • Apa skemanya, dan bagaimana skema itu berubah?
  • Bagaimana tata letak data agar kueri bisa melewati file yang tidak diperlukan?

Jawaban Iceberg adalah pohon file metadata yang bersifat immutable ditambah sebuah katalog yang menyimpan satu penunjuk ke puncak pohon tersebut. Menukar penunjuk itu secara atomik adalah inti dari sebuah commit. Karena setiap file metadata immutable, pembaca selalu melihat snapshot yang konsisten dan tidak pernah menjumpai tulisan yang separuh jadi.

Bagaimana Iceberg Berbeda dari Delta Lake dan Hudi

Ketiganya adalah format tabel terbuka di atas file kolumnar, tetapi pilihan desainnya berbeda:

  • Iceberg melacak status melalui penunjuk katalog dan pohon file metadata/manifest. Partisi bersifat hidden — dideklarasikan pada tabel dan diterapkan otomatis — dan Iceberg secara eksplisit lintas-engine, dengan spesifikasi stabil yang diimplementasikan oleh Spark, Flink, Trino, DuckDB, Snowflake, dan PyIceberg murni-Python.
  • Delta Lake melacak status melalui log transaksi JSON terurut (deltalog) yang menyatu dengan data. Partisi mengikuti tata letak direktori fisik, dan ekosistemnya secara historis berpusat di Spark (meski delta-rs memperluasnya).
  • Hudi berfokus pada upsert cepat dan aliran inkremental, dengan timeline commit serta model record-key/primary-key yang kuat. Cocok untuk beban kerja CDC yang berat.

Intinya secara praktis: pilih Iceberg ketika Anda ingin banyak engine membaca dan menulis tabel yang sama melalui katalog bersama, dan ketika Anda ingin perubahan partisi serta skema menjadi operasi metadata, bukan penulisan ulang data.

Arsitektur Iceberg

Tabel Iceberg adalah pohon berlapis. Dari atas ke bawah:

Catalog                          (menunjuk ke metadata file aktif per tabel)

└─ metadata.json (skema tabel, partition spec, daftar snapshot, id snapshot aktif)

└─ manifest list (satu file Avro per snapshot; mendaftar manifest dalam snapshot itu)

└─ manifest files (Avro; tiap file mendaftar data file + statistik & nilai partisi)

└─ data files (Parquet / ORC / Avro yang menampung baris sebenarnya)

Dibaca dari atas ke bawah:

  • Katalog memetakan identitas tabel (mis. analytics.events) ke lokasi metadata.json aktifnya. Inilah satu-satunya penunjuk yang mutable; semua di bawahnya immutable.
  • File metadata mendeskripsikan tabel: skema aktif, seluruh skema historis, partition spec, sort order, properti tabel, dan daftar snapshot. Sebuah commit menulis file metadata baru lalu memperbarui penunjuk katalog ke file itu secara atomik.
  • Sebuah snapshot adalah tampilan tabel yang lengkap pada satu titik waktu. Tiap snapshot merujuk ke satu manifest list.
  • Manifest list mencatat manifest files milik snapshot, beserta statistik ringkas (jumlah baris, rentang nilai partisi) sehingga planner bisa memangkas seluruh manifest.
  • Tiap manifest file mendaftar data files sebenarnya beserta statistik per kolom (min/max, jumlah null) dan nilai partisi file tersebut. Statistik inilah yang menggerakkan pemangkasan di tingkat file.

Karena pemangkasan terjadi dengan membaca metadata yang ringkas alih-alih membuka file data, perencanaan kueri tetap skalabel hingga jutaan file. Dan karena tiap snapshot adalah pohon file immutable yang utuh, time travel sekadar membaca manifest list dari snapshot lama.

Katalog: REST, SQL/JDBC, Glue, Hive

Katalog adalah tempat Iceberg paling kentara berbeda dari format berbasis log. Implementasi umum:

  • REST catalog — layanan yang berbicara protokol REST Iceberg. Pilihan modern dan netral-engine untuk produksi; backend-nya antara lain Nessie, Polaris, Lakekeeper, dan layanan terkelola.
  • SQL / JDBC catalog — menyimpan penunjuk tabel di basis data relasional (SQLite, Postgres, MySQL). Sangat baik untuk pengembangan lokal dan deployment kecil. Kita memakai SQL catalog berbasis SQLite lokal di tutorial ini.
  • AWS Glue catalog — memakai Glue Data Catalog sebagai metastore; native di AWS.
  • Hive catalog — memakai Hive Metastore, umum pada lingkungan Hadoop/Spark yang sudah ada.

File data fisik yang sama bisa didaftarkan di katalog berbeda; katalog hanya mengatur penunjuk serta registrasi namespace/tabel.

PyIceberg: Jalur Praktik

PyIceberg adalah implementasi murni-Python yang membaca dan menulis tabel Iceberg tanpa JVM atau Spark. Cocok untuk notebook, ETL satu node, dan menyematkan Iceberg ke dalam pipeline pandas/PyArrow.

Instalasi

pip install "pyiceberg[sql-sqlite,pyarrow]" pandas

Perintah ini memasang PyIceberg dengan SQL catalog berbasis SQLite serta lapisan data PyArrow. Tambahkan extra seperti s3fs atau glue saat Anda beranjak dari penyimpanan lokal.

Membuat SQL Catalog Lokal

Kita akan memodelkan tabel events untuk kasus analitik produk. Pertama, buat katalog yang didukung basis data SQLite lokal, dengan file data ditulis di bawah direktori warehouse lokal.

import os

from pyiceberg.catalog.sql import SqlCatalog

warehousepath = os.path.abspath("warehouse")

os.makedirs(warehousepath, existok=True)

catalog = SqlCatalog(

"local",

{

"uri": f"sqlite:///{warehousepath}/catalog.db",

"warehouse": f"file://{warehousepath}",

},

)

Katalog kini ada sebagai basis data SQLite kosong. Belum ada tabel, tetapi penyimpanan penunjuk sudah siap.

Membuat Namespace dan Tabel dengan Skema Eksplisit

Sebuah namespace adalah konsep pengelompokan Iceberg (analog dengan database/schema). Tabel berada di dalam namespace.

from pyiceberg.schema import Schema

from pyiceberg.types import (

NestedField, StringType, LongType, TimestampType, DoubleType,

)

catalog.createnamespaceifnotexists("analytics")

schema = Schema(

NestedField(fieldid=1, name="eventid", fieldtype=LongType(), required=True),

NestedField(fieldid=2, name="userid", fieldtype=LongType(), required=True),

NestedField(fieldid=3, name="eventtype", fieldtype=StringType(), required=True),

NestedField(fieldid=4, name="eventts", fieldtype=TimestampType(), required=True),

NestedField(fieldid=5, name="amount", fieldtype=DoubleType(), required=False),

)

table = catalog.createtableifnotexists(

identifier="analytics.events",

schema=schema,

)

Perhatikan fieldid eksplisit pada tiap kolom. Iceberg melacak kolom berdasarkan ID numerik yang stabil, bukan berdasarkan nama atau posisi. Inilah mekanisme yang membuat rename dan reorder aman: file data merujuk ke ID, sehingga mengganti nama kolom tidak pernah membatalkan file yang sudah ada.

Menambahkan Data (PyArrow dan pandas)

PyIceberg menulis dari sebuah tabel PyArrow. Anda bisa membangunnya langsung atau mengonversi dari pandas.

import pyarrow as pa

from datetime import datetime

rows = pa.table(

{

"eventid": [1, 2, 3, 4],

"userid": [1001, 1002, 1001, 1003],

"eventtype": ["view", "click", "purchase", "view"],

"eventts": [

datetime(2026, 5, 1, 9, 0),

datetime(2026, 5, 1, 9, 5),

datetime(2026, 5, 2, 14, 30),

datetime(2026, 5, 3, 11, 15),

],

"amount": [None, None, 49.90, None],

},

schema=table.schema().asarrow(),

)

table.append(rows)

Memakai table.schema().asarrow() menjamin tipe Arrow yang masuk selaras dengan skema tabel. Dari pandas, konversikan terlebih dahulu:

import pandas as pd

pdf = pd.DataFrame(

{

"eventid": [5, 6],

"userid": [1002, 1004],

"eventtype": ["click", "purchase"],

"eventts": pd.todatetime(["2026-05-04 08:00", "2026-05-04 08:30"]),

"amount": [None, 19.99],

}

)

table.append(pa.Table.frompandas(pdf, schema=table.schema().asarrow()))

Setiap append membuat snapshot baru dan memperbarui penunjuk katalog secara atomik.

Memindai: Filter Baris dan Proyeksi Kolom

Baca kembali dengan scan. Dorong predikat ke rowfilter dan batasi kolom dengan selectedfields agar Iceberg dapat memangkas file dan kolom sebelum mematerialisasi apa pun.

df = (

table.scan(

rowfilter="eventtype == 'purchase'",

selectedfields=("eventid", "userid", "amount", "eventts"),

)

.topandas()

)

print(df)

Anda juga bisa memberikan ekspresi programatik alih-alih string:

from pyiceberg.expressions import And, GreaterThanOrEqual, EqualTo

from datetime import datetime

scan = table.scan(

rowfilter=And(

EqualTo("eventtype", "view"),

GreaterThanOrEqual("eventts", datetime(2026, 5, 2)),

),

)

arrowtbl = scan.toarrow() # Arrow zero-copy; atau .topandas(), .toduckdb("events")

Filter dievaluasi lebih dulu terhadap statistik manifest dan file (melewati seluruh file), lalu terhadap row group, dan baru terakhir terhadap baris — sehingga kueri selektif hanya membaca sebagian kecil data.

Evolusi Skema

Karena Iceberg melacak kolom berdasarkan ID, perubahan skema hanya menyentuh metadata dan tetap aman. Gunakan transaksi updateschema:

with table.updateschema() as upd:

upd.addcolumn("sessionid", StringType(), doc="Identifier sesi klien")

upd.renamecolumn("amount", "amountusd")

upd.updatecolumn("userid", required=False)

Menghapus kolom sama amannya; file data lama cukup tidak dibaca untuk field id tersebut.

with table.updateschema() as upd:

upd.deletecolumn("sessionid")

Jaminan utama:

  • Add menambahkan kolom baru dengan field ID baru; baris lama terbaca sebagai null.
  • Rename hanya mengubah nama yang melekat pada sebuah ID; file data tidak tersentuh.
  • Drop berhenti memproyeksikan sebuah ID kolom; tidak perlu penulisan ulang data.
  • Reorder* dan promosi tipe (mis. int → long) didukung tanpa menulis ulang file.

Tidak ada cara untuk tanpa sengaja memetakan ulang data kolom lama ke kolom baru, karena ID tidak pernah dipakai ulang.

Hidden Partitioning dan Transform Partisi

Ini salah satu fitur Iceberg yang paling khas. Pada format partisi-direktori, kueri harus memfilter kolom partisi literal (mis. WHERE eventdate = '2026-05-02') dan penulis harus menghitung kolom itu. Iceberg justru menyimpan sebuah partition spec yang menurunkan nilai partisi dari kolom sumber memakai transform. Kueri memfilter kolom alami (eventts), dan Iceberg menerapkan transform secara otomatis untuk memangkas partisi — karena itu disebut "hidden."

Transform yang tersedia mencakup year, month, day, hour (pada timestamp), bucket(N, col) (hash bucketing untuk kunci ber-kardinalitas tinggi), truncate(W, col) (bucketing prefiks/rentang), dan identity.

from pyiceberg.transforms import DayTransform, BucketTransform

with table.updatespec() as spec:

spec.addfield("eventts", DayTransform(), partitionfieldname="eventday")

spec.addfield("userid", BucketTransform(numbuckets=8), partitionfieldname="userbucket")

Sekarang kueri seperti rowfilter="eventts >= '2026-05-02T00:00:00'" otomatis membatasi ke partisi hari yang cocok tanpa pengguna pernah menyebut eventday. Dua keuntungan tambahan:

  • Evolusi partisi. Anda bisa mengubah partition spec di kemudian hari (mis. dari harian ke per jam) tanpa menulis ulang data lama. File lama mempertahankan spec lamanya; file baru memakai yang baru, dan Iceberg membaca keduanya dengan benar.
  • Tanpa perawatan kolom partisi. Penulis tidak pernah menghitung kolom partisi secara manual, menghilangkan sumber skew dan tata letak buruk yang umum.

Time Travel dan Rollback Snapshot

Setiap penulisan menghasilkan snapshot. Periksa riwayatnya:

for entry in table.history():

print(entry.snapshotid, entry.madecurrentat)

for snap in table.snapshots():

print(snap.snapshotid, snap.summary.operation, snap.summary)

Baca tabel pada snapshot tertentu:

oldsnapshotid = table.history()[0].snapshotid

dfold = table.scan(snapshotid=oldsnapshotid).topandas()

Kembalikan (rollback) tabel aktif ke snapshot terdahulu (ini membuat snapshot baru yang menunjuk ulang ke status lama; riwayat tetap terjaga, bukan dihapus):

table.managesnapshots().rollbacktosnapshot(oldsnapshotid).commit()

Time travel murah karena tiap snapshot hanyalah manifest list yang menunjuk ke file immutable yang sudah ada.

Delete dan Upsert di Tingkat Baris

PyIceberg mendukung penimpaan sekumpulan baris terfilter dan upsert penuh (merge berdasarkan kunci).

Overwrite Bersyarat (Delete + Insert)

from pyiceberg.expressions import EqualTo

Hapus semua event 'view', lalu opsional tambahkan penggantinya.

table.delete(deletefilter=EqualTo("eventtype", "view"))

Timpa baris yang cocok predikat dengan data baru dalam satu commit:

table.overwrite(

df=correctedrows, # sebuah tabel PyArrow

overwritefilter=EqualTo("eventtype", "click"),

)

Upsert (Merge berdasarkan Kunci)

# Memerlukan kolom identifier (kunci); memperbarui yang cocok, menyisipkan sisanya.

result = table.upsert(

df=incomingrows, # tabel PyArrow dengan skema sama

joincols=["eventid"],

)

print(result.rowsupdated, result.rowsinserted)

Copy-on-Write vs Merge-on-Read

Iceberg mendukung dua strategi untuk menerapkan perubahan tingkat baris:

  • Copy-on-write (CoW) menulis ulang setiap data file yang memuat baris terdampak saat penulisan. Pembacaan tetap cepat (tanpa merge), tetapi penulisan lebih berat. Cocok untuk tabel padat-baca dengan pembaruan yang jarang.
  • Merge-on-read (MoR) menulis delete file ringan (position atau equality delete) berdampingan dengan data, dan pembaca menggabungkannya saat itu juga. Penulisan murah dan cepat — ideal untuk streaming/CDC — dengan biaya kerja lebih banyak saat baca hingga compaction melipat delete kembali.

Anda memilih per operasi melalui properti tabel seperti write.update.mode, write.delete.mode, dan write.merge.mode (copy-on-write atau merge-on-read). PyIceberg menulis gaya CoW; engine seperti Spark dan Flink menyediakan keduanya.

Perawatan Tabel

Iceberg menumpuk file metadata dan data seiring waktu. Perawatan rutin menjaganya tetap cepat dan murah.

Mengekspirasi Snapshot Lama

Snapshot lama menahan file data dan metadata lama. Ekspirasikan yang melewati jendela retensi Anda:

from datetime import datetime, timedelta

cutoffms = int((datetime.now() - timedelta(days=7)).timestamp() 1000)

table.expiresnapshots().expiresnapshotsolderthan(cutoffms).commit()

Ini sekaligus membebaskan penyimpanan dan memangkas metadata yang harus dibaca planner.

Compaction / Rewrite

Append kecil yang sering menghasilkan banyak file kecil, yang menurunkan performa scan. Compaction menulisnya ulang menjadi file yang lebih sedikit dan besar (serta bisa menyortir/mengelompokkan ulang sesuai partition spec). PyIceberg sedang menambahkan dukungan rewrite native; di produksi ini umum dijalankan dari Spark:

CALL local.system.rewritedatafiles(

table => 'analytics.events',

options => map('target-file-size-bytes', '134217728')

);

Pembersihan Orphan File

Penulisan yang gagal bisa meninggalkan data file yang tidak dirujuk snapshot mana pun. Setelah mengekspirasi snapshot, hapus orphan (biasanya dari Spark, karena ia memindai lapisan penyimpanan):

CALL local.system.removeorphanfiles(table => 'analytics.events');

Jalankan pembersihan orphan secara konservatif (dengan ambang usia) agar tidak pernah menghapus file yang masih dalam proses commit oleh penulisan konkuren.

Mengueri Tabel yang Sama dari Spark SQL

Karena tabel berada di katalog bersama, engine lain membaca dan menulisnya tanpa konversi. Sesi Spark yang dikonfigurasi terhadap SQL/JDBC atau REST catalog yang sama melihat analytics.events secara langsung.

-- Membuat atau mereferensikan tabel Iceberg dari Spark SQL

CREATE TABLE local.analytics.events (

eventid BIGINT,

userid BIGINT,

eventtype STRING,

eventts TIMESTAMP,

amountusd DOUBLE

)

USING iceberg

PARTITIONED BY (days(eventts), bucket(8, userid));

-- Merge (upsert) baris CDC yang masuk

MERGE INTO local.analytics.events t

USING stagedevents s

ON t.eventid = s.eventid

WHEN MATCHED THEN UPDATE SET

WHEN NOT MATCHED THEN INSERT ;

-- Time travel berdasarkan versi snapshot atau timestamp

SELECT eventtype, count() AS n

FROM local.analytics.events VERSION AS OF 3623387065522815048

GROUP BY eventtype;

SELECT FROM local.analytics.events TIMESTAMP AS OF '2026-05-02 00:00:00';

Klausa PARTITIONED BY (days(eventts), bucket(8, userid)) adalah hidden partitioning dalam bentuk SQL — perhatikan bahwa kueri tetap memfilter eventts dan userid secara langsung.

Membaca Iceberg dari DuckDB dan Trino

  • DuckDB membaca Iceberg melalui ekstensi iceberg (INSTALL iceberg; LOAD iceberg;) dan dapat melampirkan REST catalog, menjadikannya engine kueri satu node yang cepat di atas tabel lakehouse. PyIceberg juga menyediakan scan.toduckdb("events") untuk mendaftarkan scan sebagai relasi DuckDB.
  • Trino memperlakukan Iceberg sebagai konektor kelas satu: arahkan katalog iceberg-nya ke metastore yang sama (Glue, Hive, REST) dan jalankan SQL penuh, termasuk INSERT, MERGE, time travel, dan prosedur perawatan yang sama.

Intinya adalah konsistensi: semua engine ini membaca pohon metadata yang identik dan sepakat tentang isi tabel, yang merupakan janji inti dari format lintas-engine.

Praktik Terbaik

  • Gunakan katalog terkelola/REST di produksi. SQLite lokal cukup untuk pengembangan, tetapi REST catalog (Nessie, Polaris, Lakekeeper) memberi konkurensi multi-engine yang aman dan kontrol akses.
  • Partisi berdasarkan pola akses, bukan kardinalitas. Pakai day(ts) atau month(ts) untuk pembacaan deret waktu; pakai bucket(N, key) untuk kunci join/filter ber-kardinalitas tinggi. Hindari over-partitioning menjadi file mungil.
  • Dorong filter dan proyeksi ke scan(...). Selalu berikan rowfilter dan selected_fields agar statistik manifest/file bisa memangkas kerja sejak awal.
  • Jadwalkan perawatan. Ekspirasikan snapshot, compact file kecil, dan bersihkan orphan secara berkala. Tiga job ini mencegah degradasi lambat yang menjangkiti tabel lake yang tidak terawat.
  • Pilih CoW vs MoR per beban kerja. Tabel dimensi padat-baca cenderung copy-on-write; tabel fakta streaming/CDC cenderung merge-on-read dengan compaction berkala.
  • Manfaatkan evolusi partisi dan skema. Ubah tata letak saat pola akses bergeser tanpa menulis ulang riwayat; biarkan field ID yang stabil membuat rename aman.
  • Perhatikan retensi dan kepatuhan. Time travel menjaga data lama tetap terjangkau, jadi selaraskan ekspirasi snapshot dengan kewajiban penghapusan data Anda.

Kesimpulan dan Poin Penting

Apache Iceberg membuat sekumpulan file Parquet berperilaku seperti tabel transaksional sungguhan sekaligus tetap terbuka bagi engine mana pun. Ciri pembedanya adalah penunjuk katalog di atas pohon metadata immutable, hidden partitioning yang digerakkan transform, evolusi skema berbasis ID, dan spesifikasi lintas-engine yang disengaja.

  • Sebuah tabel adalah penunjuk katalog → metadata.json → manifest list → manifests → data files; commit menukar penunjuk secara atomik.
  • PyIceberg memberi jalur baca/tulis penuh dalam Python murni: buat SQL catalog, definisikan Schema, append data Arrow/pandas, dan scan(...) dengan filter serta proyeksi.
  • Hidden partitioning memungkinkan Anda memfilter kolom alami sementara Iceberg memangkas via transform (day, month, bucket, truncate), dan mendukung evolusi partisi tanpa penulisan ulang.
  • Time travel, rollback, delete, dan upsert semuanya operasi snapshot; pilih copy-on-write untuk tabel padat-baca dan merge-on-read untuk streaming/CDC.
  • Perawatan — ekspirasi snapshot, compaction, dan pembersihan orphan — menjaga tabel tetap cepat dan biaya penyimpanan terkendali.
  • Tabel yang sama dapat dibaca dari Spark, Trino, dan DuckDB melalui katalog bersama, persis yang membedakan Iceberg dari format berpusat-log seperti Delta Lake dan format berfokus-upsert seperti Hudi.

Mulailah dengan SQL catalog lokal dan PyIceberg untuk memahami modelnya, lalu beralih ke REST catalog dan engine terdistribusi saat Anda membutuhkan konkurensi multi-engine berskala besar.

Artikel Terkait

Tutorial Delta Lake: Lakehouse Andal dengan Transaksi ACID

Delta Lake: Penyimpanan Lakehouse yang Andal dengan Transaksi ACID Data lake yang dibangun di atas object storage memang...

Tutorial Pathway: Framework Real-Time Data Processing untuk AI dan Machine Learning

Tutorial Pathway: Framework Real-Time Data Processing untuk AI dan Machine Learning Pathway adalah framework open-source...

Mage: Bikin Data Pipeline Modern yang Rasanya Kayak Ngoding di Notebook

Mage: Bikin Data Pipeline Modern yang Rasanya Kayak Ngoding di Notebook Halo temen-temen, ketemu lagi sama aku Ruby Abdu...

Tutorial Ibis: API DataFrame Portabel untuk Banyak Backend

Ibis: API Dataframe Python yang Portabel di Banyak Backend Ibis adalah library dataframe Python yang memungkinkan Anda m...