Tu as un fichier CSV de plusieurs millions de lignes, et pandas commence à tousser. Tu aimerais l'interroger en SQL, sans installer un serveur PostgreSQL, sans écrire de schéma, sans attendre qu'un ETL daigne se terminer. C'est exactement le trou que DuckDB vient combler : une base de données analytique qui tient dans une bibliothèque Python, démarre en une milliseconde, et avale tes fichiers CSV, Parquet ou JSON comme s'ils étaient des tables natives.
DuckDB se surnomme lui-même le SQLite de l'analytique : là où SQLite optimise les transactions, DuckDB optimise les agrégations massives. C'est un moteur colonnaire, embarqué, sans serveur, pensé pour la charge de travail analytique (OLAP) plutôt que transactionnelle. Le résultat : des requêtes sur des gigaoctets de données qui s'exécutent en mémoire, directement depuis ton script.
Dans ce tutoriel, tu vas connecter DuckDB à Python, créer des tables, ingérer des CSV et des Parquet, interroger des DataFrames pandas sans copie, écrire des requêtes analytiques avec fenêtres et jointures, exporter tes résultats, et persister le tout dans un fichier. À la fin, tu auras remplacé une bonne partie de tes scripts pandas par du SQL propre et rapide.
Prérequis
DuckDB est un paquet autonome : pas de serveur à installer, pas de dépendance système. Il te faut Python 3.7 ou plus récent, et c'est tout. Le paquet embarque le moteur complet, compilé pour ta plateforme :
pip install duckdbVérifie l'installation en affichant la version. Les exemples de ce tutoriel fonctionnent avec la série 1.x, et l'API présentée est stable depuis la 1.0. Optionnellement, installe aussi pandas, car DuckDB s'intègre remarquablement bien avec les DataFrames :
pip install pandasCe qui rend DuckDB différent
Avant d'écrire du code, clarifions pourquoi cet outil change la donne face à des réflexes comme pandas ou un PostgreSQL classique. DuckDB coche trois cases rarement réunies :
- Embarqué : le moteur vit dans ton processus Python. Pas de serveur à lancer, pas de port à ouvrir, pas de mot de passe. Une seule bibliothèque suffit.
- Colonnaire : les données sont stockées colonne par colonne, ce qui rend les agrégations (somme, moyenne, comptage) et les filtres extrêmement rapides, même sur des millions de lignes.
- Zéro-copie avec l'écosystème : DuckDB lit un DataFrame pandas ou une table Arrow directement, sans conversion ni copie, en exploitant le format Apache Arrow comme colonne vertébrale.
Concrètement, tu peux écrire du SQL sur un DataFrame pandas comme s'il s'agissait d'une table, ou interroger directement un fichier Parquet posé sur ton disque sans aucune étape d'import. C'est cette fluidité qui fait gagner un temps considérable au quotidien. Les usages les plus fréquents :
- Analyser un gros CSV ou Parquet sans le charger dans pandas
- Faire des agrégations et des fenêtres sur des millions de lignes en une fraction de seconde
- Joindre plusieurs fichiers (CSV, Parquet, JSON) dans une seule requête
- Explorer rapidement un jeu de données en SQL depuis un notebook
- Préparer un export propre à destination d'un autre outil ou d'un collègue
Première connexion et première requête
La brique de base est la connexion. Par défaut, elle est en mémoire : tout est volatile et disparaît à la fin du script, parfait pour l'exploration. La fonction execute renvoie un curseur dont la méthode fetchall récupère les lignes :
import duckdb
# Connexion en memoire (volatile)
con = duckdb.connect()
# Une premiere requete : le resultat est une liste de tuples
resultat = con.execute("SELECT 42 AS reponse, 'duckdb' AS moteur").fetchall()
print(resultat) # [(42, 'duckdb')]Pour voir le résultat sous forme de tableau bien présenté, la méthode show fait tout le travail de mise en forme. Elle est idéale en exploration interactive :
import duckdb
con = duckdb.connect()
con.execute(
"SELECT 1 AS id, 'Alice' AS nom, 128.5 AS score "
"UNION ALL SELECT 2, 'Bob', 95.0"
).show()Requêtes directes avec duckdb.sql()
Plutôt que de gérer une connexion à la main, tu peux utiliser le module duckdb directement. La fonction sql renvoie un objet Relation, une vue paresseuse que tu peux afficher, enchaîner ou convertir. C'est le point d'entrée le plus rapide pour une exploration :
import duckdb
# Requete directe sur le module (connexion implicite en memoire)
duckdb.sql("SELECT 'bonjour' AS salutation, 2 + 3 AS somme").show()
# Le resultat est une Relation : on peut la convertir en DataFrame pandas
df = duckdb.sql("SELECT range(5) AS n").df()
print(df)La notion de Relation est centrale : elle ne matérialise rien tant qu'on ne le demande pas. Tu peux chaîner des opérations, et DuckDB optimise l'ensemble avant d'exécuter. C'est la même idée que les DataFrames paresseux de Polars ou de Spark, mais sans configuration.
Créer une table et insérer des données
Pour des données de petite taille ou des cas de test, créer une table et insérer des lignes est immédiat. La syntaxe est du SQL standard, avec les types habituels :
import duckdb
con = duckdb.connect()
con.execute(
"CREATE TABLE ventes ("
" id INTEGER, produit VARCHAR, montant DOUBLE, date DATE)"
)
con.execute(
"INSERT INTO ventes VALUES "
"(1, 'Livre', 12.5, '2024-01-15'),"
"(2, 'Casque', 89.0, '2024-01-16'),"
"(3, 'Livre', 12.5, '2024-02-01'),"
"(4, 'Clavier', 45.0, '2024-02-03')"
)
con.execute("SELECT * FROM ventes").show()La méthode fetchdf est un raccourci précieux : elle renvoie directement le résultat d'une requête sous forme de DataFrame pandas, sans passer par fetchall. Tu restes dans l'univers SQL et tu récupères un objet pandas prêt pour la suite :
import duckdb
con = duckdb.connect()
df = con.execute("SELECT * FROM ventes WHERE montant > 20").fetchdf()
print(df)Lire un CSV sans effort
Le vrai terrain de jeu de DuckDB, c'est l'ingestion de fichiers. Pour un CSV, deux approches coexistent : la lecture directe dans une requête SQL, ou la fonction read_csv qui renvoie une Relation. Dans les deux cas, le schéma est inféré automatiquement, et l'en-tête est détecté :
import duckdb
# 1. Requete SQL directe sur le fichier, sans import prealable
duckdb.sql("SELECT * FROM 'ventes.csv' LIMIT 5").show()
# 2. Via read_csv : on recupere une Relation reutilisable
rel = duckdb.read_csv("ventes.csv")
print(rel.dtypes) # schema infere automatiquementSi ton fichier a un séparateur inhabituel, un en-tête absent ou un encodage précis, les paramètres de read_csv te laissent tout piloter : delim, header, columns, encoding, et bien d'autres. DuckDB lit aussi directement des CSV compressés, ce qui est pratique pour les gros jeux de données téléchargés.
import duckdb
# CSV avec point-virgule et sans en-tete
rel = duckdb.read_csv(
"donnees.psv",
delim=";",
header=True,
)
rel.show()Lire un Parquet comme une table native
Le format Parquet est le compagnon naturel de DuckDB : colonnaire, compressé, il se lit à des vitesses impressionnantes, et DuckDB ne charge que les colonnes réellement utilisées par la requête. Interroger un Parquet se fait exactement comme un CSV :
import duckdb
# Filtre + agregation directement sur le fichier Parquet
duckdb.sql(
"SELECT produit, SUM(montant) AS total "
"FROM 'ventes.parquet' "
"WHERE date >= '2024-02-01' "
"GROUP BY produit ORDER BY total DESC"
).show()Ce comportement de projection poussée est crucial : sur un fichier de plusieurs gigaoctets, une requête qui n'utilise que deux colonnes ne décompresse que ces deux colonnes. C'est la raison pour laquelle DuckDB surclasse souvent pandas sur les grosses lectures, qui lui charge tout en mémoire par défaut.
Lire et interroger du JSON
Le JSON est partout : réponses d'API, logs d'applications, exports NoSQL. DuckDB le lit nativement et sait même descendre dans les champs imbriqués grâce à son support des types structurés. Pour un fichier JSON simple, la lecture est identique à un CSV :
import duckdb
# Lecture directe d'un fichier JSON
duckdb.sql("SELECT * FROM 'evenements.json' LIMIT 5").show()
# Acces a un champ imbrique avec la syntaxe -> ou point
duckdb.sql(
"SELECT payload.user.name AS nom, payload.total AS total "
"FROM 'evenements.json'"
).show()Pour des JSON massifs, l'option format='auto' ou des paramètres de parsing dédiés t'aident à gérer les schémas irréguliers. C'est souvent plus rapide que de charger tout dans pandas puis d'aplatir à la main.
Lire des fichiers distants (HTTP et S3)
Avec l'extension httpfs, DuckDB lit directement un fichier hébergé sur le web ou sur un bucket S3, sans le télécharger au préalable. C'est idéal pour analyser un jeu de données public ou un export stocké dans le cloud :
import duckdb
# Active l'extension de lecture de fichiers distants
duckdb.sql("INSTALL httpfs; LOAD httpfs;")
# Requete directe sur un Parquet heberge en ligne
duckdb.sql(
"SELECT COUNT(*) AS nb_lignes "
"FROM 'https://example.com/donnees/mes_ventes.parquet'"
).show()
# S3 : on configure la region et les identifiants si necessaire
duckdb.sql("SET s3_region='eu-west-1';")
duckdb.sql("SELECT * FROM 's3://mon-bucket/export.parquet' LIMIT 5;").show()Cette capacité transforme DuckDB en outil d'analyse de données cloud à coût quasi nul : pas de serveur, pas de pipeline, juste une requête SQL pointée vers un fichier distant.
Interroger un DataFrame pandas sans copie
Si tes données sont déjà dans pandas ou Arrow, inutile de les recopier dans DuckDB. Une table pandas ou Arrow enregistrée est lisible directement en SQL, sans conversion ni copie grâce au format Arrow :
import duckdb
import pandas as pd
df = pd.DataFrame({
"produit": ["A", "B", "C", "A"],
"prix": [10.0, 20.0, 30.0, 15.0],
})
# Le DataFrame est interroge directement, comme une table
duckdb.sql(
"SELECT produit, SUM(prix) AS total, COUNT(*) AS nb "
"FROM df GROUP BY produit"
).show()Le sens inverse fonctionne aussi : toute Relation DuckDB se convertit en DataFrame pandas via .df(), en table Arrow via .arrow(), ou en Polars via .pl(). Tu peux ainsi garder DuckDB pour les calculs lourds et pandas pour la suite de ton pipeline, sans friction.
Agrégations et fonctions de fenêtrage
C'est ici que DuckDB brille. Les fonctions de fenêtrage (window functions) permettent des calculs analytiques qui seraient laborieux en pandas : classement, moyennes glissantes, cumuls. Voici un exemple qui combine agrégation et classement :
import duckdb
duckdb.sql(
"""
SELECT produit,
COUNT(*) AS nb_ventes,
SUM(montant) AS total,
RANK() OVER (ORDER BY SUM(montant) DESC) AS rang
FROM 'ventes.csv'
GROUP BY produit
ORDER BY total DESC
"""
).show()La même requête exprime aussi bien une moyenne mobile ou un total cumulé : remplace RANK() par AVG(montant) OVER (PARTITION BY produit ORDER BY date) pour une moyenne glissante, ou par SUM(montant) OVER (ORDER BY date) pour un cumul. La syntaxe SQL standard fait gagner en lisibilité ce qu'on perdait en verbosité pandas.
Jointures entre fichiers
DuckDB n'exige pas que tes données vivent dans une seule table. Tu peux joindre deux fichiers directement, sans les charger au préalable. C'est l'équivalent d'un JOIN en base de données, mais sur des fichiers plats :
import duckdb
duckdb.sql(
"""
SELECT c.nom, v.produit, v.montant
FROM 'clients.csv' AS c
JOIN 'ventes.csv' AS v ON c.id = v.client_id
WHERE v.montant > 30
ORDER BY v.montant DESC
"""
).show()Tu peux enchaîner plusieurs jointures, utiliser des sous-requêtes, ou mélanger des fichiers CSV et Parquet dans la même requête. C'est la flexibilité d'un moteur SQL complet, appliquée à des données qui n'ont jamais quitté leur fichier.
Exporter les résultats
Après calcul, il faut sortir les données. La commande COPY écrit le résultat d'une requête dans un fichier, dans le format de ton choix. Tu peux aussi utiliser une Relation pour un export plus lisible :
import duckdb
# Export direct en Parquet via COPY
duckdb.sql(
"COPY (SELECT * FROM 'ventes.csv' WHERE montant > 20) "
"TO 'resultat.parquet' (FORMAT PARQUET)"
)
# Ou depuis une Relation, vers CSV
rel = duckdb.sql("SELECT produit, COUNT(*) AS nb FROM 'ventes.csv' GROUP BY produit")
rel.write_csv("top_produits.csv")Les formats supportés couvrent l'essentiel : Parquet, CSV, JSON, et même des exports vers PostgreSQL ou d'autres bases via les extensions. Pour un pipeline de données, cette capacité à lire d'un format et écrire dans un autre en une commande est un gain de temps énorme.
Persister dans un fichier .duckdb
Jusqu'ici, tout était en mémoire. Pour garder tes tables entre deux exécutions, donne un chemin de fichier à la connexion : DuckDB crée une base persistante au format .duckdb, que tu peux rouvrir plus tard ou partager :
import duckdb
# Ouvre (ou cree) un fichier de base persistante
con = duckdb.connect("analyses.duckdb")
con.execute("CREATE TABLE IF NOT EXISTS logs (ts TIMESTAMP, message VARCHAR)")
con.execute("INSERT INTO logs VALUES (now(), 'demarrage du script')")
con.execute("SELECT * FROM logs").show()
con.close()
# Plus tard, dans un autre script
con = duckdb.connect("analyses.duckdb")
con.execute("SELECT COUNT(*) AS nb_logs FROM logs").show()Le fichier .duckdb est auto-contenu et portable : tu peux le copier, le versionner, ou le lire en lecture seule avec read_only=True. C'est un excellent format d'échange pour partager un jeu de données préparé avec un collègue, sans lui imposer un serveur. À noter : le format évolue au fil des versions majeures, donc pense à recréer ou exporter tes bases importantes lors d'une montée de version.
Fonctions Python personnalisées (UDF)
Quand le SQL ne suffit pas, tu peux enregistrer tes propres fonctions Python et les appeler directement dans une requête. C'est le pont entre la logique métier écrite en Python et la puissance d'exécution de DuckDB :
import duckdb
con = duckdb.connect()
# Une fonction Python simple, exposee a SQL
def doubler(x):
return x * 2
con.create_function("doubler", doubler, [duckdb.typing.DOUBLE], duckdb.typing.DOUBLE)
print(con.execute("SELECT doubler(21)").fetchall()) # [(42.0,)]Les UDF sont utiles pour réutiliser une transformation existante sans la réécrire en SQL, mais garde en tête qu'une fonction Python pure sera moins rapide qu'une fonction native DuckDB. Réserve-les aux cas ponctuels, et privilégie le SQL pour les opérations de masse.
Bonnes pratiques et limites
DuckDB est redoutable, mais ce n'est pas une baguette magique. Quelques repères pour l'utiliser à bon escient :
- Choisis le bon outil : DuckDB excelle en analytique mono-machine. Pour des dizaines d'utilisateurs concurrents ou des transactions, oriente-toi vers PostgreSQL.
- Garde la donnée au repos : interroge les fichiers directement tant que possible, plutôt que de tout charger en mémoire.
- Exploite le Parquet : c'est le format le plus rapide pour DuckDB, et il se compresse remarquablement.
- Attention à la mémoire : une agrégation massive en mémoire peut consommer beaucoup de RAM. Surveille tes requêtes les plus lourdes.
- Versionne ton schéma : comme toute base, une table persistante évolue ; documente tes migrations même en usage personnel.
Un dernier conseil : DuckDB propose un vaste écosystème d'extensions (lecture de fichiers distants HTTP/S3, géospatial, index de recherche plein texte). Si un besoin te semble manquer, il y a de bonnes chances qu'une extension le couvre déjà.
Conclusion
Tu as maintenant un moteur SQL analytique complet dans ton script Python : requêtes directes sur fichiers, ingestion CSV et Parquet, intégration pandas sans copie, fenêtres analytiques, jointures, exports, persistance et fonctions personnalisées. Le tout sans serveur, sans configuration, en quelques lignes.
Pour aller plus loin, explore les extensions (notamment la lecture de fichiers distants et le géospatial), branche DuckDB sur un dashboard, ou remplace un pipeline pandas entier par du SQL lisible. Tu découvriras vite que beaucoup de tâches qui semblaient exiger une vraie base de données tiennent désormais dans un fichier.
Le point fort de DuckDB, c'est qu'il ne t'oblige à rien : tu peux l'utiliser pour une requête unique en exploration, ou en faire le cœur d'un pipeline entier. Il se glisse dans ton script sans le réorganiser, et c'est précisément ce qui en fait un outil aussi addictif une fois qu'on y a goûté.
Pour aller plus loin
La documentation officielle et la référence de l'API Python : le point de départ pour creuser chaque fonction.
La page Pourquoi DuckDB détaille la philosophie du moteur et ses cas d'usage.






