DuckDB u Pythonu: Analiza podataka i ETL pipeline vodič za 2026

Kompletan vodič za DuckDB u Pythonu za 2026: instalacija, SQL upiti nad Parquet i Pandasom, ETL pipeline, pytest testovi i dbt-duckdb integracija.

DuckDB Python ETL Vodič (2026)

Ažurirano: 21. srpnja 2026.

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.

python -m venv .venv
source .venv/bin/activate
pip install "duckdb==1.4.*" pandas pyarrow polars

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čajkaDuckDB 1.4SQLite 3.46Polars 1.x
Primarni slučajOLAP / analitikaOLTP / aplikacijski storageDataFrame ETL
Format skladištenjaStupčanoRedoviStupčano (Arrow)
Vektorizirano izvršavanjeDaNeDa
SučeljeSQL + Python APISQLPython (Rust) DSL
Čitanje Parqueta direktnoDaNeDa
Cloud storage (S3/GCS)Da (httpfs)NeDjelomično
Concurrent writesOgraničenoDaN/A
InstalacijaJedan pip paketUgrađen u PythonJedan 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.

import duckdb
import pytest
from etl.orders import build_daily_orders


@pytest.fixture
def con():
    connection = duckdb.connect(":memory:")
    yield connection
    connection.close()


def test_build_daily_orders_filters_non_positive_amounts(con, tmp_path):
    fixture = tmp_path / "orders.parquet"
    duckdb.sql(f"""
        COPY (
            SELECT * FROM (VALUES
                (1, 100, 'EU',   50.0, TIMESTAMP '2026-07-01 10:00:00'),
                (2, 101, 'US',   -5.0, TIMESTAMP '2026-07-01 11:00:00'),
                (3, 102, 'EU',    0.0, TIMESTAMP '2026-07-01 12:00:00'),
                (4, 103, 'APAC', 25.0, TIMESTAMP '2026-07-02 09:00:00'),
            ) t(order_id, user_id, region, amount, order_ts)
        ) TO '{fixture}' (FORMAT PARQUET)
    """)

    build_daily_orders(con, str(fixture))

    result = con.sql("""
        SELECT order_day, region, orders, gross_revenue
        FROM fact_daily_orders
        ORDER BY order_day, region
    """).fetchall()

    assert result == [
        ('2026-07-01', 'EU',   1, 50.0),
        ('2026-07-02', 'APAC', 1, 25.0),
    ]

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:

pip install "dbt-core==1.9.*" "dbt-duckdb==1.9.*"
dbt init my_project

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 detaljan pregled dbt-duckdb funkcionalnosti i podržanih makronaredbi, službeni dbt-duckdb GitHub repozitorij je autoritativni izvor. Za DuckDB Python API reference i sve read_* funkcije, koristim službenu DuckDB Python dokumentaciju.

Često postavljana pitanja

Koliko je DuckDB brz u usporedbi s Pandasom?

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

Hannah Walsh
O Autoru Hannah Walsh

Data engineer making sure the pipelines feeding the models don't silently break at 3am. Big fan of dbt and bigger fan of testing.