DuckDB: SQL sobre els teus CSV i Excel sense muntar cap base de dades
Consultar fitxers directament amb SQL, creuar-los, resumir-los i exportar-los. Sense servidor, sense importar res i amb fitxers més grans que la RAM.
·8 min de lectura
Quan un fitxer creix, arriba un punt on el full de càlcul deixa de servir: 800.000 files no s’obren, un VLOOKUP contra una altra taula triga minuts, i creuar tres exportacions diferents és mitja tarda de feina manual.
L’instint és muntar una base de dades. Instal·lar Postgres, crear l’esquema, escriure el COPY, i quan tot funciona resulta que era per a una consulta que faràs tres vegades.
DuckDB elimina aquest pas: és una base de dades analítica que corre dins del teu procés, sense servidor, i que sap llegir un CSV, un Parquet o un Excel com si fos una taula, sense importar-lo enlloc.
Instal·lar-lo
pip install duckdb # per fer-lo servir des de Python
brew install duckdb # l'intèrpret de línia d'ordres
També hi ha binari per a Windows i Linux, i és un sol fitxer executable sense dependències.
La primera consulta
Obre l’intèrpret i pregunta a un fitxer directament:
SELECT * FROM 'vendes.csv' LIMIT 10;
Això és tot. No hi ha CREATE TABLE, no hi ha importació, no hi ha esquema. DuckDB obre el fitxer, en dedueix el delimitador, la capçalera i els tipus, i respon.
I quan la deducció no és correcta —que passa—, es diu explícitament:
SELECT * FROM read_csv('vendes.csv',
delim = ';',
header = true,
dateformat = '%d/%m/%Y',
types = {'codi_client': 'VARCHAR', 'import': 'DOUBLE'}
);
Aquest types amb VARCHAR és el mateix consell de sempre: un codi amb zeros al davant s’ha de llegir com a text o els perds.
Si el fitxer té files malformades i vols continuar igualment:
SELECT * FROM read_csv('vendes.csv', ignore_errors = true, store_rejects = true);
SELECT * FROM reject_errors; -- què s'ha descartat i per què
Aquesta segona taula és or: et diu exactament quines línies estaven trencades, en comptes de fallar amb un error genèric o —pitjor— saltar-se-les en silenci.
Conèixer un fitxer en dues ordres
DESCRIBE SELECT * FROM 'vendes.csv';
Torna les columnes i els tipus deduïts.
SUMMARIZE SELECT * FROM 'vendes.csv';
I aquesta és la joia: per a cada columna et dona el mínim, el màxim, la mitjana, la desviació, els quartils, el nombre aproximat de valors únics i el percentatge de nuls. És una auditoria bàsica en una línia i sobre el fitxer sencer, no sobre una mostra.
Llegir molts fitxers alhora
Aquest és el cas que justifica DuckDB tot sol. Tens una carpeta amb dotze exportacions mensuals:
SELECT * FROM read_csv('exportacions/2026-*.csv',
union_by_name = true,
filename = true
);
union_by_name = trueuneix els fitxers per nom de columna, no per posició. Si el volcat d’agost va afegir una columna al mig, no es desquadra res: les files que no la tenen surten amb nul.filename = trueafegeix una columna amb el nom del fitxer d’origen. Quan trobis una fila estranya, sabràs de quin volcat ve.
Fer això a mà amb fulls de càlcul és una tarda. Aquí són dues línies.
Excel
Cal l’extensió, que s’instal·la un sol cop:
INSTALL excel;
LOAD excel;
SELECT * FROM read_xlsx('informe.xlsx', sheet = 'Dades', header = true);
Necessita una versió raonablement recent de DuckDB; en versions antigues això es feia amb l’extensió spatial, que era molt menys còmode.
Creuar dues fonts
Aquí és on es guanya de veritat. Un CSV del CRM, un Excel de comptabilitat, i la pregunta que ningú pot respondre:
INSTALL excel; LOAD excel;
SELECT
c.client_id,
c.nom,
count(f.factura_id) AS factures,
sum(f.import) AS total
FROM 'crm/clients.csv' AS c
LEFT JOIN read_xlsx('comptabilitat/factures.xlsx') AS f
ON f.client_id = c.client_id
GROUP BY c.client_id, c.nom
HAVING sum(f.import) IS NULL OR sum(f.import) = 0
ORDER BY c.nom;
«Clients al CRM que no han facturat res.» Dos formats diferents, dues carpetes diferents, cap importació, i la resposta en segons.
I la comprovació inversa, la dels registres orfes, que és la que sempre sorprèn:
SELECT count(*) AS factures_sense_client
FROM read_xlsx('comptabilitat/factures.xlsx') AS f
WHERE NOT EXISTS (
SELECT 1 FROM 'crm/clients.csv' c WHERE c.client_id = f.client_id
);
Buscar duplicats i validar claus
La consulta que hauries de fer a qualsevol fitxer nou:
SELECT nif, count(*) AS vegades
FROM 'clients.csv'
GROUP BY nif
HAVING count(*) > 1
ORDER BY vegades DESC;
I per quedar-te amb un sol registre per clau —el més recent—, DuckDB té QUALIFY, que estalvia la subconsulta que caldria en SQL estàndard:
SELECT *
FROM 'moviments.csv'
QUALIFY row_number() OVER (PARTITION BY client_id ORDER BY data DESC) = 1;
Dues comoditats de sintaxi que fan servei
DuckDB estén SQL amb coses petites que estalvien molt teclat:
-- Totes les columnes menys dues
SELECT * EXCLUDE (observacions, camp_intern) FROM 'clients.csv';
-- Totes les columnes, però transformant-ne una
SELECT * REPLACE (upper(trim(provincia)) AS provincia) FROM 'clients.csv';
-- Agrupar per tot el que no és una agregació
SELECT provincia, estat, count(*) FROM 'clients.csv' GROUP BY ALL;
Exportar el resultat
COPY (
SELECT * FROM 'vendes.csv' WHERE import > 1000
) TO 'sortida/grans.csv' (HEADER, DELIMITER ',');
I si el resultat l’has de tornar a consultar sovint, val la pena desar-lo en Parquet: ocupa una fracció, conserva els tipus i es llegeix molt més ràpid.
COPY (SELECT * FROM read_csv('exportacions/*.csv', union_by_name = true))
TO 'historic.parquet' (FORMAT PARQUET);
A partir d’aquí, SELECT * FROM 'historic.parquet' sobre milions de files respon a l’instant.
Des de Python, amb pandas
La integració amb pandas és el detall que el fa encaixar en qualsevol script existent:
import duckdb, pandas as pd
clients = pd.read_csv('clients.csv', dtype=str)
# El DataFrame es consulta pel nom de la variable, sense registrar res
resultat = duckdb.sql("""
SELECT provincia, count(*) AS n
FROM clients
GROUP BY provincia
ORDER BY n DESC
""").df()
Va en les dues direccions: consultes un DataFrame amb SQL i reps un DataFrame. Per a les agregacions i els creuaments, sol ser molt més llegible que encadenar groupby, merge i reset_index.
Per treballar amb una base persistent en comptes de fitxers solts:
con = duckdb.connect('projecte.duckdb')
con.sql("CREATE TABLE clients AS SELECT * FROM 'clients.csv'")
Quan el fitxer no cap a la memòria
DuckDB processa per trossos i escriu a disc quan cal, així que pot consultar fitxers més grans que la RAM disponible. Es pot acotar:
SET memory_limit = '4GB';
SET threads = 4;
En Parquet, a més, només llegeix les columnes que la consulta demana i es salta blocs sencers que no poden contenir el que busques. Per això SELECT sum(import) FROM 'historic.parquet' sobre 50 milions de files acaba abans del que sembla raonable.
Quan DuckDB no és el que vols
Per honestedat: està fet per a analítica, per llegir molt i calcular. No és el lloc per a una aplicació amb molts usuaris escrivint alhora — allà vols Postgres. I no substitueix el full de càlcul per a la feina del dia a dia, on el que necessites és veure i tocar les cel·les.
El seu lloc és el mig: quan el full ja no arriba i muntar un servidor és desproporcionat. Que és, resulta, on viuen la majoria de fitxers d’una empresa.