DuckDB: Database Analitik In-Process untuk Data Science

# DuckDB: Database Analitik In-Process untuk Data Science DuckDB adalah database analitik in-process yang dirancang khusus untuk workload OLAP (Online Analytical Processing). Berbeda dengan database...

By Ruby Abdullah · · tutorial
DuckDBAnalyticsSQLData SciencePython

DuckDB: Database Analitik In-Process untuk Data Science

DuckDB adalah database analitik in-process yang dirancang khusus untuk workload OLAP (Online Analytical Processing). Berbeda dengan database tradisional seperti PostgreSQL atau MySQL yang memerlukan server terpisah, DuckDB berjalan langsung di dalam proses aplikasi Anda, mirip seperti SQLite tetapi dioptimalkan untuk analitik. Dalam tutorial ini, kita akan mempelajari cara menggunakan DuckDB untuk workflow data science yang efisien dan powerful.

Mengapa DuckDB?

Sebelum membahas teknis, mari pahami mengapa DuckDB menjadi pilihan populer di kalangan data scientist:

  • Tanpa server: Tidak perlu instalasi database server, konfigurasi, atau manajemen
  • Columnar storage: Data disimpan per kolom, optimal untuk query analitik
  • Vectorized execution: Memproses data dalam batch untuk performa maksimal
  • Query file langsung: Bisa query CSV, Parquet, dan JSON tanpa import terlebih dahulu
  • Integrasi Python: API Python yang seamless dengan Pandas dan Polars
  • SQL standar: Mendukung SQL yang kaya fitur termasuk window functions dan CTE

Instalasi

Instalasi DuckDB sangat mudah. Cukup gunakan pip:

pip install duckdb

Untuk verifikasi instalasi:

import duckdb

print(duckdb.version)

Jika Anda ingin menggunakan CLI DuckDB, bisa download binary dari website resmi atau gunakan:

pip install duckdb[cli]

Memulai dengan DuckDB

In-Memory Database

Cara paling sederhana untuk memulai adalah dengan in-memory database:

import duckdb

Membuat koneksi in-memory

con = duckdb.connect()

Atau secara eksplisit

con = duckdb.connect(database=':memory:')

Menjalankan query sederhana

result = con.sql("SELECT 42 AS jawaban, 'Halo DuckDB' AS pesan")

print(result.fetchall())

[(42, 'Halo DuckDB')]

Persistent Database

Untuk menyimpan data secara permanen, berikan nama file:

import duckdb

Membuat atau membuka database persisten

con = duckdb.connect('analitik.duckdb')

Membuat tabel

con.sql("""

CREATE TABLE IF NOT EXISTS penjualan (

id INTEGER PRIMARY KEY,

produk VARCHAR,

kategori VARCHAR,

jumlah INTEGER,

harga DECIMAL(10, 2),

tanggal DATE

)

""")

Insert data

con.sql("""

INSERT INTO penjualan VALUES

(1, 'Laptop Pro', 'Elektronik', 5, 15000000, '2026-01-15'),

(2, 'Mouse Wireless', 'Aksesoris', 50, 250000, '2026-01-16'),

(3, 'Keyboard Mekanik', 'Aksesoris', 30, 750000, '2026-01-16'),

(4, 'Monitor 27 inch', 'Elektronik', 10, 4500000, '2026-01-17'),

(5, 'Headset Gaming', 'Aksesoris', 25, 500000, '2026-01-18')

""")

Query data

result = con.sql("SELECT FROM penjualan WHERE kategori = 'Elektronik'")

result.show()

Query File Langsung Tanpa Import

Salah satu fitur paling powerful dari DuckDB adalah kemampuan query file langsung.

Query CSV

import duckdb

Query CSV langsung

result = duckdb.sql("""

SELECT

FROM readcsvauto('data/transaksi.csv')

WHERE total > 1000000

ORDER BY tanggal DESC

LIMIT 10

""")

result.show()

Dengan opsi spesifik

result = duckdb.sql("""

SELECT

FROM readcsv('data/transaksi.csv',

delim=',',

header=true,

dateformat='%Y-%m-%d'

)

""")

