Satu sore, tim backend mengumumkan perubahan kolom di tabel raw_pesanan mulai besok. Pertanyaan pertama tim data bukan “bagaimana cara mengubah kode”, melainkan “tabel dan dashboard mana saja yang terdampak”. Tanpa peta dependensi, jawabannya hanya tebakan, karena pipeline TokoKita sudah terdiri dari belasan tabel staging dan mart yang saling menumpuk. Data lineage menjawab pertanyaan tersebut dengan memetakan asal-usul setiap tabel — dari data mentah sampai mart laporan — sehingga dampak sebuah perubahan bisa dihitung sebelum kode diganti. Chapter ini membangun graf lineage langsung dari kode pipeline, lalu memakai graf tersebut untuk analisis dampak.

Apa Itu Data Lineage #

Data lineage adalah catatan dependensi antar artefak data: tabel mana yang dibaca, tabel mana yang ditulis, dan bagaimana nilai mengalir di antaranya. Ada dua tingkat ketelitian yang perlu kita bedakan:

  • Table-level lineage memetakan relasi antar tabel, misalnya mart_penjualan_harian dibangun dari stg_pesanan dan stg_pelanggan.
  • Column-level lineage memetakan relasi antar kolom, misalnya kolom gmv di mart dihitung dari kolom total di stg_pesanan.

Table-level lineage cukup untuk menjawab “apa yang terdampak jika tabel X berubah”. Column-level lineage dibutuhkan saat kita harus mengaudit rumus: ketika angka gmv mencurigakan, kita menelusuri bahwa nilainya berasal dari total yang difilter kolom status. Untuk analisis dampak, table-level sudah cukup dan jauh lebih mudah dibangun, sehingga kita mulai dari sana.

Membangun Graf Lineage dari Kode Pipeline #

Sumber lineage paling jujur adalah kode pipeline itu sendiri. Setiap langkah SQL menyatakan upstream secara eksplisit lewat klausa FROM dan JOIN, sedangkan nama langkah menyatakan tabel target. Kita definisikan mini-pipeline TokoKita sebagai kamus target -> SQL, plus satu langkah pandas yang ditulis manual:

python
import re

LANGKAH_SQL = {
    "stg_pesanan": """
        SELECT id_pesanan, id_pelanggan, total, status
        FROM raw_pesanan
        WHERE status = 'lunas'
    """,
    "stg_pelanggan": """
        SELECT id_pelanggan, nama, kota
        FROM raw_pelanggan
    """,
    "mart_penjualan_harian": """
        SELECT DATE(p.tanggal) AS tanggal, c.kota, SUM(p.total) AS gmv
        FROM stg_pesanan p
        JOIN stg_pelanggan c ON p.id_pelanggan = c.id_pelanggan
        GROUP BY 1, 2
    """,
}

LANGKAH_PANDAS = {
    "mart_gmv_bulanan": ["mart_penjualan_harian"],
}

POLA_TABEL = re.compile(r"\b(?:FROM|JOIN)\s+([a-z_][a-z0-9_]*)", re.IGNORECASE)


def ekstrak_upstream(sql):
    """Ambil nama tabel setelah FROM atau JOIN."""
    temuan = POLA_TABEL.findall(sql)
    return sorted({t.lower() for t in temuan})


def bangun_lineage():
    lineage = {}
    for target, sql in LANGKAH_SQL.items():
        lineage[target] = ekstrak_upstream(sql)
    for target, sumber in LANGKAH_PANDAS.items():
        lineage[target] = sorted(sumber)
    return lineage


lineage = bangun_lineage()
print("== Graf lineage (tabel -> upstream) ==")
for target in sorted(lineage):
    print(f"{target:24s} <- {', '.join(lineage[target])}")

Output yang Dihasilkan:

text
== Graf lineage (tabel -> upstream) ==
mart_gmv_bulanan         <- mart_penjualan_harian
mart_penjualan_harian    <- stg_pelanggan, stg_pesanan
stg_pelanggan            <- raw_pelanggan
stg_pesanan              <- raw_pesanan

