DuckDB: Analisi Dati su File Locali Senza un Server
C'è una situazione che capita spesso a chi lavora con i dati: hai una cartella piena di CSV ed export Parquet, vuoi fare qualche analisi seria con SQL, ma non vuoi tirare su un server, configurare un database, gestire utenti e connessioni. Vuoi solo aprire un terminale o un notebook e interrogare quei file come se fossero tabelle.
DuckDB nasce esattamente per questo. È un database analitico embedded: vive dentro il tuo processo Python, dentro una CLI o dentro un'applicazione, e non ha bisogno di un demone in ascolto su una porta. È column-oriented, vettorizzato, e la sua specialità è leggere file direttamente dal disco senza importarli prima.
In questo articolo vediamo cosa fa DuckDB, come si usa nella pratica e soprattutto quando è la scelta giusta e quando invece ti conviene restare su altro.
Un database analitico dentro il tuo processo
La differenza fondamentale con SQLite è l'orientamento dei dati. SQLite è row-oriented e transazionale: ottimo per un'applicazione che legge e scrive singole righe, per un gestionale, per un'app mobile. DuckDB è column-oriented: legge e comprime colonne, quindi è veloce quando devi aggregare milioni di righe su poche colonne, che è il tipico pattern analitico.
Entrambi sono embedded, entrambi sono un file su disco (se vuoi persistere i dati), entrambi non hanno un server. Ma servono a due mondi diversi. Un modo semplice per ricordarlo:
| Caratteristica | SQLite | DuckDB |
|---|---|---|
| Orientamento | Per riga | Per colonna |
| Carico tipico | OLTP, molte scritture piccole | OLAP, poche query su molti dati |
| Concorrenza scrittura | Buona | Limitata (un writer) |
| Legge file esterni | No | Sì (CSV, Parquet, JSON) |
| Formato file | Proprietario, stabile | Proprietario, in evoluzione |
DuckDB è pensato per il singolo processo che analizza dati, non per dieci applicazioni che scrivono contemporaneamente. Se tieni a mente questo, la maggior parte delle sorprese sparisce.
Query direttamente sui file: CSV, Parquet, JSON
Il pezzo più interessante è la capacità di trattare un file come una tabella. Non serve CREATE TABLE né COPY: basta il percorso del file tra apici.
SELECT *
FROM 'vendite_2024.csv'
LIMIT 5;
DuckDB rileva automaticamente il separatore, il tipo delle colonne e l'eventuale riga di intestazione. Per i Parquet, che sono già tipizzati e compressi, il salto di prestazioni è netto:
SELECT mese, sum(importo) AS fatturato
FROM 'vendite/*.parquet'
GROUP BY mese
ORDER BY mese;
Il wildcard sui percorsi permette di leggere un'intera cartella in una sola query. Se i file sono organizzati con una struttura di cartelle tipo anno=2024/mese=03/, puoi attivare l'hive partitioning e le cartelle diventano colonne:
SELECT *
FROM read_parquet('vendite/**/*.parquet', hive_partitioning = true)
WHERE anno = 2024 AND mese = '03';
Questa è la modalità che uso più spesso: i dati restano dove sono, in un formato colonnare, e non c'è nessuna fase di caricamento da gestire.
Da Python, senza passare da pandas
In Python l'integrazione è immediata. Puoi aprire un database persistente su file oppure lavorare in memoria:
import duckdb
con = duckdb.connect("analisi.duckdb")
con.sql("""
SELECT negozio, sum(pezzi) AS pezzi_totali
FROM 'vendite/*.parquet'
GROUP BY negozio
ORDER BY pezzi_totali DESC
""").show()
Il risultato di .sql() è un oggetto relazionale lazy: non esegue nulla finché non chiedi il risultato. Questo permette di concatenare trasformazioni in stile pipeline, in parte in SQL e in parte in Python:
rel = con.sql("SELECT * FROM 'vendite/*.parquet'")
rel = rel.filter("importo > 0")
print(rel.aggregate("negozio, sum(importo) AS totale", "negozio").df())
Molto utile anche l'accesso diretto ai DataFrame: puoi interrogare una variabile pandas o Polars come se fosse una tabella, senza copiare i dati.
import pandas as pd
import duckdb
df = pd.read_csv("vendite.csv") # esempio
risultato = duckdb.sql("SELECT categoria, avg(prezzo) FROM df GROUP BY categoria").df()
Per analisi esplorative su file di medie dimensioni, questo elimina parecchio codice di contorno.
Un esempio concreto: vendite per negozio e categoria
Immaginiamo una tabella di esempio con poche righe, giusto per mostrare le query tipiche. I numeri sono inventati a scopo illustrativo.
CREATE TABLE vendite (
data DATE,
negozio VARCHAR,
categoria VARCHAR,
pezzi INT,
prezzo_unitario DECIMAL(10,2)
);
INSERT INTO vendite VALUES
('2024-03-01', 'Centro', 'Camicie', 12, 49.90),
('2024-03-01', 'Centro', 'Pantaloni', 7, 79.90),
('2024-03-02', 'Nord', 'Camicie', 9, 49.90),
('2024-03-02', 'Nord', 'Pantaloni', 11, 79.90),
('2024-03-03', 'Centro', 'Camicie', 15, 49.90);
Fatturato per categoria, con la quota sul totale calcolata con una window function:
SELECT
categoria,
sum(pezzi * prezzo_unitario) AS fatturato,
round(
100.0 * sum(pezzi * prezzo_unitario)
/ sum(sum(pezzi * prezzo_unitario)) OVER (),
1
) AS quota_percento
FROM vendite
GROUP BY categoria
ORDER BY fatturato DESC;
Classifica dei negozi per pezzi venduti, sempre nell'esempio:
SELECT
negozio,
sum(pezzi) AS pezzi_totali,
rank() OVER (ORDER BY sum(pezzi) DESC) AS posizione
FROM vendite
GROUP BY negozio
ORDER BY pezzi_totali DESC;
Le window function, le CTE, i GROUP BY con HAVING, sono tutte disponibili e si comportano come ti aspetti da un database analitico moderno.
DuckDB dentro le pipeline: Parquet come formato di scambio
Uno degli usi più pratici è trasformare dati grezzi in Parquet partizionato. È un passo intermedio che rende tutto il resto più veloce e più ordinato:
COPY (
SELECT
*,
strftime(data, '%Y-%m') AS mese
FROM 'raw/*.csv'
)
TO 'curated/vendite'
(FORMAT PARQUET, PARTITION_BY (mese), OVERWRITE_OR_IGNORE true);
Il risultato è una cartella con un sottolivello per mese. Da lì in poi puoi leggere solo le partizioni che ti servono, in DuckDB o in qualsiasi altro strumento che parla Parquet. Questo schema, chiamato a volte ELT leggero, è comodo quando i volumi non giustificano un cluster ma superano la comodità di un singolo CSV.
Un secondo pattern utile è l'unione di fonti diverse. Puoi leggere un Parquet dal disco e un CSV scaricato da un'API nella stessa query, facendo un JOIN tra i due:
SELECT p.negozio, p.fatturato, a.regione
FROM 'curated/vendite/*.parquet' p
JOIN 'anagrafiche/negozi.csv' a ON p.negozio = a.negozio;
Niente staging intermedio, niente tabelle temporanee da ripulire.
Confronto onesto: quando DuckDB è la scelta giusta
Vale la pena mettere le opzioni una accanto all'altra, senza forzature.
| Scenario | Strumento ragionevole |
|---|---|
| App con letture/scritture frequenti di singole righe | SQLite o PostgreSQL |
| Analisi esplorativa su file CSV/Parquet locali | DuckDB |
| Più utenti e più processi che scrivono insieme | PostgreSQL |
| Dataset da decine di GB con team e permessi | Data warehouse (BigQuery, Snowflake, ecc.) |
| Trasformazioni ricorrenti e testate in produzione | dbt su warehouse, o DuckDB in pipeline batch |
| Script una tantum su un export del gestionale | DuckDB |
La regola pratica: se il collo di bottiglia è leggere molto e scrivere poco, e il tuo carico è analitico, DuckDB è difficile da battere in semplicità. Se invece hai bisogno di transazioni concorrenti, utenti con permessi differenziati e un servizio sempre acceso, stai descrivendo un altro strumento.
Limiti e trappole da conoscere
Alcune cose le scopri solo usandolo, meglio saperle prima.
- Un solo writer: il processo che scrive sul file di database tiene un lock esclusivo. Due processi che scrivono in parallelo sullo stesso file non funzionano. Se devi condividere dati tra processi, pubblica Parquet e leggi da lì.
- Non è un database transazionale concorrente: non aspettarti il comportamento di PostgreSQL su isolamento e concorrenza.
- File su rete: leggere da NFS o da una cartella di rete può essere lento e, in casi limite, dare problemi di coerenza. Meglio avere i file in locale.
- Gestione della memoria: puoi limitarla e far scrivere su disco i risultati intermedi quando sono grandi.
SET memory_limit = '4GB';
SET threads = 4;
SET temp_directory = '/tmp/duckdb_tmp';
Queste tre righe risolvono la maggior parte dei problemi quando una query esplode o la macchina inizia a swappare.
- Formato del file: il formato di DuckDB evolve. Se apri lo stesso file con versioni molto diverse del motore puoi avere problemi. Per i dati "da scambiare", Parquet resta una scelta più sicura del file
.duckdb. - Non è un sostituto del warehouse: appena entrano in gioco governance, lineage, utenti multipli e orchestrazione seria, ti serve qualcosa di più. DuckDB resta utile come strumento da riga di comando accanto a quello.
Un'ultima nota: le estensioni. DuckDB ha un sistema di plugin che aggiunge funzionalità come la lettura diretta da HTTP o da object storage, tipi geografici e altre integrazioni. Si installano al volo con INSTALL e LOAD, e aprono scenari interessanti quando i file Parquet stanno su uno storage remoto.
Conclusione
DuckDB occupa uno spazio che prima era scomodo: analisi SQL veloce su dati che vivono in file, senza infrastruttura. Se lavori con export, CSV e Parquet, è uno di quegli strumenti che vale la pena provare subito, perché in cinque minuti hai già la prima query.
I punti chiave:
- È un database analitico embedded, column-oriented, pensato per query su molti dati e poche scritture.
- Legge CSV, Parquet e JSON direttamente dal disco, con wildcard e partizionamento hive.
- Si integra con Python, pandas e Polars in modo naturale, con un'API relazionale lazy.
- È ottimo per esplorazione, script una tantum e trasformazioni batch verso Parquet.
- Non è un sostituto di PostgreSQL per concorrenza e transazioni, né di un warehouse per team e governance.
- Attenzione a lock di scrittura, file su rete e limiti di memoria: tre righe di configurazione evitano parecchi mal di testa.
Se il tuo problema è "ho dei file e vorrei interrogarli con SQL", la risposta probabilmente non è tirare su un server. È aprire DuckDB e scrivere la query.
Foto di copertina di Myriam Jessier su Unsplash.