Query Parquet

Parquet adalah format file kolumnar yang sangat efisien. DuckDB membacanya secara native:

import duckdb

Query file Parquet

result = duckdb.sql("""

SELECT

kategori,

COUNT() AS jumlahtransaksi,

SUM(total) AS totalpendapatan,

AVG(total) AS ratarata

FROM readparquet('data/transaksi2026.parquet')

GROUP BY kategori

ORDER BY totalpendapatan DESC

""")

result.show()

Query multiple file Parquet dengan glob pattern

result = duckdb.sql("""

SELECT

FROM readparquet('data/transaksi.parquet')

""")

Query Parquet dari URL (S3, HTTP)

result = duckdb.sql("""

SELECT

FROM readparquet('https://example.com/data/sample.parquet')

LIMIT 100

""")

Query JSON

import duckdb

Query file JSON

result = duckdb.sql("""

SELECT

FROM readjsonauto('data/logaktivitas.json')

WHERE eventtype = 'purchase'

""")

result.show()

JSON Lines format

result = duckdb.sql("""

SELECT

FROM readjsonauto('data/events.jsonl', format='newlinedelimited')

""")

Python API dan Integrasi

Integrasi dengan Pandas

DuckDB terintegrasi sangat baik dengan Pandas DataFrame:

import duckdb

import pandas as pd

Membuat DataFrame Pandas

dfpenjualan = pd.DataFrame({

'produk': ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Headset'],

'kategori': ['Elektronik', 'Aksesoris', 'Aksesoris', 'Elektronik', 'Aksesoris'],

'harga': [15000000, 250000, 750000, 4500000, 500000],

'stok': [10, 100, 50, 20, 75]

})

Query DataFrame langsung dengan SQL

result = duckdb.sql("""

SELECT

kategori,

COUNT() AS jumlahproduk,

AVG(harga) AS ratarataharga,

SUM(stok) AS totalstok

FROM dfpenjualan

GROUP BY kategori

""")

Konversi hasil ke DataFrame

dfresult = result.df()

print(dfresult)

Gabungkan DataFrame dan file

dfpelanggan = pd.DataFrame({

'id': [1, 2, 3],

'nama': ['Andi', 'Budi', 'Citra'],

'kota': ['Jakarta', 'Surabaya', 'Bandung']

})

result = duckdb.sql("""

SELECT

p.nama,

p.kota,

t.

FROM dfpelanggan p

JOIN readcsvauto('data/transaksi.csv') t

ON p.id = t.pelangganid

ORDER BY t.total DESC

""")

Integrasi dengan Polars

DuckDB juga mendukung Polars, library DataFrame modern yang ditulis dalam Rust:

import duckdb

import polars as pl

Membuat Polars DataFrame

df = pl.DataFrame({

'nama': ['Produk A', 'Produk B', 'Produk C', 'Produk D'],

'penjualanq1': [1500, 2300, 800, 3100],

'penjualanq2': [1800, 2100, 1200, 2900],

'penjualanq3': [2000, 2500, 1500, 3300],

'penjualanq4': [2200, 2800, 1800, 3500]

})

Query Polars DataFrame dengan DuckDB

result = duckdb.sql("""

SELECT

nama,

penjualanq1 + penjualanq2 + penjualanq3 + penjualanq4 AS totaltahunan,

(penjualanq4 - penjualanq1)::FLOAT / penjualanq1 100 AS pertumbuhanpersen

FROM df

ORDER BY totaltahunan DESC

""")

Konversi ke Polars DataFrame

plresult = result.pl()

print(plresult)

Window Functions dan Aggregasi Lanjutan

DuckDB mendukung window functions yang powerful untuk analisis data:

import duckdb

con = duckdb.connect()

Membuat data sampel

