écrits/tutorial/2026/07
Tutorial18 juil. 2026·25 min

DuckLake et DuckDB : construire un data lakehouse moderne en pur SQL

Apprenez à construire un data lakehouse complet avec DuckLake et DuckDB — transactions ACID, time travel, évolution de schéma et catalogue PostgreSQL partagé — avec rien d'autre que du SQL et des fichiers Parquet.

Pendant longtemps, « data lakehouse » rimait avec infrastructure lourde : un cluster Spark, un service de catalogue Iceberg ou Delta Lake, un object store, et une petite équipe pour faire tourner l'ensemble. DuckLake change la donne. C'est un format lakehouse ouvert créé par l'équipe DuckDB : les données des tables sont stockées en fichiers Parquet classiques et — c'est là toute l'astuce — l'intégralité des métadonnées du catalogue vit dans une base de données SQL ordinaire, au lieu d'une nuée de fichiers manifestes JSON et Avro.

Résultat : vous obtenez des transactions ACID, des snapshots, le time travel et l'évolution de schéma au-dessus de formats de fichiers ouverts, et tout cela tourne sur votre ordinateur portable avec une seule instruction ATTACH. Le jour où vous avez besoin d'un accès multi-utilisateurs, vous remplacez la base de métadonnées par PostgreSQL et pointez le chemin des données vers S3. Même SQL, même workflow.

Dans ce tutoriel, vous allez construire un lakehouse opérationnel à partir de zéro : créer un catalogue DuckLake, charger des données de façon transactionnelle, remonter le temps grâce aux snapshots, faire évoluer votre schéma en toute sécurité, vous connecter depuis Python, puis faire passer l'installation à l'échelle d'une équipe avec un catalogue PostgreSQL et un stockage cloud.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • DuckDB 1.3 ou plus récent (l'extension ducklake est disponible à partir de la 1.3) — installation via brew install duckdb ou téléchargement sur duckdb.org
  • Python 3.10+ avec pip pour les étapes d'intégration Python
  • Des bases en SQL (CREATE TABLE, INSERT, SELECT, JOIN)
  • Optionnel pour l'étape 7 : une instance PostgreSQL et un bucket compatible S3 (AWS S3, Cloudflare R2 ou MinIO)

Vérifiez d'abord votre version de DuckDB :

duckdb --version
# v1.3.x ou plus récent

Ce que vous allez construire

Un lakehouse « local-first » pour un scénario d'analytique e-commerce :

  1. Un catalogue DuckLake adossé à un fichier de métadonnées DuckDB local
  2. Une table orders stockée en fichiers Parquet avec garanties ACID complètes
  3. Le time travel par snapshots — interroger la table telle qu'elle existait à n'importe quel instant
  4. L'évolution de schéma sans réécriture des données
  5. Un script d'analyse Python lisant le lakehouse avec pandas
  6. Une configuration d'équipe : catalogue PostgreSQL et chemin de données S3

Tout ce qui précède l'étape 7 s'exécute entièrement sur votre machine, sans aucun service externe.

Pourquoi DuckLake plutôt qu'Iceberg ou Delta Lake ?

Une minute de contexte avant de taper des commandes. Des formats comme Apache Iceberg et Delta Lake stockent les métadonnées des tables (schémas, snapshots, listes de fichiers, statistiques) sous forme de fichiers posés à côté des données : manifestes JSON, listes de manifestes Avro, journaux de transactions. Chaque requête commence par une chasse au trésor dans l'object store pour reconstituer l'état de la table, et chaque commit réécrit des fichiers de métadonnées avec des protocoles d'évitement de conflits délicats.

