Idempotensi

Merancang pipeline yang aman dijalankan ulang dengan idempotensi sebagai keutamaan inti data engineering.

Daftar isi

Topik ini membahas idempotensi sebagai keutamaan inti saat merancang pipeline batch, yaitu sifat pekerjaan yang aman dijalankan berulang tanpa mengubah keadaan akhirnya. Dua pola muat yang idempoten diuraikan beserta kuncinya, kemudian satu demonstrasi dijalankan dua kali untuk menunjukkan perbedaannya dengan pola append mentah. Setelah membacanya, keputusan menjalankan ulang pekerjaan yang gagal dapat diambil tanpa risiko data menjadi ganda.

Menjalankan ulang adalah kepastian, bukan kecelakaan#

Pekerjaan batch hampir pasti dijalankan ulang karena tiga hal. Orkestrator mencoba kembali task yang gagal, operator menghentikan lalu mengulang run yang bermasalah, dan pengisian ulang data lama (backfill) menjalankan pekerjaan yang sama pada jendela waktu lama. Idempoten (idempotent) adalah sifat operasi yang keadaan akhirnya sama apabila dijalankan sekali maupun berkali-kali dengan masukan yang sama. Pada pipeline, sifat itu berarti run ulang tidak menambah, tidak mengurangi, dan tidak menggandakan satu baris pun pada target.

Sifat ini menentukan harga dari setiap kegagalan. Pipeline yang idempoten dipulihkan dengan menjalankan ulang pekerjaan yang sama, sehingga pemulihannya berupa satu perintah. Pipeline yang tidak idempoten memaksa keputusan sulit di tengah keadaan darurat, yaitu membersihkan target secara manual, menghitung duplikat yang sudah masuk, atau membiarkan data ganda merusak laporan. Perhitungan agregat seperti total peminjaman per hari tidak dapat membedakan peminjam sungguhan dari duplikat akibat run ganda, sehingga kerusakannya sering baru terlihat saat laporan dipertanyakan.

Dua pola muat yang idempoten#

Pola pertama adalah DELETE-INSERT per partisi, yaitu menghapus dulu data lama pada lingkup yang sama sebelum menyisipkan data baru. Lingkupnya dibatasi oleh kunci partisi, biasanya kolom tanggal hari yang sedang diproses, sehingga DELETE hanya menyentuh partisi yang akan ditulis ulang dan partisi lain tidak terganggu. Run ulang menghasilkan keadaan akhir yang sama karena data lama pada partisi itu pasti sudah terhapus sebelum data baru masuk.

Pola kedua adalah upsert, yaitu penyisipan yang otomatis menjadi pembaruan bila kuncinya sudah ada, dan DuckDB menyediakannya sebagai INSERT OR REPLACE maupun bentuk lengkapnya INSERT ... ON CONFLICT ... DO UPDATE. Pola ini mensyaratkan tabel target memiliki PRIMARY KEY atau UNIQUE pada kunci bisnisnya, karena tanpa kunci itu tidak ada dasar untuk memutuskan baris mana yang digantikan. Contoh kecil berikut menyisipkan baris stok yang sama tiga kali pada tabel berkunci utama.

python Pola INSERT OR REPLACE pada tabel berkunci utama
import duckdb

con = duckdb.connect()
con.execute("CREATE TABLE stok (id VARCHAR PRIMARY KEY, jumlah INTEGER)")
con.execute("INSERT INTO stok VALUES ('B001', 3)")
# penyisipan yang sama dijalankan tiga kali
for _ in range(3):
    con.execute("INSERT OR REPLACE INTO stok VALUES ('B001', 3)")
print(con.execute("SELECT count(*) FROM stok").fetchone()[0], "baris")
text Keluaran
1 baris

Tiga penyisipan yang sama berakhir menjadi satu baris, karena setiap penyisipan berikutnya menggantikan baris yang sudah ada pada kunci yang sama. Kedua pola itu melayani situasi yang berbeda. DELETE-INSERT cocok untuk memuat ulang partisi harian secara penuh, sedangkan upsert cocok untuk batch kecil yang hanya mengubah sebagian baris.

Demonstrasi dua run pada target yang sama#

Skrip berikut memuat tiga baris peminjaman buku ke dua tabel pada basis data yang sama, satu dengan append murni dan satu dengan DELETE-INSERT per tanggal. Kedua pola menerima data yang sama persis pada setiap run, sehingga perbedaan keadaan akhirnya sepenuhnya berasal dari pola muatnya. Jalankan pertama membangun kedua tabel dari nol.

python muat-idempoten.py
"""Dua pola muat pada data yang sama: append mentah dan DELETE-INSERT per tanggal."""

