to_sql() dan read_sql() tidak bisa bekerja sendirian: keduanya butuh koneksi ke database, dan pandas tidak menyertakan driver database apa pun. Di sinilah SQLAlchemy masuk — library ini menyediakan objek engine yang tahu cara berbicara dengan masing-masing database (SQLite, PostgreSQL, MySQL, dan lainnya), sementara pandas menyerahkan pembacaan dan penulisan tabel ke engine tersebut. Pada Chapter ini kita membangun engine SQLite, menulis DataFrame menjadi tabel, membaca hasil query, dan mengamankan query dengan parameter.

Peran SQLAlchemy sebagai Jembatan Database #

Objek utama yang kita butuhkan hanyalah engine dari create_engine(). Argumennya berupa connection string berpola dialect+driver://lokasi; untuk SQLite cukup sqlite:///nama_berkas.db. pandas kemudian menerima engine itu pada empat fungsi I/O:

Fungsi pandasArahKegunaan
df.to_sql(nama, engine)DataFrame → tabelmenulis isi DataFrame menjadi tabel database
pd.read_sql(sql, engine)SQL → DataFramemenjalankan query apa pun dan menampung hasilnya
pd.read_sql_table(nama, engine)tabel → DataFramemembaca satu tabel utuh tanpa menulis SQL
pd.read_sql_query(sql, engine)SQL → DataFramepadanan read_sql khusus query

Perlu dibedakan: read_sql_table dan read_sql_query hanya berlaku dengan SQLAlchemy; read_sql cerdas dan menerima keduanya. Engine juga dikelola sebagai sumber daya — setelah selesai, panggil engine.dispose() agar koneksi tertutup rapi.

Contoh berikut menulis tabel penjualan sembako ke database SQLite sementara, lalu membacanya kembali dengan tiga gaya: query SQL biasa, query berparameter, dan pembacaan tabel utuh.

python
import os
import tempfile

import pandas as pd
from sqlalchemy import create_engine, text

penjualan = pd.DataFrame({
    "kota": ["Jakarta", "Jakarta", "Jakarta", "Jakarta",
             "Bandung", "Bandung", "Bandung", "Bandung",
             "Surabaya", "Surabaya", "Surabaya", "Surabaya"],
    "produk": ["Beras", "Minyak", "Gula", "Kopi",
               "Beras", "Minyak", "Gula", "Kopi",
               "Beras", "Minyak", "Gula", "Kopi"],
    "jumlah": [40, 25, 30, 20, 32, 22, 27, 18, 36, 19, 25, 21],
    "harga": [68000, 35000, 18000, 26000, 67500, 34500, 17500, 25500, 67000, 34000, 17200, 25000],
})

with tempfile.TemporaryDirectory() as folder:
    db_path = os.path.join(folder, "penjualan.db")
    engine = create_engine(f"sqlite:///{db_path}")

    # 1. Menulis DataFrame menjadi tabel database
    penjualan.to_sql("penjualan", engine, index=False, if_exists="replace")
    print("tabel penjualan dibuat")
    print()

    # 2. Membaca hasil query SQL menjadi DataFrame
    top = pd.read_sql(
        "SELECT kota, produk, jumlah * harga AS total "
        "FROM penjualan ORDER BY total DESC LIMIT 3",
        engine,
    )
    print(top)
    print()

    # 3. Query berparameter untuk menghindari SQL injection
    jakarta = pd.read_sql(
        "SELECT produk, jumlah FROM penjualan "
        "WHERE kota = :kota ORDER BY jumlah DESC",
        engine,
        params={"kota": "Jakarta"},
    )
    print(jakarta)
    print()

    # 4. Menulis ulang dan membaca seluruh tabel
    with engine.begin() as conn:
        conn.execute(
            text("UPDATE penjualan SET jumlah = jumlah + 5 WHERE kota = 'Bandung'")
        )
    cek = pd.read_sql("SELECT COUNT(*) AS jumlah_baris FROM penjualan", engine)
    print("jumlah baris:", cek["jumlah_baris"].tolist())

    seluruh = pd.read_sql_table("penjualan", engine)
    print("kolom:", seluruh.columns.tolist())
    print("dtype kota:", seluruh["kota"].dtype)

    engine.dispose()

print("database sementara terhapus bersama folder temp")

Output yang Dihasilkan:

text
tabel penjualan dibuat

       kota produk    total
0   Jakarta  Beras  2720000
1  Surabaya  Beras  2412000
2   Bandung  Beras  2160000

   produk  jumlah
0   Beras      40
1    Gula      30
2  Minyak      25
3    Kopi      20

jumlah baris: [12]
kolom: ['kota', 'produk', 'jumlah', 'harga']
dtype kota: str
database sementara terhapus bersama folder temp

Mari kita telaah baris-baris pentingnya:

  1. Baris 18-20 (TemporaryDirectory, create_engine(...)): database dibuat di folder sementara, dan create_engine("sqlite:///...") menghasilkan engine; pembuatan koneksi sebenarnya ditunda hingga query pertama.
  2. Baris 23 (penjualan.to_sql(...)): index=False mencegah index 0-11 ikut tersimpan sebagai kolom database, dan if_exists="replace" menimpa tabel bila sudah ada; pandas menyusun perintah CREATE TABLE beserta tipe kolomnya secara otomatis.
  3. Baris 28-32 (pd.read_sql(...)): string SQL dijalankan oleh engine; kolom hasil total berasal dari ekspresi jumlah * harga, dan ORDER BY ... DESC LIMIT 3 mengembalikan tiga transaksi terbesar.
  4. Baris 37-42 (params={"kota": "Jakarta"}): penanda :kota pada SQL dipasangkan dengan dictionary di params; nilai dikirim terpisah dari teks SQL sehingga isi params tidak pernah dieksekusi sebagai perintah.
  5. Baris 47-50 (engine.begin(), conn.execute(text(...))): perintah perubahan data (UPDATE, INSERT, DELETE) dijalankan lewat koneksi SQLAlchemy langsung; blok engine.begin() otomatis commit bila sukses dan membatalkan bila gagal. Perintah SQL dibungkus text() sesuai gaya SQLAlchemy 2.x.
  6. Baris 51-56 (read_sql, read_sql_table): COUNT(*) dikembalikan sebagai DataFrame satu sel, dan read_sql_table("penjualan", engine) membaca seluruh tabel — kolom teksnya kembali sebagai dtype str bawaan pandas 3.0.

Perhatikan bahwa hasil query selalu menjadi DataFrame lengkap dengan index baru dan dtype yang ditebak pandas dari tipe kolom database; kebutuhan penyempurnaan dtype cukup dilakukan setelahnya dengan astype().

Dengan database, data kini mengalir dua arah antara pandas dan penyimpanan relasional. Chapter berikutnya kembali ke meja kerja harian yang paling sering kita gunakan: Jupyter Notebook, tempat hasil read_sql biasanya langsung dieksplorasi.

Referensi resmi untuk pendalaman: dokumentasi SQLAlchemy , pandas.DataFrame.to_sql , dan pandas.read_sql .