Le constat de DuckLake est simple : si un déploiement sérieux nécessite de toute façon une base de données catalogue (c'est pratiquement toujours le cas avec Iceberg), pourquoi ne pas y mettre toutes les métadonnées ? DuckLake stocke schémas, snapshots, listes de fichiers et statistiques de colonnes dans des tables SQL ordinaires. Les commits deviennent des transactions SQL. La résolution de conflits redevient ce que les bases de données savent faire depuis quarante ans.

Conséquences pratiques :

  • Moins d'allers-retours. La planification de requête interroge une base de données, pas des dizaines de petits fichiers sur S3.
  • De vraies transactions. Les transactions multi-tables et multi-instructions fonctionnent — un point faible notoire des formats basés fichiers.
  • Installation locale triviale. La base de métadonnées peut être un simple fichier DuckDB sur votre portable.
  • Données ouvertes. Vos lignes vivent dans du Parquet standard, lisible par n'importe quel moteur, pour toujours.

Étape 1 : installer DuckDB et l'extension DuckLake

Ouvrez un shell DuckDB et installez l'extension. Elle se télécharge une fois et reste en cache :

INSTALL ducklake;
LOAD ducklake;

C'est toute l'installation. L'extension embarque tout le nécessaire pour la gestion du catalogue, les snapshots et le suivi des fichiers Parquet.

Astuce : dans un script, LOAD ducklake est implicite dès que vous attachez un catalogue DuckLake — DuckDB charge automatiquement les extensions connues. L'écrire explicitement rend simplement vos scripts auto-documentés.

Étape 2 : créer votre premier catalogue DuckLake

Un catalogue DuckLake a besoin de deux emplacements : une base de métadonnées (où vit l'état des tables) et un chemin de données (où sont écrits les fichiers Parquet). Créez un répertoire de travail et attachez :

mkdir -p ~/lakehouse && cd ~/lakehouse
duckdb
ATTACH 'ducklake:metadata.ducklake' AS lake (DATA_PATH 'data/');
USE lake;

Ce qui vient de se passer :

  • metadata.ducklake est un fichier de base DuckDB contenant le catalogue : schémas, snapshots, listes de fichiers, statistiques.
  • data/ est le répertoire où seront écrits tous les fichiers Parquet de toutes les tables.
  • USE lake fait du lakehouse votre catalogue par défaut : les noms de tables non qualifiés s'y résolvent.

Lancez ls dans un autre terminal — vous verrez metadata.ducklake créé immédiatement, et data/ se remplira dès la première insertion.

Étape 3 : charger des données et exécuter des transactions ACID

Créez une table et insérez des données exactement comme dans n'importe quelle base :

CREATE TABLE orders (
  order_id   INTEGER,
  customer   VARCHAR,
  amount     DECIMAL(10, 2),
  country    VARCHAR,
  ordered_at TIMESTAMP
);
 
INSERT INTO orders VALUES
  (1, 'Amel',   129.90, 'TN', '2026-07-01 09:15:00'),
  (2, 'Youssef', 89.00, 'TN', '2026-07-02 14:30:00'),
  (3, 'Sarah',  240.50, 'FR', '2026-07-03 11:05:00');

En coulisses, DuckLake a écrit un fichier Parquet dans data/ et enregistré un nouveau snapshot dans la base de métadonnées. Chaque instruction validée produit un snapshot — une version immuable et numérotée de l'ensemble du catalogue.

Les transactions couvrant plusieurs instructions et plusieurs tables fonctionnent nativement :

CREATE TABLE refunds (
  order_id INTEGER,
  amount   DECIMAL(10, 2),
  refunded_at TIMESTAMP
);
 
BEGIN TRANSACTION;
INSERT INTO refunds VALUES (2, 89.00, '2026-07-05 10:00:00');
UPDATE orders SET amount = 0 WHERE order_id = 2;
COMMIT;

Soit les deux modifications sont appliquées, soit aucune. Cette atomicité inter-tables est un vrai différenciateur — les formats lakehouse purement basés fichiers valident généralement une table à la fois.

Les mises à jour et suppressions fonctionnent aussi, tout simplement. DuckLake les gère via de nouveaux fichiers de données et un suivi des suppressions dans le catalogue, de façon transparente :

DELETE FROM orders WHERE country = 'FR';
SELECT count(*) FROM orders;  -- 2

Étape 4 : le time travel avec les snapshots

Chaque commit a créé un snapshot. Listez-les :

SELECT * FROM lake.snapshots();

Vous verrez des versions numérotées avec horodatage et un résumé des changements de chacune. Interrogeons maintenant le passé. Vous vous souvenez de la commande française supprimée ? Lisez la table telle qu'elle était à une version antérieure :

-- Par numéro de version (à ajuster selon votre liste de snapshots)
SELECT * FROM orders AT (VERSION => 4);
 
-- Par horodatage
SELECT * FROM orders AT (TIMESTAMP => now() - INTERVAL 10 MINUTE);

Les lignes supprimées sont de retour — pas restaurées, simplement visibles, car les anciens fichiers Parquet et l'ancien état du catalogue existent toujours. Le time travel alimente trois usages concrets :

  1. Audits : « Que contenait cette table quand le rapport a tourné le 3 juillet ? »
  2. Débogage de pipelines : comparer deux snapshots pour voir exactement ce qu'un job a modifié.
  3. Récupération instantanée : réparer une suppression malheureuse en réinsérant depuis une version passée :
INSERT INTO orders
SELECT * FROM orders AT (VERSION => 4)
WHERE country = 'FR';

Étape 5 : l'évolution de schéma sans réécriture

Les besoins changent ; votre lakehouse ne devrait pas exiger un week-end de migration. DuckLake gère l'évolution de schéma comme des opérations purement métadonnées — les fichiers Parquet existants ne sont pas réécrits :

ALTER TABLE orders ADD COLUMN channel VARCHAR DEFAULT 'web';
ALTER TABLE orders RENAME COLUMN customer TO customer_name;

Interrogez la table : les anciennes lignes affichent la valeur par défaut de la nouvelle colonne, les nouvelles insertions peuvent la renseigner. Les anciens fichiers sur disque restent intacts ; le catalogue se contente de mapper les anciens schémas de fichiers vers le schéma courant de la table. Et comme les changements de schéma sont eux aussi snapshotés, le time travel restitue le schéma tel qu'il était :

SELECT * FROM orders AT (VERSION => 4);  -- affiche encore 'customer', pas de 'channel'

C'est radicalement plus simple que la valse de réécriture de manifestes exigée par d'autres formats, et c'est l'un des arguments les plus forts de DuckLake pour faire évoluer sereinement des schémas analytiques.

Étape 6 : utiliser le lakehouse depuis Python

Le client Python de DuckDB fait du lakehouse un citoyen de première classe dans les notebooks et les pipelines. Installation :

pip install duckdb pandas

Puis lisez et écrivez le même catalogue :

import duckdb
 
con = duckdb.connect()
con.sql("ATTACH 'ducklake:/Users/vous/lakehouse/metadata.ducklake' AS lake (DATA_PATH '/Users/vous/lakehouse/data/')")
con.sql("USE lake")
 
# Requête analytique directement vers un DataFrame
df = con.sql("""
    SELECT country, count(*) AS orders, sum(amount) AS revenue
    FROM orders
    GROUP BY country
    ORDER BY revenue DESC
""").df()
print(df)
 
# Réécrire un DataFrame comme nouvelle table, de façon transactionnelle
import pandas as pd
returns = pd.DataFrame([
    dict(order_id=1, reason="damaged", created_at="2026-07-06 08:00:00"),
])
con.sql("CREATE TABLE IF NOT EXISTS returns AS SELECT * FROM returns")

Le même schéma fonctionne depuis Node.js avec @duckdb/node-api, depuis la CLI dans des tâches cron, et depuis tout outil BI qui parle DuckDB. Un seul catalogue, plusieurs lecteurs et rédacteurs, coordonnés par les transactions du catalogue.

Étape 7 : passer à l'échelle — catalogue PostgreSQL et données sur S3

L'installation locale est mono-machine. Pour partager le lakehouse entre une équipe ou une flotte de services, changez exactement deux choses : les métadonnées vont dans PostgreSQL, les données dans un object store.

Créez d'abord une base PostgreSQL vide (ici lake_catalog), puis attachez avec une chaîne de connexion PostgreSQL comme backend de métadonnées :

INSTALL postgres;
 
CREATE SECRET s3_creds (
  TYPE s3,
  KEY_ID 'VOTRE_ACCESS_KEY',
  SECRET 'VOTRE_SECRET_KEY',
  REGION 'eu-west-1'
);
 
ATTACH 'ducklake:postgres:dbname=lake_catalog host=db.internal user=lake password=***'
  AS lake (DATA_PATH 's3://mon-lakehouse-entreprise/main/');
USE lake;

Tout ce que vous avez fait des étapes 3 à 6 fonctionne à l'identique. Ce qui change sur le plan opérationnel :

  • Concurrence : plusieurs clients DuckDB — portables, conteneurs, workers Airflow — s'attachent au même catalogue. Le moteur transactionnel de PostgreSQL arbitre les commits ; les écritures conflictuelles se résolvent comme dans une base de données, pas avec des boucles de retry sur object store.
  • Durabilité : fichiers Parquet sur S3, catalogue dans un Postgres managé et sauvegardé.
  • Contrôle d'accès : accordez ou révoquez l'accès au catalogue avec des utilisateurs et rôles de base de données classiques.

Attention : ne commitez jamais d'identifiants dans des scripts SQL. Utilisez les secrets DuckDB comme ci-dessus, des variables d'environnement, ou des rôles IAM quand c'est possible.

Une progression raisonnable : commencez en local (étapes 2 à 6), et quand un second consommateur apparaît, migrez en attachant les deux catalogues et en copiant les tables avec CREATE TABLE lake_shared.orders AS SELECT * FROM lake_local.orders.

Étape 8 : maintenance — snapshots et compaction de fichiers

Deux réalités opérationnelles de tout lakehouse : les vieux snapshots s'accumulent, et les petites insertions fréquentes créent beaucoup de petits fichiers Parquet. DuckLake fournit des fonctions de maintenance pour les deux.

Expirez les snapshots plus anciens que votre fenêtre de rétention, puis supprimez les fichiers Parquet qu'aucun snapshot survivant ne référence :

CALL ducklake_expire_snapshots('lake', older_than => now() - INTERVAL 7 DAY);
CALL ducklake_cleanup_old_files('lake', cleanup_all => true);

Compactez les petits fichiers adjacents en fichiers plus gros pour des scans plus rapides :

CALL ducklake_merge_adjacent_files('lake');

Planifiez ces opérations dans une tâche nocturne (cron, Airflow, ou un simple timer systemd). La durée de rétention est une décision métier : sept jours de time travel suffisent largement pour le débogage ; les environnements réglementés en garderont bien davantage.

Tester votre implémentation

Vérifiez chaque capacité de bout en bout :

  1. ACID : ouvrez deux shells DuckDB attachés au même catalogue. Démarrez une transaction d'insertion dans l'un ; vérifiez que le second ne voit rien avant le COMMIT.
  2. Time travel : exécutez SELECT count(*) à deux versions différentes et vérifiez que les comptes diffèrent comme attendu.
  3. Évolution de schéma : vérifiez que SELECT * FROM orders AT (VERSION => 1) montre le jeu de colonnes d'origine.
  4. Aller-retour Python : lancez le script de l'étape 6 et vérifiez que le DataFrame correspond aux résultats du shell.
  5. Contrôle des données ouvertes : la garantie la plus forte de toutes — lisez un fichier de données directement, en contournant complètement DuckLake :
SELECT * FROM read_parquet('data/**/*.parquet') LIMIT 5;

Vos données restent du Parquet ordinaire. Si vous abandonnez DuckLake un jour, les fichiers resteront lisibles par tout l'écosystème.

Dépannage

« Extension ducklake not found. » Votre DuckDB est antérieur à la 1.3. Mettez à jour le binaire, puis relancez INSTALL ducklake.

L'ATTACH échoue avec une erreur de verrou en local. Un catalogue en fichier DuckDB n'autorise qu'un seul processus rédacteur à la fois. Fermez l'autre shell, ou passez au catalogue PostgreSQL (étape 7) pour une vraie concurrence.

Une requête time travel renvoie « snapshot not found ». La version a été expirée par ducklake_expire_snapshots. Consultez SELECT * FROM lake.snapshots() pour les versions survivantes et ajustez votre tâche de rétention.

Les écritures S3 échouent avec un 403. Vérifiez la clé du secret, la région, et que la policy du bucket autorise PutObject sur le préfixe du chemin de données. Testez avec SELECT * FROM read_parquet('s3://bucket/chemin/*.parquet') pour distinguer permissions de lecture et d'écriture.

Requêtes lentes après de nombreuses petites insertions. Le classique problème des petits fichiers — exécutez ducklake_merge_adjacent_files et regroupez les insertions futures par lots quand c'est possible.

Prochaines étapes

  • Exécutez de l'analytique entièrement dans le navigateur avec notre tutoriel DuckDB-WASM et Next.js — un excellent complément pour des dashboards sur des extraits du lakehouse.
  • Alimentez un frontend réactif à partir des tables du lakehouse avec TanStack DB.
  • Explorez l'attachement de votre catalogue DuckLake depuis MotherDuck ou plusieurs régions cloud avec des réplicas en lecture du catalogue Postgres.
  • Ajoutez dbt par-dessus : dbt-duckdb fonctionne avec les catalogues DuckLake attachés pour des transformations modélisées.

Conclusion

Vous avez construit un lakehouse complet sans cluster, sans JVM et sans chasse au trésor dans les fichiers de métadonnées : un catalogue en SQL, des données en Parquet, des transactions ACID, du time travel, de l'évolution de schéma, un accès Python, et un chemin clair du portable à l'échelle d'une équipe avec PostgreSQL et S3. Le pari de DuckLake — les métadonnées d'un lakehouse appartiennent à une base de données, pas à des fichiers — rend le cas simple trivial et le cas à l'échelle ennuyeux, dans le meilleur sens du terme. Commencez en local, livrez de la valeur, et ne faites évoluer le catalogue que le jour où un second rédacteur se présente vraiment.