import duckdb

BERKAS_DB = "pinjaman.duckdb"

BARISAN = [
    ("P001", "2026-10-01", 2),
    ("P002", "2026-10-01", 1),
    ("P003", "2026-10-02", 3),
]


def muat_mentah(con: duckdb.DuckDBPyConnection) -> None:
    """Append murni: setiap run menambah salinan baru tanpa melihat isi lama."""
    con.executemany("INSERT INTO peminjaman_mentah VALUES (?, ?, ?)", BARISAN)


def muat_idempoten(con: duckdb.DuckDBPyConnection) -> None:
    """Hapus dulu isi partisi tanggal yang sama, lalu sisipkan ulang datanya."""
    for tanggal in sorted({b[1] for b in BARISAN}):
        con.execute("DELETE FROM peminjaman_bersih WHERE tanggal = ?", [tanggal])
    con.executemany("INSERT INTO peminjaman_bersih VALUES (?, ?, ?)", BARISAN)


con = duckdb.connect(BERKAS_DB)
con.execute(
    "CREATE TABLE IF NOT EXISTS peminjaman_mentah "
    "(id VARCHAR, tanggal VARCHAR, jumlah_buku INTEGER)"
)
con.execute(
    "CREATE TABLE IF NOT EXISTS peminjaman_bersih "
    "(id VARCHAR, tanggal VARCHAR, jumlah_buku INTEGER)"
)
muat_mentah(con)
muat_idempoten(con)
n_mentah = con.execute("SELECT count(*) FROM peminjaman_mentah").fetchone()[0]
n_bersih = con.execute("SELECT count(*) FROM peminjaman_bersih").fetchone()[0]
con.close()
print(f"peminjaman_mentah : {n_mentah} baris (append murni)")
print(f"peminjaman_bersih : {n_bersih} baris (DELETE-INSERT per tanggal)")
text Keluaran
peminjaman_mentah : 3 baris (append murni)
peminjaman_bersih : 3 baris (DELETE-INSERT per tanggal)

Jalankan ulang skrip yang sama tanpa menghapus basis datanya.

bash
# jalankan ulang skrip yang sama, basis datanya dibiarkan ada
python3 muat-idempoten.py
text Keluaran
peminjaman_mentah : 6 baris (append murni)
peminjaman_bersih : 3 baris (DELETE-INSERT per tanggal)

Angka pada tabel mentah berlipat dari tiga menjadi enam, sedangkan tabel bersih tetap tiga baris. Pada tabel bersih, fungsi muat_idempoten menghapus dulu baris bertanggal 2026-10-01 dan 2026-10-02 sebelum menyisipkan ulang, sehingga run kedua menimpa hasil run pertama pada partisi yang sama. Perilaku inilah yang dikehendaki dari pekerjaan yang dapat dijalankan ulang, yaitu keadaan akhir yang sama untuk masukan yang sama.

Kesalahan umum#

  1. Menunda idempotensi dengan rencana dedup saat query. Penyebabnya, append mentah terasa paling mudah dan dedup dianggap urusan belakangan. Akibatnya, setiap query analitik harus mengingat kolom pemisah duplikatnya, dan satu kelalaian saja menghasilkan angka yang salah secara senyap. Perbaikannya, buat muat idempoten sejak hari pertama agar target selalu layak dibaca.
  2. DELETE tanpa lingkup partisi. Penyebabnya, pernyataan hapus ditulis tanpa WHERE karena terasa lebih sederhana. Akibatnya, run inkremental menghapus partisi lain yang tidak ikut disisipkan ulang, sehingga data hilang secara permanen. Perbaikannya, ikat DELETE pada kunci partisi yang sama dengan data yang disisipkan, lalu bandingkan jumlah baris sebelum dan sesudahnya saat menguji.
  3. Tabel target tanpa kunci unik untuk upsert. Penyebabnya, kendala kunci dianggap beban yang menunda pemuatan. Akibatnya, INSERT OR REPLACE dan ON CONFLICT tidak dapat berjalan, atau lebih buruk, duplikat tidak terdeteksi sama sekali. Perbaikannya, tetapkan kunci bisnis yang stabil sebagai PRIMARY KEY sebelum pola upsert dipakai.

Ringkasan#

  • Idempotensi berarti run berulang dengan masukan yang sama berakhir pada keadaan akhir yang sama, dan sifat ini menjadikan pemulihan semudah menjalankan ulang.
  • DELETE-INSERT per partisi dan upsert berbasis kunci unik adalah dua pola muat yang idempoten.
  • Append murni menggandakan data pada setiap run ulang, dan demonstrasi dua run menunjukkan kontrasnya secara nyata.

Referensi#