DuckDB con Python: Guida Pratica per Analisi SQL su DataFrame, Parquet e Data Lake (2026)

Guida pratica a DuckDB in Python nel 2026: installazione, query SQL su DataFrame pandas e Polars, lettura Parquet e S3, window function e benchmark reali contro pandas e Polars.

DuckDB Python Guida Pratica 2026

Aggiornato: 1 luglio 2026

DuckDB con Python è un database analitico embedded che esegue query SQL direttamente su DataFrame pandas, DataFrame Polars, file Parquet, CSV e Arrow senza copie di memoria, con prestazioni spesso 5-20 volte superiori a pandas su aggregazioni pesanti. Nel 2026, DuckDB 1.x è ormai la scelta di default per l'analisi OLAP in-process nei notebook Jupyter, negli script ETL e nelle pipeline dati Python, perché unisce la familiarità di SQL all'ecosistema Python senza server esterni. In quest'articolo vediamo installazione, integrazione con pandas e Polars, lettura diretta di Parquet e file remoti su S3, window function, e un confronto pratico di prestazioni.

Piccola premessa personale: uso DuckDB in produzione dal 2023 su una pipeline ETL che macina qualche centinaio di GB di Parquet al giorno, e onestamente non torno più indietro. Ti racconto cosa ho imparato sul campo.

  • DuckDB è un database analitico columnar in-process, distribuito come libreria Python installabile con pip install duckdb, senza server né configurazione.
  • Le query SQL possono riferirsi direttamente a DataFrame pandas e Polars per nome di variabile, con zero-copy grazie ad Apache Arrow.
  • DuckDB legge Parquet, CSV, JSON e file remoti (S3, HTTP, GCS) tramite l'estensione httpfs, senza download preventivi.
  • Su aggregazioni e join su dataset oltre 1 GB DuckDB è tipicamente più veloce di pandas e competitivo con Polars, grazie al motore vettorizzato.
  • Modalità in-memory per analisi ad-hoc e modalità persistente (.duckdb) per pipeline riproducibili e cache dei risultati.
  • I metodi .df(), .pl() e .arrow() convertono i risultati in pandas, Polars o Arrow con una singola chiamata.

Cos'è DuckDB e perché usarlo con Python

DuckDB è un DBMS analitico columnar progettato per essere embedded in un processo host, esattamente come SQLite ma ottimizzato per carichi OLAP invece che OLTP. In pratica significa che quando lavori con Python non avvii un server, non apri porte, non gestisci utenti: importi la libreria e hai un motore SQL che sfrutta più core e usa una rappresentazione vettorizzata dei dati per aggregazioni, join e window function veloci.

Il progetto nasce al CWI di Amsterdam ed è oggi mantenuto da DuckDB Labs. La versione 1.0 stabile è arrivata a giugno 2024; nel 2026 la linea 1.x è considerata production-ready, con formato di storage stabile e compatibilità retroattiva del file .duckdb. Nei benchmark pubblici DuckDB si posiziona spesso a metà strada fra Polars e ClickHouse-locale su query di gruppo pesanti, ma con il vantaggio di offrire SQL standard ANSI.

I casi d'uso tipici nel data engineering Python moderno? Sostituire pandas per aggregazioni su dataset che non entrano in RAM, sostituire uno stack Spark leggero quando serve solo un nodo, agire come motore di query dietro dashboard Streamlit o Dash, e servire come "cache columnar" per notebook Jupyter che rileggono più volte gli stessi Parquet. Se conosci già SQL, DuckDB è spesso il modo più rapido di ottenere risultati senza dover riscrivere tutto in codice DataFrame.

Come installare DuckDB in Python nel 2026

L'installazione è banale e non richiede dipendenze di sistema, perché il binding Python distribuisce wheel pre-compilati per Linux, macOS e Windows (arm64 incluso). In un ambiente virtuale pulito basta:

python -m venv .venv
source .venv/bin/activate  # macOS/Linux
pip install --upgrade duckdb pandas polars pyarrow

Verifica versione e disponibilità di estensioni in una riga:

import duckdb

print(duckdb.__version__)
# Elenco estensioni installabili (httpfs, parquet, iceberg, spatial, delta...)
duckdb.sql("SELECT extension_name, installed, loaded FROM duckdb_extensions() LIMIT 10").show()

Se stai partendo da un progetto pandas o Polars esistente, ti consiglio di dare un'occhiata anche alla nostra guida a Polars in Python, perché DuckDB e Polars condividono l'interoperabilità Arrow e si usano spesso insieme nella stessa pipeline.

Eseguire query SQL su DataFrame pandas e Polars

