En bref : pointez DuckDB vers le fichier.
duckdb -c "SELECT region, SUM(amount) FROM 'sales.csv' GROUP BY region"
Pas de schéma, pas d'import, pas de serveur, pas de CREATE TABLE. Le CSV reste exactement où il est, et la ligne d'en-tête devient les noms de colonnes. Si vous n'installez qu'un seul outil aujourd'hui, installez celui-là.
Le reste de cette page explique pourquoi ça marche, que faire quand DuckDB n'est pas disponible, et comment gérer les deux pièges habituels : les guillemets autour d'un nom de fichier, et les colonnes dont le nom contient des espaces.
Pourquoi il n'y a pas besoin de charger le fichier quelque part
Le réflexe de « d'abord mettre les données dans une base » date de l'époque où les moteurs SQL ne savaient lire que leur propre stockage. Les moteurs modernes n'ont plus cette limite. DuckDB, SQLite et plusieurs petits outils en ligne de commande traitent un CSV comme une table le temps d'une requête, déduisent les types en échantillonnant le fichier, répondent, puis oublient tout.
Cela change complètement le calcul. Une question ponctuelle sur un fichier reçu par e-mail ne justifie plus un schéma, un script de chargement et une étape de nettoyage. Elle justifie une ligne.
Charger les données dans une vraie base reste la bonne option dans exactement deux cas : les données seront interrogées à répétition par plusieurs personnes, ou elles ont besoin de contraintes et de relations qu'un simple fichier plat ne peut pas exprimer. Ni l'un ni l'autre ne décrit « quel est le total par région dans cet export ».
Les options, honnêtement
| Outil | Installation | Idéal pour | Faible sur |
|---|---|---|---|
| DuckDB | Un seul binaire, aucune dépendance | Tout jusqu'à plusieurs dizaines de Go | Presque rien pour cette tâche |
SQLite + .import | Déjà présent sur macOS et Linux | Garder la table pour plus tard | Les types : tout arrive en TEXT |
q | pip install q-text-as-data | Des one-liners rapides dans un pipeline shell | Les gros fichiers ; il est plus lent |
csvsql (csvkit) | pip install csvkit | Générer un CREATE TABLE pour une vraie base | La vitesse : le plus lent d'ici |
Pandas + pandasql | Vous avez déjà pandas | Quand le résultat alimente d'autre code Python | Le dialecte SQL est celui de SQLite, pas de Postgres |
| Appli mobile avec SQL | App Store / Play | Le fichier est arrivé par e-mail, sur votre téléphone | Tout ce que vous voudriez automatiser |
Le classement honnête est court : DuckDB pour presque tout, SQLite quand vous ne pouvez rien installer, et une appli mobile quand vous n'êtes pas devant un ordinateur. Les autres sont corrects et ont leurs moments, mais aucun ne bat DuckDB sur ce travail précis.
DuckDB, en détail
L'installation tient en un seul binaire : brew install duckdb, ou téléchargez-le et placez-le dans votre path. Ensuite :
duckdb
D SELECT * FROM 'orders.csv' LIMIT 10;
Deux choses à savoir tout de suite :
Le nom du fichier demande des guillemets simples. FROM orders.csv est interprété comme un schéma orders et une table csv, et le message d'erreur ne le dit pas clairement. FROM 'orders.csv' est la bonne forme.
Les noms de colonnes avec espaces demandent des guillemets doubles. SELECT "Order Date" FROM …. Avec des guillemets simples, vous obtenez la chaîne littérale, en silence, pour chaque ligne, ce qui donne l'impression que la requête a fonctionné.
Utilisez un glob pour tout un dossier quand l'export est arrivé en morceaux mensuels :
SELECT * FROM 'exports/*.csv'
Quand la détection des types se trompe, en général sur une colonne d'identifiants avec des zéros en tête ou sur des dates dans un format qu'elle ne reconnaît pas, prenez la main sur la lecture plutôt que de lutter contre elle :
SELECT * FROM read_csv('orders.csv',
header = true,
columns = {'id': 'VARCHAR', 'ordered_at': 'DATE', 'amount': 'DOUBLE'},
dateformat = '%d/%m/%Y'
)
read_csv accepte aussi delim, quote, escape, ignore_errors = true et sample_size = -1 (analyser tout le fichier avant de choisir les types, au lieu des 20 480 premières lignes). Ce dernier est la solution quand une colonne paraît numérique pendant dix mille lignes puis tombe sur N/A.
Réécrire le résultat sans quitter SQL :
COPY (SELECT * FROM 'orders.csv' WHERE amount > 1000)
TO 'large-orders.csv' (HEADER, DELIMITER ',');
Et si le fichier doit être interrogé plus de quelques fois, convertissez-le une fois avec COPY (SELECT * FROM 'orders.csv') TO 'orders.parquet' et interrogez le Parquet à la place. Il est typé, compressé, et généralement de cinq à vingt fois plus rapide à parcourir.
SQLite, quand vous ne pouvez rien installer
sqlite3 est livré avec macOS et la plupart des distributions Linux : c'est donc l'option qui marche sur une machine verrouillée.
sqlite3 shop.db
sqlite> .mode csv
sqlite> .import orders.csv orders
sqlite> SELECT region, COUNT(*) FROM orders GROUP BY region;
Le piège, et il compte : .import met toutes les colonnes en TEXT. Le tri devient alors lexicographique, donc 100 passe avant 20, et SUM() réserve des surprises. Faites un cast au moment de la requête (CAST(amount AS REAL)), ou créez la table avec de vrais types avant d'importer.
Contrairement à DuckDB, cela laisse un shop.db derrière lui : c'est voulu si vous voulez retrouver la table demain, et du désordre sinon.
q, pour un pipeline shell
Quand la requête n'est qu'une étape dans une chaîne de commandes shell, q lit l'entrée standard comme n'importe quel outil Unix :
q -H -d, "SELECT region, SUM(amount) FROM - GROUP BY region" < orders.csv
-H indique que la première ligne est un en-tête, -d, fixe le séparateur, et - désigne stdin. C'est du SQLite en dessous, donc le dialecte est celui de SQLite. C'est aussi l'option raisonnable la plus lente sur un gros fichier : correct pour quelques centaines de milliers de lignes, pénible au-delà.
Quand le fichier est sur votre téléphone
Rien de tout cela n'aide quand l'export est une pièce jointe dans votre messagerie et que vous n'êtes pas à un bureau. Ça arrive souvent, et la réponse habituelle, « envoyez-le-vous et regardez plus tard », est mauvaise.
Exécute du vrai SQL sur le fichier, sur iOS et Android, avec un générateur de filtres visuel pour quand vous préférez ne pas taper une requête sur un clavier de téléphone, et un assistant en langage courant pour quand vous ne connaissez pas encore les noms des colonnes.
Ce que ça faitLe dialecte est celui de SQLite : les requêtes de cet article se transposent donc avec les réserves habituelles. Pas de fonctions de fenêtrage comme dans les exemples DuckDB, et les mêmes habitudes de CAST s'appliquent.
Des requêtes à garder sous la main
Ce que contient ce fichier :
SELECT * FROM 'file.csv' LIMIT 20;
DESCRIBE SELECT * FROM 'file.csv';
SELECT COUNT(*) FROM 'file.csv';
Où sont les valeurs nulles, colonne par colonne étant fastidieux, donc :
SELECT COUNT(*) - COUNT(email) AS missing_email,
COUNT(*) - COUNT(phone) AS missing_phone
FROM 'contacts.csv';
Doublons sur une clé :
SELECT id, COUNT(*) AS n
FROM 'orders.csv'
GROUP BY id HAVING n > 1
ORDER BY n DESC;
Joindre deux exports est la requête qui justifie le plus souvent tout l'exercice, car le faire avec RECHERCHEV entre deux feuilles est bien pire :
SELECT o.id, o.amount, c.name
FROM 'orders.csv' o
JOIN 'customers.csv' c ON c.id = o.customer_id;
Un total mensuel à partir d'une colonne de dates :
SELECT date_trunc('month', ordered_at) AS month, SUM(amount)
FROM 'orders.csv'
GROUP BY month ORDER BY month;
Quand ça se passe mal
Tout est revenu dans une seule colonne. Le séparateur n'est pas une virgule ; le point-virgule est la norme dans une bonne partie de l'Europe. read_csv('f.csv', delim = ';'). Si le fichier est plus étrange que ça, il est peut-être réellement corrompu et pas seulement inhabituel.
Une colonne numérique ne se somme pas. Elle a été déduite comme du texte, parce qu'une valeur non numérique s'y trouve. Trouvez le coupable avec SELECT amount FROM 'f.csv' WHERE TRY_CAST(amount AS DOUBLE) IS NULL LIMIT 10;. C'est généralement un symbole monétaire, un séparateur de milliers ou la chaîne N/A.
Les zéros en tête ont disparu. La colonne a été lue comme un nombre. Déclarez-la VARCHAR dans columns, et ne laissez aucun tableur s'en approcher ensuite.
La ligne d'en-tête est répétée dans tout le fichier. Plusieurs exports ont été concaténés. WHERE id <> 'id' est la solution brutale, et généralement la bonne.
Mémoire insuffisante. DuckDB travaille en flux, donc cela vient normalement d'un ORDER BY ou d'un DISTINCT sur tout le fichier. Donnez-lui un répertoire temporaire (SET temp_directory = '/tmp/duckdb') et il écrira sur le disque. Ou lisez comment ouvrir un fichier CSV de 1 Go pour la question plus large de ce qui tient à quelle échelle.