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 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:
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:
Criterio
DuckDB 1.x
Polars 1.x
pandas 2.x
Interfaccia principale
SQL standard
API DataFrame Rust-like
API DataFrame storica
Motore
Vettorizzato columnar
Vettorizzato Rust + Arrow
NumPy/Arrow, poco parallelo
Ideale per
Analisi ad-hoc, join complessi, data lake
Pipeline ETL, streaming lazy
Preparazione dati, ML dev
Query su file Parquet remoti
Nativa (httpfs)
Nativa (scan_parquet)
Solo via PyArrow/fsspec
Curva di apprendimento
Bassa se conosci SQL
Media
Bassa
Memoria oltre la RAM
Sì, spill su disco
Sì, in modalità streaming
No
Integrazione ML sklearn
Tramite .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.
Polars è la libreria DataFrame piu veloce per Python nel 2026: fino a 30x piu rapida di pandas. Guida pratica con Lazy API, streaming engine, window function, join paralleli e una strategia di migrazione da pandas testata in produzione.
Impara l'analisi statistica in Python con SciPy 1.17 e statsmodels 0.14: statistica descrittiva, t-test, ANOVA, chi-quadrato, correlazione, regressione lineare, test non parametrici e correzione per test multipli con esempi di codice pronti all'uso.