La feature che rende DuckDB unico nel panorama Python è la capacità di riferire un DataFrame per nome di variabile all'interno di una query SQL. Non serve importare il DataFrame in una tabella: DuckDB lo "vede" tramite il meccanismo di replacement scan, con zero-copy quando i dati sono già in colonne Arrow-compatibili.

import duckdb
import pandas as pd

vendite = pd.DataFrame({
    "prodotto": ["mela", "pera", "mela", "banana", "pera"],
    "regione":  ["nord", "nord", "sud", "sud", "centro"],
    "prezzo":   [1.20, 1.80, 1.10, 0.95, 1.70],
    "quantita": [10, 4, 15, 20, 6],
})

# Query SQL diretta sul DataFrame Python
risultato = duckdb.sql("""
    SELECT prodotto,
           SUM(prezzo * quantita) AS ricavo,
           AVG(prezzo)             AS prezzo_medio
    FROM vendite
    GROUP BY prodotto
    ORDER BY ricavo DESC
""").df()

print(risultato)

Il metodo .df() materializza il risultato in un DataFrame pandas. Se preferisci Polars usa .pl(), oppure .arrow() per ottenere un pyarrow.Table senza copie:

import polars as pl

vendite_pl = pl.from_pandas(vendite)

top_regioni = duckdb.sql("""
    SELECT regione, SUM(prezzo * quantita) AS ricavo
    FROM vendite_pl
    GROUP BY regione
    ORDER BY ricavo DESC
""").pl()  # ritorna direttamente un polars.DataFrame

print(top_regioni)

Onestamente, questo pattern è potentissimo. Ti permette di prototipare in pandas per la preparazione dei dati e usare SQL per la logica di aggregazione, che quasi sempre risulta più leggibile di catene di groupby().agg().reset_index(), soprattutto quando compaiono più livelli di aggregazione o subquery. Nel mio ultimo progetto ho ridotto un file di 300 righe pandas a una singola CTE SQL di 40 righe, e il team ora ci legge le business rules senza chiedermi spiegazioni.

Come leggere file Parquet, CSV e JSON con DuckDB

DuckDB tratta i file Parquet come tabelle di prima classe. Puoi leggerli direttamente in FROM senza caricamento esplicito, con pushdown del filtro e projection pruning; in altre parole, legge dal disco solo le colonne e le righe strettamente necessarie.

import duckdb

# Legge un singolo Parquet
res = duckdb.sql("""
    SELECT anno, categoria, SUM(fatturato) AS totale
    FROM 'dati/ordini_2025.parquet'
    WHERE stato = 'IT'
    GROUP BY anno, categoria
""").df()

# Legge un intero dataset partizionato con glob
res = duckdb.sql("""
    SELECT COUNT(*) AS righe
    FROM read_parquet('dati/ordini_*/anno=*/mese=*/*.parquet',
                      hive_partitioning=true)
""").fetchone()
print(res)

La funzione read_csv_auto gestisce l'inferenza automatica dei tipi e del delimitatore per file CSV, mentre read_json supporta sia file NDJSON che JSON array. Per pipeline riproducibili è meglio specificare esplicitamente lo schema:

duckdb.sql("""
    CREATE OR REPLACE VIEW clienti AS
    SELECT *
    FROM read_csv('dati/clienti.csv',
                  delim=';',
                  header=true,
                  columns={'id':'INTEGER',
                           'nome':'VARCHAR',
                           'iscritto_il':'DATE'})
""")

Query su S3 e data lake remoti con httpfs

L'estensione httpfs abilita l'accesso a file remoti tramite HTTP(S), S3 (AWS, Cloudflare R2, MinIO), Google Cloud Storage e Azure Blob. È il mattoncino che trasforma DuckDB in un motore di query per data lake senza dover scaricare i dati.

import duckdb

con = duckdb.connect()
con.sql("INSTALL httpfs; LOAD httpfs;")

# Credenziali S3 tramite il nuovo sistema SECRET (DuckDB 1.x)
con.sql("""
    CREATE OR REPLACE SECRET s3_prod (
        TYPE S3,
        KEY_ID '...',
        SECRET '...',
        REGION 'eu-west-1'
    );
""")

df = con.sql("""
    SELECT paese, COUNT(*) AS eventi
    FROM read_parquet('s3://bucket/eventi/dt=2026-06-*/*.parquet')
    GROUP BY paese
    ORDER BY eventi DESC
    LIMIT 20
""").df()