Mari kita telaah baris-baris kode di atas:

  1. Baris 3-19 (LANGKAH_SQL): tiga langkah SQL didefinisikan sebagai kamus; kunci adalah tabel target, nilai adalah query yang membangun tabel tersebut.
  2. Baris 21-23 (LANGKAH_PANDAS): langkah non-SQL dideklarasikan manual karena kode pandas tidak bisa diparsing dengan regex yang sama.
  3. Baris 25 (POLA_TABEL): regex menangkap identifier pertama setelah kata FROM atau JOIN; alias seperti p dan c berada setelah nama tabel sehingga tidak ikut tertangkap, dan \b memastikan FROM hanya cocok sebagai kata utuh.
  4. Baris 28-31 (ekstrak_upstream): findall mengembalikan semua kecocokan, lalu set membuang duplikat dan sorted menjaga urutan stabil.
  5. Baris 34-40 (bangun_lineage): graf disimpan sebagai kamus tabel -> daftar upstream; tabel raw_pesanan tidak pernah menjadi kunci karena tabel tersebut tidak dibangun pipeline ini — tabel tersebut adalah sumber.

Analisis Dampak saat Tabel Berubah #

Graf lineage menjawab pertanyaan praktis: jika kolom di stg_pesanan berubah, apa yang harus dicek ulang? Kita balik arah graf menjadi relasi upstream -> pemakai, lalu menjalankan BFS dari tabel yang berubah untuk mengumpulkan semua downstream transitif:

python
from collections import deque


def downstream(lineage, mulai):
    """BFS: semua tabel yang (secara transitif) bergantung pada `mulai`."""
    pemakai = {}
    for target, sumber in lineage.items():
        for s in sumber:
            pemakai.setdefault(s, []).append(target)

    terdampak = []
    antrean = deque([mulai])
    dikunjungi = {mulai}
    while antrean:
        tabel = antrean.popleft()
        for p in pemakai.get(tabel, []):
            if p not in dikunjungi:
                dikunjungi.add(p)
                terdampak.append(p)
                antrean.append(p)
    return terdampak


print("== Analisis dampak: kolom di stg_pesanan berubah ==")
print("Tabel downstream yang perlu dicek ulang:")
for t in downstream(lineage, "stg_pesanan"):
    print(f"  - {t}")

print()
print("== Analisis dampak: raw_pesanan berubah ==")
for t in downstream(lineage, "raw_pesanan"):
    print(f"  - {t}")

Output yang Dihasilkan:

text
== Analisis dampak: kolom di stg_pesanan berubah ==
Tabel downstream yang perlu dicek ulang:
  - mart_penjualan_harian
  - mart_gmv_bulanan

== Analisis dampak: raw_pesanan berubah ==
  - stg_pesanan
  - mart_penjualan_harian
  - mart_gmv_bulanan

Fungsi downstream bekerja dalam dua tahap. Pertama, loop for di awal membalik graf: kamus pemakai memetakan setiap tabel ke daftar tabel yang membacanya. Kedua, BFS mengunjungi pemakai secara berlapis — stg_pesanan dulu, lalu mart_penjualan_harian, lalu mart_gmv_bulanan — sehingga hasilnya sudah terurut menurut jarak dari tabel yang berubah. Himpunan dikunjungi mencegah tabel tercatat dua kali bila graf bercabang lalu menyatu kembali.

Visualisasi Graf Lineage #

Graf yang sama mudah dirender sebagai diagram. Node menyatakan tabel, panah menyatakan arah aliran data:

Diagram ini adalah versi visual dari kamus lineage: setiap baris sumber --> target berasal dari satu pasangan di graf. Pada pipeline nyata dengan puluhan tabel, diagram seperti ini menjadi peta navigasi utama saat onboarding engineer baru maupun saat menilai dampak perubahan.

Lineage dari Kode versus Lineage Otomatis #

AspekLineage dari kodeLineage otomatis
Sumber kebenaranSQL dan kode transform di repositoryevent lineage yang dikirim runtime pipeline
Pemeliharaanikut berubah saat kode diubahbutuh integrasi di setiap tool
Cakupanhanya langkah yang tertulis di kodetermasuk query ad-hoc dan layer BI
Cocok untuktim kecil, pipeline batch sederhanaorganisasi dengan banyak tim dan tool

Pendekatan dari kode menuntut disiplin: setiap langkah harus terdaftar di satu tempat. Begitu ada tabel yang dibangun lewat query manual di notebook, graf langsung bohong — dan lineage yang bohong lebih berbahaya daripada tidak ada lineage sama sekali.

Lineage memberi tahu kita apa yang terdampak ketika sesuatu berubah. Namun untuk mengetahui apa yang terjadi di dalam pipeline setiap kali pipeline dijalankan — tahap mana yang lambat, berapa baris yang diproses, kapan kegagalan terjadi — kita butuh catatan eksekusi yang terstruktur. Materi tersebut dibahas di chapter Pipeline Logging.