con.sql("""

CREATE TABLE transaksi AS

SELECT FROM (VALUES

('2026-01-01', 'Elektronik', 5000000),

('2026-01-02', 'Pakaian', 1500000),

('2026-01-03', 'Elektronik', 3000000),

('2026-01-04', 'Makanan', 500000),

('2026-01-05', 'Pakaian', 2000000),

('2026-01-06', 'Elektronik', 7000000),

('2026-01-07', 'Makanan', 800000),

('2026-01-08', 'Pakaian', 1800000),

('2026-01-09', 'Elektronik', 4500000),

('2026-01-10', 'Makanan', 600000)

) AS t(tanggal, kategori, total)

""")

Running total per kategori

result = con.sql("""

SELECT

tanggal,

kategori,

total,

SUM(total) OVER (

PARTITION BY kategori

ORDER BY tanggal

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

) AS runningtotal,

ROWNUMBER() OVER (

PARTITION BY kategori

ORDER BY total DESC

) AS ranking,

LAG(total) OVER (

PARTITION BY kategori

ORDER BY tanggal

) AS totalsebelumnya,

total - COALESCE(LAG(total) OVER (

PARTITION BY kategori

ORDER BY tanggal

), 0) AS selisih

FROM transaksi

ORDER BY kategori, tanggal

""")

result.show()

Moving average

result = con.sql("""

SELECT

tanggal,

total,

AVG(total) OVER (

ORDER BY tanggal

ROWS BETWEEN 2 PRECEDING AND CURRENT ROW

) AS movingavg3hari,

NTILE(4) OVER (ORDER BY total) AS kuartil

FROM transaksi

""")

result.show()

Aggregasi dengan GROUPING SETS, ROLLUP, dan CUBE

# ROLLUP untuk subtotal bertingkat

result = con.sql("""

SELECT

kategori,

EXTRACT(MONTH FROM tanggal::DATE) AS bulan,

SUM(total) AS totalpenjualan,

COUNT() AS jumlahtransaksi

FROM transaksi

GROUP BY ROLLUP (kategori, EXTRACT(MONTH FROM tanggal::DATE))

ORDER BY kategori NULLS LAST, bulan NULLS LAST

""")

result.show()

Extensions DuckDB

DuckDB memiliki sistem extension yang memperluas fungsionalitasnya:

import duckdb

con = duckdb.connect()

Melihat extension yang tersedia

con.sql("SELECT FROM duckdbextensions()").show()

Install dan load extension

con.sql("INSTALL httpfs")

con.sql("LOAD httpfs")

Sekarang bisa query file dari HTTP/S3

result = con.sql("""

SELECT

FROM readparquet('https://example.com/data/sample.parquet')

LIMIT 10

""")

Extension untuk format spatial

con.sql("INSTALL spatial")

con.sql("LOAD spatial")

Extension JSON

con.sql("INSTALL json")

con.sql("LOAD json")

Extension full-text search

con.sql("INSTALL fts")

con.sql("LOAD fts")

Perbandingan Performa: DuckDB vs SQLite vs Pandas

Berikut perbandingan karakteristik ketiga tool populer ini:

import duckdb

import pandas as pd

import sqlite3

import time

Membuat dataset besar untuk benchmark

nrows = 1000000

duckdb.sql(f"""

CREATE TABLE benchmark AS

SELECT

i AS id,

'Produk' || (i % 1000) AS produk,

'Kategori' || (i % 50) AS kategori,

(random() 1000000)::INTEGER AS total,

'2026-01-01'::DATE + (i % 365) AS tanggal

FROM generateseries(1, {nrows}) AS t(i)

""")

Benchmark DuckDB

start = time.time()

resultduck = duckdb.sql("""

SELECT

kategori,

COUNT() AS cnt,

SUM(total) AS sumtotal,

AVG(total) AS avgtotal

FROM benchmark

GROUP BY kategori

ORDER BY sumtotal DESC

""").df()

timeduck = time.time() - start

print(f"DuckDB: {timeduck:.4f} detik")

Benchmark Pandas (equivalen)

df = duckdb.sql("SELECT FROM benchmark").df()

start = time.time()

resultpandas = (

df.groupby('kategori')

.agg(cnt=('id', 'count'), sumtotal=('total', 'sum'), avgtotal=('total', 'mean'))

.sortvalues('sumtotal', ascending=False)

.resetindex()

)

