DuckDB je in-process analitička SQL baza podataka koja se u Pythonu koristi za brzu obradu velikih tabularnih skupova podataka bez potrebe za posebnim serverom. Instalira se s pip install duckdb, a upite izvršava direktno nad Parquet, CSV, JSON i Pandas DataFrame-ovima uz vektorizirano stupčano izvršavanje. U ovom vodiču pokazujem kako u 2026. postaviti DuckDB u Pythonu, napisati produkcijski ETL pipeline, testirati transformacije s pytest-om i integrirati ga s Polarsom, dbt-om i cloud storageom.
DuckDB 1.4 (2026) je in-process OLAP baza, bez servera, a cijela biblioteka je jedan pip paket manji od 40 MB.
SQL upiti se u Pythonu pišu direktno nad Parquet, CSV i Pandas/Polars DataFrame-ovima bez ručnog učitavanja.
Za analitičke agregacije DuckDB je 10 do 100 puta brži od SQLite-a i tipično 2 do 5 puta brži od Pandasa na tablicama iznad 10 milijuna redaka.
httpfs ekstenzija čita Parquet direktno s S3, GCS ili Azure Blob-a, što je idealno za lakke ETL pipeline-e bez podizanja klastera.
Testiranje pipeline-a radi se s pytest fixture-ima koji koriste in-memory DuckDB instance, pa je svaki test izoliran i završi u milisekundama.
Za dbt korisnike, adapter dbt-duckdb omogućuje lokalni razvoj SQL modela bez oblak warehousea kao što je Snowflake ili BigQuery.
Što je DuckDB i zašto je bitan za data engineering
DuckDB je otvorenokodna analitička baza podataka koju stvara istraživački tim s CWI Amsterdam, a arhitektonski se pozicionira kao "SQLite za analitiku". Umjesto redova, podatke sprema u stupčanom formatu i izvršava vektorizirano, tako da svaka operacija radi nad blokom od nekoliko tisuća vrijednosti odjednom. To drastično poboljšava iskorištenost L2 cachea modernih CPU-a.
U mojoj svakodnevnoj praksi kao data inženjera, DuckDB je unatrag godinu dana potpuno zamijenio tri klase alata koje sam prije koristila. Prvo, ad-hoc analitika na Parquet ili CSV datotekama više ne zahtijeva učitavanje cijelog fajla u Pandas, nego samo SQL upit koji čita relevantne stupce. Drugo, staging sloj u lakim ETL-ovima ne mora živjeti u PostgreSQL-u nego u lokalnoj .duckdb datoteci. Treće, lokalno testiranje dbt modela više ne zahtijeva Snowflake konekciju za svaki merge request.
Verzija DuckDB 1.4, objavljena u prvoj polovici 2026., donijela je asinkroni httpfs, stabilnu podršku za DELETE/UPDATE nad Parquet particijama i unaprijeđeni parser JOIN klauzula. Za razliku od distribuiranih engineova poput Sparka, DuckDB radi na jednom čvoru. To ga čini optimalnim kad vaš dataset stane u desetke ili stotine gigabajta na disku, a ne kad krećete prema petabajtima.
Instalacija DuckDB-a u Python okruženju
Instalacija je namjerno dosadna: jedna komanda, bez ovisnosti o C bibliotekama koje treba ručno prevoditi.
Preporučujem pinnati minor verziju u requirements.txt-u ili pyproject.toml-u. DuckDB se razvija brzo i patch verzije unutar iste minor grane su binarno kompatibilne, ali skok s 1.3 na 1.4 promijenio je defaultno ponašanje parsera za NULL-safe usporedbe (bugove smo lovili tjedan dana).
Nakon instalacije pokrenite kratki sanity check:
import duckdb
print(duckdb.__version__)
print(duckdb.sql("SELECT 42 AS answer, current_date AS today").fetchall())
# ('1.4.x',) i [(42, datetime.date(2026, 7, 21))]
Kako pokrenuti SQL upit u Pythonu s DuckDB-om?
Za pokretanje SQL upita u Pythonu s DuckDB-om koristite funkciju duckdb.sql() koja vraća DuckDBPyRelation objekt, tj. lazy handle koji se materijalizira tek kad ga konvertirate u .df(), .pl() ili .arrow(). Za dulje sesije bolje je otvoriti konekciju eksplicitno preko duckdb.connect().
import duckdb
con = duckdb.connect(database=":memory:")
# Registriraj Pandas DataFrame kao virtualnu tablicu
import pandas as pd
sales = pd.DataFrame({
"order_id": [1, 2, 3, 4, 5],
"region": ["EU", "US", "EU", "APAC", "US"],
"amount": [120.50, 85.00, 200.00, 55.75, 340.10],
})
con.register("sales", sales)
result = con.sql("""
SELECT region,
COUNT(*) AS orders,
SUM(amount) AS revenue,
AVG(amount) AS avg_ticket
FROM sales
GROUP BY region
ORDER BY revenue DESC
""").df()
print(result)
Bitan detalj koji me jednom skupo koštao. DuckDB nema fazu "kopiranja" DataFrame-a u internu tablicu. Kad pozovete register, on samo zapamti Python referencu i čita blokove Arrow buffera on-demand. To znači da ne smijete mutirati originalni DataFrame između poziva na isti upit, jer ćete dobiti nedeterministične rezultate. Ako mutirate podatke, kopirajte ih ili koristite CREATE TABLE AS SELECT koji materijalizira snimku unutar DuckDB-a.
Čitanje Parquet, CSV i JSON datoteka direktno
Najsnažnija značajka DuckDB-a za data inženjere je što ne trebate prvo učitati podatke, jer SQL parser prihvaća putanje datoteka na mjestu tablica:
import duckdb
# Jedna Parquet datoteka
con = duckdb.connect()
con.sql("SELECT COUNT(*) FROM 'data/events_2026_07.parquet'").show()
# Glob preko particija (godina/mjesec/dan struktura)
con.sql("""
SELECT date_trunc('day', event_ts) AS day,
COUNT(*) AS events
FROM 'data/events/year=2026/month=07/day=*/*.parquet'
WHERE event_type = 'purchase'
GROUP BY day
ORDER BY day
""").show()
# CSV s automatskim otkrivanjem tipova
con.sql("""
SELECT * FROM read_csv_auto('logs/access.csv',
header = true,
sample_size = 20000)
LIMIT 10
""").show()
Projekcija stupaca i predikat pushdown rade automatski. DuckDB će iz Parqueta pročitati samo stupce spomenute u SELECT-u i preskočiti row groupove koji ne odgovaraju WHERE uvjetima. Ovo je najveća razlika prema pandas.read_parquet(...)-u koji cijeli fajl uvijek povuče u memoriju.
Integracija s Pandas i Polars DataFrame-ovima
DuckDB podržava dvosmjernu razmjenu s glavnim Python DataFrame bibliotekama preko Apache Arrow-a. Ako trebate kombinirati SQL agregacije s Pandas obradom, evo šablone koju koristim u većini projekata:
import duckdb
import pandas as pd
raw = pd.read_csv("transactions.csv", parse_dates=["ts"])
# SQL za teški deo (agregacija, join, window)
enriched = duckdb.sql("""
SELECT user_id,
COUNT(*) AS txn_count,
SUM(amount) AS total_spent,
MAX(ts) AS last_seen,
LAG(amount) OVER (PARTITION BY user_id ORDER BY ts) AS prev_amount
FROM raw
GROUP BY user_id, ts, amount
""").df()
# Pandas za feature engineering korak
enriched["days_since_seen"] = (pd.Timestamp("2026-07-21") - enriched["last_seen"]).dt.days
enriched["is_high_value"] = enriched["total_spent"] > 1000
print(enriched.head())
Za Polars integraciju koristite .pl() umjesto .df(). Cijeli prijenos ide zero-copy preko Arrow buffera, tako da ni jedno rješenje ne kopira podatke u RAM više puta. Više o tome kako Pandas rješava povezano čišćenje pročitajte u našem vodiču o rukovanju nedostajućim podacima s Pandas fillna i dropna, a za kombiniranje tablica pogledajte Pandas merge, join i concat vodič.
DuckDB vs SQLite vs Polars: koju alatku odabrati?
Sva tri alata dijele isti in-process model (nema servera), ali imaju vrlo različite ciljeve. Tablica ispod sažima kad koji odabrati.
Značajka
DuckDB 1.4
SQLite 3.46
Polars 1.x
Primarni slučaj
OLAP / analitika
OLTP / aplikacijski storage
DataFrame ETL
Format skladištenja
Stupčano
Redovi
Stupčano (Arrow)
Vektorizirano izvršavanje
Da
Ne
Da
Sučelje
SQL + Python API
SQL
Python (Rust) DSL
Čitanje Parqueta direktno
Da
Ne
Da
Cloud storage (S3/GCS)
Da (httpfs)
Ne
Djelomično
Concurrent writes
Ograničeno
Da
N/A
Instalacija
Jedan pip paket
Ugrađen u Python
Jedan pip paket
Praktični izbor u mojoj praksi: SQLite za konfiguracijske i tranzakcijske podatke aplikacije, DuckDB za analitiku i staging sloj u pipeline-ovima, a Polars kad transformacije žele idiomatski Python DSL umjesto SQL-a. Iskreno, nije neuobičajeno vidjeti sva tri alata u istom projektu.
In-memory vs perzistentna baza
DuckDB podržava dva načina rada: kratkotrajnu in-memory instancu i perzistentnu .duckdb datoteku na disku. Odabir utječe na performanse, ali još važnije, na obrasce testiranja i deploya.
import duckdb
# In-memory: nestaje kad Python proces završi
con_mem = duckdb.connect(database=":memory:")
# Perzistentno: jedna datoteka koju možete verzionirati
con_disk = duckdb.connect(database="warehouse.duckdb")
con_disk.sql("CREATE OR REPLACE TABLE fact_orders AS SELECT * FROM 'orders/*.parquet'")
con_disk.close()
Perzistentna datoteka ima ACID transakcije i podržava WAL, tako da restart nakon crash-a ne gubi commitane podatke. In-memory instance su idealne za jedinice testiranja i kratke ad-hoc analize. Za produkcijski ETL koji svake noći radi backfill, ja koristim perzistentnu .duckdb datoteku kao staging sloj koji čistim tjedno.
Korisne ekstenzije: httpfs, spatial i JSON
DuckDB dolazi s core setom funkcija, ali stvarna moć je u ekstenzijama koje se učitavaju runtime-om. Tri koje najčešće koristim:
httpfs, za čitanje i pisanje na S3, GCS, Azure Blob i običnom HTTPS-u.
spatial, za GEOMETRY tip, ST_* funkcije, čitanje Shapefile/GeoJSON.
json, za parsiranje ugniježđenih JSON polja s json_extract i strukturnim tipovima.
import duckdb
con = duckdb.connect()
con.sql("INSTALL httpfs; LOAD httpfs;")
con.sql("SET s3_region = 'eu-west-1';")
con.sql("""
CREATE OR REPLACE TABLE daily_metrics AS
SELECT date_trunc('day', event_ts) AS day,
user_id,
COUNT(*) AS event_count
FROM read_parquet('s3://analytics-lake/events/2026/*/*.parquet')
GROUP BY 1, 2
""")
S3 kredencijali dolaze iz uobičajenog AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env varijabli ili preko IAM role kad je code na EC2. Detaljnu referencu možete provjeriti u službenoj httpfs dokumentaciji.
Produkcijski ETL pipeline s DuckDB-om
Sljedeći primjer prikazuje minimalni, ali produkcijski upotrebljiv ETL modul. Struktura je namjerno funkcionalna, pa svaki korak vraća relaciju, tako da se lako testira i komponira.
from __future__ import annotations
from pathlib import Path
import duckdb
def build_daily_orders(con: duckdb.DuckDBPyConnection, source_glob: str) -> None:
"""
Idempotentan ETL korak: čita sirove ordere iz Parquet-a
i piše dnevnu agregaciju u fact_daily_orders tablicu.
"""
con.sql(f"""
CREATE OR REPLACE TABLE staging_orders AS
SELECT order_id,
user_id,
region,
amount,
CAST(order_ts AS TIMESTAMP) AS order_ts
FROM read_parquet('{source_glob}')
WHERE amount IS NOT NULL AND amount > 0
""")
con.sql("""
CREATE OR REPLACE TABLE fact_daily_orders AS
SELECT date_trunc('day', order_ts) AS order_day,
region,
COUNT(*) AS orders,
SUM(amount) AS gross_revenue,
COUNT(DISTINCT user_id) AS unique_customers
FROM staging_orders
GROUP BY 1, 2
""")
def main() -> None:
warehouse = Path("warehouse.duckdb")
with duckdb.connect(str(warehouse)) as con:
build_daily_orders(con, "landing/orders/*.parquet")
row_count = con.sql(
"SELECT COUNT(*) FROM fact_daily_orders"
).fetchone()[0]
print(f"fact_daily_orders: {row_count} redaka")
if __name__ == "__main__":
main()
Ovakav layout uklapa se ravno u Airflow, Prefect ili GitHub Actions cron. Backfill je trivijalan (samo mijenjate source_glob parametar), i nema DELETE + INSERT pesme koju obično imate na row-oriented warehousima.
Testiranje ETL pipeline-a s pytest-om
Pravilo koje ponavljam svakom novom članu tima: ako je transformacija dovoljno važna da završi u produkciji, dovoljno je važna i za test. DuckDB-ova in-memory priroda čini pipeline testove trivijalno brzim, tako da svaki test počinje s praznim state-om u milisekundama.
Test pokriva jedno konkretno pravilo: nule i negativne iznose ne smiju završiti u fact_daily_orders. Za slojevito testiranje ML koraka koji dolaze iza ETL-a, pogledajte moj vodič o scikit-learn Pipeline-u i ColumnTransformer-u. Ako preferirate deklarativnije provjere umjesto ručnog assert-a, biblioteka Great Expectations radi lijepo s DuckDB-om.
Kombinacija s dbt-om preko dbt-duckdb adaptera
Ako vaš tim već piše dbt modele za Snowflake ili BigQuery, adapter dbt-duckdb vam daje lokalni razvojni loop bez čekanja na warehouse. Instalacija:
U profiles.yml-u konfigurirate lokalnu .duckdb datoteku kao target, a dbt run materijalizira modele točno onako kako bi ih materijalizirao Snowflake. Ovo mi je promijenilo razvojni ciklus dramatično; ono što je nekad tražilo 3 minute waita na warehouse compute kredite sada završi za sekundu.
Za analitičke agregacije nad tablicama iznad 10 milijuna redaka, DuckDB je tipično 2 do 5 puta brži od Pandasa, a memorijska potrošnja mu je često manja od polovice. Razlika raste sa skalom, jer na 100 milijuna redaka DuckDB često ostane brz dok Pandas prijeđe u swap.
Može li DuckDB zamijeniti PostgreSQL u produkciji?
Ne za OLTP terete koji zahtijevaju konkurentni upis mnogih malih transakcija, jer DuckDB dopušta samo jedan writer proces po datoteci. Za analitički warehouse, staging sloj ili ad-hoc reporting nad Parquet lakeom, DuckDB je često bolji izbor.
Podržava li DuckDB čitanje datoteka s S3-a?
Da, preko httpfs ekstenzije. Nakon INSTALL httpfs; LOAD httpfs; možete koristiti s3://bucket/path/*.parquet putanje u bilo kojem SQL upitu. Kredencijale čita iz standardnih AWS_* env varijabli ili IAM role.
Kako testirati DuckDB ETL kod?
Koristite pytest fixture koji otvara duckdb.connect(":memory:") instancu po testu i pripremite mali Parquet fixture pomoću COPY (SELECT ... FROM VALUES) TO 'file.parquet'. Testovi tako startaju za milisekunde i ostaju potpuno izolirani.
Radi li DuckDB s dbt-om?
Da, preko službenog dbt-duckdb adaptera. Vaši postojeći SQL modeli iz Snowflakea ili BigQueryja se u velikoj većini prevode bez izmjena, a lokalni dbt run završi u sekundi umjesto minutu (savršeno za razvoj i CI provjere).
Praktičan vodič za scikit-learn Pipeline i ColumnTransformer. Kako spriječiti data leakage, tunirati hiperparametre s GridSearchCV, pisati custom transformere i deployati Pipeline u produkciju s joblibom.
Pandas .str akcesor vektorizira Python string metode nad DataFrame stupcima. Vodič pokriva contains, replace, split, extract i PyArrow StringDtype u pandas 3.0 s primjerima koda.
Praktični vodič za rukovanje NaN vrijednostima u pandasu 2.2: fillna, dropna, ffill, bfill i interpolate, s primjerima iz stvarnih projekata i smjernicama kada koja metoda ima smisla.