Database operasional toko online adalah sumber data paling kaya: tabel pelanggan, produk, dan pesanan hidup di sana dengan relasi yang rapi. Namun database yang sama juga melayani transaksi pelanggan setiap detik, sehingga ekstraksi untuk analitik tidak boleh asal menarik — query yang membaca jutaan baris tanpa batas bisa memperlambat kasir online. Chapter ini membahas tiga keterampilan ekstraksi dari database: memilih kolom secara eksplisit, membagi pembacaan menjadi halaman dengan teknik yang tepat, dan menjaga konsistensi antar halaman — lalu menutup dengan pengantar change data capture. Contoh memakai SQLite sebagai sumber, tetapi polanya sama untuk PostgreSQL di produksi.

SELECT Ekstraksi: Pilih Kolom Secara Eksplisit #

Kebiasaan pertama yang perlu dibuang adalah SELECT *. Menulis nama kolom secara eksplisit — SELECT id, pelanggan, total, created_at FROM pesanan — memberi tiga keuntungan: query hanya memindahkan data yang diperlukan, urutan kolom terkunci sebagai kontrak yang tidak berubah ketika sumber menambah kolom baru, dan schema tujuan kita tidak diam-diam berubah karena ulah pihak lain. Daftar kolom eksplisit juga menjadi dokumentasi hidup: pembaca kode langsung tahu data apa yang mengalir.

Batching: LIMIT/OFFSET vs Keyset Pagination #

Tabel besar tidak boleh dibaca sekali jalan — hasil besar menggerus memori kedua sisi. Datanya perlu dibagi halaman, dan ada dua cara membaginya. Cara pertama, offset pagination, melompati baris yang sudah dibaca:

sql
SELECT id, pelanggan, total, created_at FROM pesanan
ORDER BY id LIMIT 1000 OFFSET 5000;

Cara kedua, keyset pagination, melanjutkan dari kunci terakhir yang terbaca:

sql
SELECT id, pelanggan, total, created_at FROM pesanan
WHERE id > 5000 ORDER BY id LIMIT 1000;

Keduanya tampak setara, tetapi tidak. Query OFFSET 5000 memaksa database memproduksi lalu membuang 5.000 baris setiap kali — biaya halaman makin mahal makin dalam lompatannya. Query keyset cukup melompat tepat ke id > 5000 lewat indeks primary key, sehingga biaya per halaman konstan. Lebih fatal lagi: bila ada baris disisipkan atau dihapus di tengah ekstraksi, jendela OFFSET bergeser sehingga baris bisa terlewat atau terbaca dua kali. Keyset kebal terhadap kejadian tersebut karena posisi lanjut ditentukan nilai id terakhir, bukan hitungan baris.

Snapshot Konsisten #

Ekstraksi berhalaman berarti banyak query terpisah. Tanpa penjagaan, halaman pertama bisa membaca keadaan sebelum sebuah pesanan masuk dan halaman kedua sesudahnya — hasil akhirnya campuran dua momen waktu. Solusinya: bungkus seluruh rangkaian halaman dalam satu transaksi baca. SQLite dan PostgreSQL menjaga snapshot transaksi sejak pernyataan baca pertama, sehingga semua halaman melihat keadaan database yang persis sama — materi lengkapnya sudah kita bahas di section Database dan Penyimpanan Data.

Implementasi: Ekstraksi Keyset SQLite ke SQLite #

Kita praktekkan seluruh pola: sumber sumber_db.db berisi 7 pesanan, diekstraksi per 3 baris ke gudang_db.db dalam satu transaksi baca:

python
import sqlite3

sumber = sqlite3.connect("sumber_db.db")
tujuan = sqlite3.connect("gudang_db.db")

SKEMA = """
    CREATE TABLE IF NOT EXISTS pesanan (
        id INTEGER PRIMARY KEY,
        pelanggan TEXT NOT NULL,
        total INTEGER NOT NULL,
        created_at TEXT NOT NULL
    )
"""
tujuan.execute(SKEMA)

UKURAN_HALAMAN = 3
kunci_terakhir = 0
total = 0

sumber.execute("BEGIN")  # transaksi baca: snapshot konsisten selama ekstraksi
while True:
    halaman = sumber.execute(
        "SELECT id, pelanggan, total, created_at FROM pesanan "
        "WHERE id > ? ORDER BY id LIMIT ?",
        (kunci_terakhir, UKURAN_HALAMAN),
    ).fetchall()
    if not halaman:
        break
    tujuan.executemany(
        "INSERT OR REPLACE INTO pesanan VALUES (?, ?, ?, ?)", halaman
    )
    kunci_terakhir = halaman[-1][0]
    total += len(halaman)
    print(f"Halaman: id {halaman[0][0]}-{halaman[-1][0]} ({len(halaman)} baris)")
sumber.execute("COMMIT")
tujuan.commit()

cek = tujuan.execute("SELECT COUNT(*), MIN(id), MAX(id) FROM pesanan").fetchone()
print(f"Total diekstraksi: {total} baris")
print(f"Isi tujuan: {cek[0]} baris (id {cek[1]} sampai {cek[2]})")
sumber.close()
tujuan.close()

Output yang Dihasilkan:

text
Halaman: id 1-3 (3 baris)
Halaman: id 4-6 (3 baris)
Halaman: id 7-7 (1 baris)
Total diekstraksi: 7 baris
Isi tujuan: 7 baris (id 1 sampai 7)

Mari kita telaah baris-baris kode di atas:

  1. Baris 20 (BEGIN): transaksi baca dibuka sebelum halaman pertama — sejak pernyataan baca pertama, SQLite memegang snapshot yang sama untuk semua halaman.
  2. Baris 22-26 (WHERE id > ? ORDER BY id LIMIT ?): inti keyset pagination. Parameter kunci_terakhir bergerak maju setiap halaman, dan indeks primary key membuat lompatan ke posisi tersebut murah.
  3. Baris 27-28 (if not halaman: break): halaman kosong menandakan seluruh tabel terbaca — loop berhenti tanpa perlu tahu jumlah baris di muka.
  4. Baris 32 (kunci_terakhir = halaman[-1][0]): kunci diperbarui dari baris terakhir halaman, bukan dihitung — sifat inilah yang membuat posisi lanjut stabil terhadap perubahan data.
  5. Baris 35-36 (dua COMMIT terpisah): transaksi baca sumber ditutup setelah ekstraksi selesai, sedangkan penulisan tujuan di-commit terpisah — kegagalan penulisan tidak menjebak snapshot sumber lebih lama dari perlu.

Nilai kunci_terakhir yang tersimpan juga berfungsi sebagai titik lanjut: bila run menyerah di tengah jalan, run ulang cukup melanjutkan dari kunci terakhir yang berhasil ditulis. Dokumentasi lengkapnya di dokumentasi sqlite3 dan SQLite docs .

Sekilas Change Data Capture #

Seluruh pola di atas adalah polling: kita bertanya ke sumber secara berkala. Change data capture (CDC) membalik pendekatan dengan membaca log transaksi database sehingga setiap insert, update, dan delete terkirim sebagai event — termasuk perubahan yang tak terlihat oleh watermark. Pendekatan ini menjadi andal ketika volume perubahan besar dan latensi harus rendah; kita bedah tuntas di chapter Change Data Capture.

Database memberi kontrol penuh lewat query, tetapi tidak semua sumber bisa diquery. Layanan pihak ketiga seperti payment gateway hanya membuka akses lewat HTTP — pintu yang kita buka di chapter Ingestion dari API.