timepandas = time.time() - start

print(f"Pandas: {timepandas:.4f} detik")

Secara umum, keunggulan masing-masing:

| Aspek | DuckDB | SQLite | Pandas |

|-------|--------|--------|--------|

| Tipe workload | OLAP (Analitik) | OLTP (Transaksional) | Analitik |

| Storage | Columnar | Row-based | In-memory |

| Dataset besar | Sangat baik | Kurang optimal | Terbatas RAM |

| SQL support | Sangat kaya | Dasar | Tidak native |

| Memory usage | Efisien (streaming) | Efisien | Tinggi |

| Kecepatan aggregasi | Sangat cepat | Lambat | Cepat (kecil) |

Contoh Praktis: Analisis Dataset Besar Tanpa Load ke Memory

Berikut contoh nyata menganalisis dataset besar tanpa harus memuatnya sepenuhnya ke memori:

import duckdb

con = duckdb.connect()

Simulasi: buat dataset besar dan simpan sebagai Parquet

con.sql("""

COPY (

SELECT

i AS transactionid,

'CUST-' || LPAD((i % 10000)::VARCHAR, 5, '0') AS customerid,

CASE (i % 5)

WHEN 0 THEN 'Elektronik'

WHEN 1 THEN 'Pakaian'

WHEN 2 THEN 'Makanan'

WHEN 3 THEN 'Olahraga'

WHEN 4 THEN 'Buku'

END AS kategori,

CASE (i % 3)

WHEN 0 THEN 'Jakarta'

WHEN 1 THEN 'Surabaya'

WHEN 2 THEN 'Bandung'

END AS kota,

(random() 5000000 + 50000)::INTEGER AS total,

'2025-01-01'::DATE + (i % 365) AS tanggal

FROM generateseries(1, 5000000) AS t(i)

) TO 'datatransaksibesar.parquet' (FORMAT PARQUET)

""")

print("Dataset 5 juta baris berhasil dibuat!")

Analisis 1: Tren penjualan bulanan per kategori

result = con.sql("""

SELECT

DATETRUNC('month', tanggal) AS bulan,

kategori,

COUNT() AS jumlahtransaksi,

SUM(total) AS totalpenjualan,

AVG(total)::INTEGER AS rataratatransaksi

FROM readparquet('datatransaksibesar.parquet')

GROUP BY DATETRUNC('month', tanggal), kategori

ORDER BY bulan, totalpenjualan DESC

""")

print("\n=== Tren Penjualan Bulanan ===")

result.show()

Analisis 2: Top customer per kota

result = con.sql("""

WITH customerspending AS (

SELECT

customerid,

kota,

SUM(total) AS totalbelanja,

COUNT() AS frekuensi,

AVG(total)::INTEGER AS avgbelanja

FROM readparquet('datatransaksibesar.parquet')

GROUP BY customerid, kota

),

ranked AS (

SELECT

,

ROWNUMBER() OVER (

PARTITION BY kota

ORDER BY totalbelanja DESC

) AS ranking

FROM customerspending

)

SELECT kota, customerid, totalbelanja, frekuensi, avgbelanja

FROM ranked

WHERE ranking <= 5

ORDER BY kota, ranking

""")

print("\n=== Top 5 Customer per Kota ===")

result.show()

Analisis 3: Distribusi penjualan dengan percentile

result = con.sql("""

SELECT

kategori,

MIN(total) AS mintotal,

PERCENTILECONT(0.25) WITHIN GROUP (ORDER BY total)::INTEGER AS p25,

PERCENTILECONT(0.50) WITHIN GROUP (ORDER BY total)::INTEGER AS median,

PERCENTILECONT(0.75) WITHIN GROUP (ORDER BY total)::INTEGER AS p75,

MAX(total) AS maxtotal,

STDDEV(total)::INTEGER AS stddev

FROM readparquet('datatransaksibesar.parquet')

GROUP BY kategori

ORDER BY median DESC

""")

print("\n=== Distribusi Penjualan per Kategori ===")

result.show()

