Torna al blog
DuckDB: Analisi Dati su File Locali Senza un Server

DuckDB: Analisi Dati su File Locali Senza un Server

9 min di lettura1 visualizzazioni
di Marco Salmaso
DuckDB è un database analitico embedded: legge CSV, Parquet e DataFrame direttamente dal disco, senza server né import. Vediamo quando conviene rispetto a SQLite, PostgreSQL o a un data warehouse, con esempi di query, partizionamento e limiti da conoscere.

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:

CaratteristicaSQLiteDuckDB
OrientamentoPer rigaPer colonna
Carico tipicoOLTP, molte scritture piccoleOLAP, poche query su molti dati
Concorrenza scritturaBuonaLimitata (un writer)
Legge file esterniNoSì (CSV, Parquet, JSON)
Formato fileProprietario, stabileProprietario, 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.

ScenarioStrumento ragionevole
App con letture/scritture frequenti di singole righeSQLite o PostgreSQL
Analisi esplorativa su file CSV/Parquet localiDuckDB
Più utenti e più processi che scrivono insiemePostgreSQL
Dataset da decine di GB con team e permessiData warehouse (BigQuery, Snowflake, ecc.)
Trasformazioni ricorrenti e testate in produzionedbt su warehouse, o DuckDB in pipeline batch
Script una tantum su un export del gestionaleDuckDB

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.