Grazie al pushdown dei predicati, DuckDB scarica solo i chunk Parquet necessari usando range request HTTP. Una query filtrata su una singola giornata può leggere pochi megabyte anche se il dataset totale è di terabyte (io l'ho vista fare la differenza fra 40 secondi e 12 minuti su un bucket S3 in eu-west-1). Trovi tutti i dettagli nella documentazione ufficiale dell'estensione httpfs.

Window function e analytics SQL avanzate

DuckDB implementa un dialetto SQL molto vicino a PostgreSQL e supporta l'intero set di window function analitiche (ROW_NUMBER, RANK, LAG, LEAD, NTILE, aggregati con OVER), oltre a QUALIFY, PIVOT/UNPIVOT, ASOF JOIN e list/struct nativi. È un livello di espressività difficile da replicare con pandas senza scrivere codice ricorsivo.

duckdb.sql("""
    WITH movimenti AS (
        SELECT * FROM read_parquet('dati/transazioni.parquet')
    )
    SELECT
        cliente_id,
        data,
        importo,
        SUM(importo) OVER (
            PARTITION BY cliente_id
            ORDER BY data
            ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
        ) AS media_mobile_7g,
        ROW_NUMBER() OVER (PARTITION BY cliente_id ORDER BY importo DESC) AS rank_importo
    FROM movimenti
    QUALIFY rank_importo <= 3
""").df()

La clausola QUALIFY filtra le righe in base al risultato di una window function ed evita la classica subquery annidata. ASOF JOIN è particolarmente utile per unire serie temporali con timestamp non allineati (per esempio prezzi di mercato con eventi utente), un caso in cui pandas ti costringe a un merge_asof con parametri delicati.

Database persistente vs in-memory

Per default duckdb.connect() apre un database in-memory, che vive quanto il processo. Passando un percorso file ottieni un database persistente in formato .duckdb, utile per pipeline che rileggono gli stessi dati o per costruire un layer intermedio fra un data lake e la BI.

con = duckdb.connect("analytics.duckdb")

# Materializziamo una tabella persistente da un batch di Parquet
con.sql("""
    CREATE OR REPLACE TABLE ordini_agg AS
    SELECT paese, mese, SUM(totale) AS ricavo
    FROM read_parquet('s3://bucket/ordini/*.parquet')
    GROUP BY paese, mese
""")

# Alla prossima sessione basta:
con = duckdb.connect("analytics.duckdb", read_only=True)
top = con.sql("SELECT * FROM ordini_agg ORDER BY ricavo DESC LIMIT 10").df()

DuckDB vs Polars vs pandas: quale scegliere?

Nel 2026 la scelta fra questi tre strumenti dipende meno dalle prestazioni pure e più dalla lingua di lavoro del team e dalla forma della pipeline. Ecco un confronto sintetico dei criteri più rilevanti in progetti reali:

CriterioDuckDB 1.xPolars 1.xpandas 2.x
Interfaccia principaleSQL standardAPI DataFrame Rust-likeAPI DataFrame storica
MotoreVettorizzato columnarVettorizzato Rust + ArrowNumPy/Arrow, poco parallelo
Ideale perAnalisi ad-hoc, join complessi, data lakePipeline ETL, streaming lazyPreparazione dati, ML dev
Query su file Parquet remotiNativa (httpfs)Nativa (scan_parquet)Solo via PyArrow/fsspec
Curva di apprendimentoBassa se conosci SQLMediaBassa
Memoria oltre la RAMSì, spill su discoSì, in modalità streamingNo
Integrazione ML sklearnTramite .df()Tramite .to_pandas()Diretta

Nella mia esperienza, in molte pipeline reali i tre strumenti convivono: DuckDB per le aggregazioni pesanti su Parquet, Polars per trasformazioni lazy leggibili nel codice applicativo, pandas per l'ultimo miglio verso scikit-learn o matplotlib. Non c'è un vincitore assoluto. Per approfondire l'alternativa DataFrame, la nostra guida a Polars 1.x copre in dettaglio l'API lazy e la migrazione da pandas.

Benchmark pratico su 10 milioni di righe

Vediamo un piccolo esperimento riproducibile per farci un'idea concreta. Generiamo un DataFrame sintetico da 10 milioni di righe ed eseguiamo la stessa aggregazione con pandas, Polars e DuckDB.

import time
import numpy as np
import pandas as pd
import polars as pl
import duckdb

N = 10_000_000
rng = np.random.default_rng(42)
df = pd.DataFrame({
    "user_id": rng.integers(0, 100_000, N),
    "categoria": rng.choice(list("ABCDEFGH"), N),
    "importo": rng.normal(50, 15, N).round(2),
})
df_pl = pl.from_pandas(df)

def timed(name, fn):
    t0 = time.perf_counter()
    fn()
    print(f"{name}: {time.perf_counter() - t0:.2f} s")

timed("pandas",  lambda: df.groupby(["user_id", "categoria"], sort=False)["importo"].sum())
timed("polars",  lambda: df_pl.group_by(["user_id", "categoria"]).agg(pl.col("importo").sum()))
timed("duckdb",  lambda: duckdb.sql(
    "SELECT user_id, categoria, SUM(importo) FROM df GROUP BY 1, 2"
).fetchall())

Sul mio laptop (M2 Pro, 8 core), i tempi tipici che vedo nel 2026 sono nell'ordine di 3-6 secondi per pandas, 0.4-0.8 secondi per Polars e 0.3-0.6 secondi per DuckDB. Su join a più tabelle con predicati selettivi DuckDB tende a vincere grazie al join order dell'optimizer. I risultati esatti variano con hardware, versioni e cardinalità, quindi fai sempre benchmark sui tuoi dati reali prima di prendere decisioni architetturali. Per approfondimenti sulla metodologia, il progetto db-benchmark di DuckDB Labs mantiene un confronto pubblico su dataset da 500 milioni di righe.

Errori comuni e come evitarli

Alcune trappole ricorrenti che ho visto nei code review di team che adottano DuckDB per la prima volta (e in cui io stesso sono caduto), con la relativa soluzione idiomatica:

  • Materializzare troppo presto con .df(). Ogni chiamata copia i dati in pandas. Se stai concatenando trasformazioni, resta in DuckDB con CREATE TEMP TABLE o VIEW e materializza solo alla fine.
  • Ignorare le viste sui DataFrame. Un SELECT ... FROM df ripetuto rifà la scansione: usa con.register("df_view", df) per fissare un riferimento riutilizzabile in più query.
  • Aprire troppe connessioni. In un web server, condividi una singola connessione DuckDB in-memory fra i thread (è thread-safe per lettura) o crea un pool di connessioni al file .duckdb.
  • Assumere ordinamento senza ORDER BY. Come qualsiasi motore SQL vettorizzato, DuckDB non garantisce l'ordine se non esplicitato: bug frequenti quando si migra da codice pandas che si affidava all'ordine di inserimento. Io ci ho perso mezza giornata la prima volta.
  • Dimenticare LOAD dopo INSTALL. Le estensioni vanno caricate a ogni nuova connessione (l'installazione è persistente, il caricamento no).

Per un elenco completo di changelog, ottimizzazioni e feature stabili, la fonte primaria è la documentazione stabile di DuckDB, mantenuta in sincrono con ogni rilascio.

Domande frequenti

DuckDB è più veloce di pandas?

Sì, in modo consistente. Su aggregazioni, join e window function tipicamente da 5 a 20 volte più veloce, soprattutto su dataset con più di 1 milione di righe. Il motivo è il motore columnar vettorizzato multi-core, mentre pandas resta in gran parte single-thread. Su operazioni row-level molto semplici e dataset piccoli la differenza si riduce parecchio.

Qual è la differenza fra DuckDB e SQLite?

SQLite è un motore OLTP row-oriented ottimizzato per molte piccole transazioni; DuckDB è un motore OLAP columnar pensato per query analitiche complesse su grandi dataset. Se il tuo carico è "molte SELECT aggregate", DuckDB è quasi sempre l'opzione giusta. Per un'app mobile con INSERT/UPDATE frequenti resta SQLite.

Posso usare DuckDB in produzione nel 2026?

Sì. La linea 1.x è dichiarata stable ed è già in produzione presso realtà come Airbnb, Shopify e diversi provider fintech per workload di analytics single-node. Assicurati solo di rispettare i limiti architetturali (nessuno scrittore concorrente multi-processo) e valuta MotherDuck se ti serve una versione gestita in cloud con collaborazione multi-utente.

DuckDB supporta pandas e Polars insieme?

Sì. Grazie ad Apache Arrow puoi passare da un formato all'altro senza copie: leggi Parquet con DuckDB, converti in Polars con .pl() per manipolazioni lazy, poi torna a pandas con .to_pandas() per addestrare un modello scikit-learn. È il pattern più diffuso nei notebook Jupyter del 2026.

Come si installa l'estensione httpfs per leggere S3?

Basta eseguire INSTALL httpfs; LOAD httpfs; una volta per connessione. L'installazione è persistente sul filesystem locale, ma il LOAD va ripetuto a ogni nuova connessione. Per le credenziali AWS usa il nuovo comando CREATE SECRET introdotto in DuckDB 1.x, molto più sicuro delle vecchie variabili di sessione.

Editorial Team
Sull'Autore Editorial Team

Our team of expert writers and editors.