Analisis 4: Cohort analysis - retensi pelanggan

result = con.sql("""

WITH firstpurchase AS (

SELECT

customerid,

MIN(tanggal) AS tanggalpertama

FROM readparquet('datatransaksibesar.parquet')

GROUP BY customerid

),

cohortdata AS (

SELECT

DATETRUNC('month', fp.tanggalpertama) AS cohortbulan,

DATEDIFF('month', fp.tanggalpertama, t.tanggal) AS bulanke,

COUNT(DISTINCT t.customerid) AS jumlahcustomer

FROM readparquet('datatransaksibesar.parquet') t

JOIN firstpurchase fp ON t.customerid = fp.customerid

GROUP BY cohortbulan, bulanke

)

SELECT

cohortbulan,

bulanke,

jumlahcustomer

FROM cohortdata

WHERE bulanke <= 6

ORDER BY cohortbulan, bulanke

LIMIT 30

""")

print("\n=== Cohort Analysis ===")

result.show()

Export hasil analisis

con.sql("""

COPY (

SELECT

kota,

kategori,

DATETRUNC('month', tanggal) AS bulan,

SUM(total) AS totalpenjualan,

COUNT() AS jumlahtransaksi

FROM readparquet('datatransaksibesar.parquet')

GROUP BY kota, kategori, DATETRUNC('month', tanggal)

) TO 'ringkasanpenjualan.csv' (HEADER, DELIMITER ',')

""")

print("\nHasil analisis berhasil diekspor ke CSV!")

Tips dan Best Practices

1. Gunakan Parquet untuk Dataset Besar

# Konversi CSV ke Parquet untuk performa lebih baik

duckdb.sql("""

COPY (SELECT FROM readcsvauto('databesar.csv'))

TO 'databesar.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)

""")

2. Manfaatkan Lazy Evaluation

# Gunakan relation untuk lazy evaluation

rel = duckdb.sql("SELECT FROM readparquet('data.parquet')")

filtered = rel.filter("total > 1000000")

grouped = filtered.aggregate("kategori, SUM(total) AS total")

Query baru dieksekusi saat .show() atau .df() dipanggil

grouped.show()

3. Konfigurasi Memory dan Thread

import duckdb

con = duckdb.connect()

con.sql("SET memorylimit = '4GB'")

con.sql("SET threads = 4")

con.sql("SET tempdirectory = '/tmp/duckdbtemp'")

4. Gunakan EXPLAIN untuk Optimasi Query

con.sql("""

EXPLAIN ANALYZE

SELECT kategori, SUM(total)

FROM read_parquet('data.parquet')

GROUP BY kategori

""").show()

Kesimpulan

DuckDB adalah tool yang sangat powerful untuk data scientist. Kemampuannya untuk query file langsung, integrasi seamless dengan Python ecosystem, dan performa yang luar biasa untuk workload analitik menjadikannya pilihan ideal untuk:

  • Eksplorasi data awal (EDA) dengan SQL
  • Analisis dataset besar tanpa infrastruktur database
  • Prototyping pipeline data
  • Pengganti Pandas untuk dataset yang tidak muat di memori
  • ETL sederhana tanpa setup yang rumit

Dengan DuckDB, Anda mendapatkan kekuatan SQL analitik yang setara dengan data warehouse, langsung di laptop Anda tanpa perlu setup server apapun.

Artikel Terkait

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...

Tutorial PostgreSQL Advanced untuk ML: Analytics dan Feature Engineering

Tutorial 17: PostgreSQL Lanjutan untuk Machine Learning Daftar Isi Pendahuluan Prasyarat Window Functions untuk Rekayasa...

Tutorial Marimo: Notebook Python Reaktif dan Reproducible

Marimo: Notebook Python yang Reaktif dan Reproducible Marimo adalah notebook Python yang menyimpan isinya sebagai berkas...

Tutorial Kedro: Pipeline Data Science yang Reproducible dan Terstruktur

Kedro: Pipeline Data Science yang Reproducible dan Mudah Dirawat Sebagian besar proyek data science dimulai dari satu no...