
Après avoir exploré les concepts théoriques des architectures Lakehouse, votre directeur technique Marc vous confie maintenant une mission pratique : choisir et implémenter le format de table ouvert le plus adapté aux besoins de l'entreprise. Les équipes ont identifié deux candidats principaux : Delta Lake et Apache Iceberg. Chacun promet des transactions ACID, une évolution des schémas et des performances optimisées, mais lequel répondra le mieux aux spécificités agricoles de GreenFarm ? Marc vous demande de plonger dans les détails techniques de ces deux technologies et de réaliser vos premières expérimentations. C'est parti !
Les data lakes sont des entrepôts de fichiers : flexibles, peu coûteux, mais sans garantie d’intégrité.
Pour une requête analytique ou une mise à jour de données, cela peut devenir un casse-tête : fichiers partiels, schémas incohérents, difficultés à rejouer une opération...
Les open table formats résolvent ces problèmes en ajoutant :
un journal de transactions pour garantir les opérations ACID ;
un système de versioning (time travel) ;
une gestion des métadonnées pour optimiser les requêtes ;
et une compatibilité multi-outils : Spark, Trino, Flink, DuckDB, Python...
Ces formats, comme Delta Lake ou Apache Iceberg, sont la clé de l’architecture Lakehouse — un pont entre la souplesse du data lake et la fiabilité du data warehouse.
Delta Lake est un format de stockage open source créé par Databricks et aujourd’hui géré par la Linux Foundation. Il a été conçu pour apporter fiabilité et gouvernance aux data lakes modernes, tout en restant interopérable et ouvert. Delta Lake s’appuie sur des fichiers Parquet, mais leur ajoute une couche de gestion transactionnelle et un mécanisme d’historisation. Autrement dit, il transforme un simple répertoire de fichiers en une véritable table de base de données transactionnelle stockée sur le cloud.
Avant Delta Lake, les pipelines de données “classiques” écrivaient directement dans des fichiers Parquet. Voici le problème :
si un job échoue, les données peuvent être partiellement écrites ;
il n’y a pas de contrôle de version ou de rollback possible ;
et les utilisateurs ne savent jamais si la lecture d’un fichier reflète un état complet et cohérent.
Delta Lake résout cela grâce à :
des transactions ACID (comme dans une base de données relationnelle) ;
un journal de logs (_delta_log) qui enregistre chaque modification ;
une compatibilité ascendante avec Parquet (les données restent lisibles par d’autres outils) ;
et un support multi-langages : Spark, Python, Rust, Scala, Java, Presto, Trino…
Delta Lake enregistre toutes les opérations dans un dossier spécial nommé _delta_log.
Chaque opération (ajout, suppression, modification, changement de schéma) génère un fichier JSON contenant la description des fichiers affectés, des métadonnées, et de la nouvelle version de la table.
Chaque fichier JSON représente un commit avec les actions effectuées sur les données.
/data/customers/
├── part-0000.parquet
├── part-0001.parquet
└── _delta_log/
├── 00000000000000000000.json
├── 00000000000000000001.jsonGrâce à ce mécanisme :
les opérations sont atomiques ;
les tables sont versionnées ;
l’intégrité est garantie même en cas de panne.
C’est ce qui permet à Delta Lake d’offrir un comportement de base de données ACID tout en conservant la scalabilité du stockage objet (S3, ADLS, GCS...).
Delta Lake est simple à prendre en main : une fois installé, vous pouvez manipuler vos tables directement depuis Python.
Voyons comment le mettre en place, créer une table, lire son contenu et explorer la gestion des versions.
Commençons par installer la librairie deltalake, qui est l’implémentation Python du moteur Delta-RS (en Rust). Cette bibliothèque vous permet d’écrire et de lire des tables Delta de manière locale, tout en profitant des fonctionnalités clés : versioning, métadonnées, et compatibilité Parquet.
pip install deltalake pandas pyarrow
Une fois la bibliothèque installée, vous êtes prêt à créer votre première table Delta !
Nous allons créer une petite table à partir d’un DataFrame pandas. Ce DataFrame représente une liste de clients fictifs, que nous allons ensuite sauvegarder au format Delta.
from pathlib import Path
import pandas as pd
from deltalake import write_deltalake
df = pd.DataFrame({
"id": [1, 2, 3],
"name": ["Alice", "Bob", "Charlie"]
})
base_path = Path(__file__).parent
delta_path = base_path / "delta_demo"
write_deltalake(
str(delta_path),
df,
"overwrite"
)
print(f"Table Delta créée dans : {delta_path.resolve()}")Ce code crée automatiquement un dossier /tmp/delta_demo contenant :
vos données en fichiers Parquet,
et le dossier spécial _delta_log où sont enregistrées les métadonnées et versions.

Maintenant que votre table est créée, vous pouvez la lire et la manipuler. Voyons comment l’ouvrir, ajouter des lignes, et vérifier la version courante de la table.
# Lecture simple
table = DeltaTable(str(delta_path))
print(table.to_pandas())
# Ajout de nouvelles données
df2 = pd.DataFrame({"id": [4], "name": ["Diana"]})
write_deltalake(
str(delta_path),
df2,
mode="append"
)
print(f"Table Delta updated.")
# Relecture de la table pour vérifier l'insertion
table = DeltaTable(str(delta_path))
print(table.to_pandas())
# Vérifiez la nouvelle version
print("Version :", table.version())À chaque écriture, Delta Lake crée une nouvelle version de la table dans le dossier _delta_log .
Cela permet d’assurer une traçabilité complète de toutes les opérations.

Vous pouvez revenir à n’importe quelle version précédente grâce à la fonctionnalité de time travel.
C’est particulièrement utile pour restaurer une table, comparer deux états ou auditer les modifications passées.
# Lecture simple
old_table = DeltaTable(str(delta_path), version=0)
print(old_table.to_pandas())

Ici, nous lisons la première version (v0) de la table, avant l’ajout de “Diana”. Delta Lake gère automatiquement la reconstruction de cette version à partir de son journal de transactions.
pip install polars deltalake
mport polars as pl
from pathlib import Path
from deltalake import write_deltalake, DeltaTable
base_path = Path(__file__).parent
delta_path = base_path / "delta_polars"
df = pl.DataFrame({"id": [1, 2, 3], "city": ["Paris", "Lyon", "Marseille"]})
# Conversion Polars -> Arrow (robuste)
arrow_table = df.to_arrow()
write_deltalake(str(delta_path),arrow_table, mode="overwrite")
table = DeltaTable(str(delta_path))
print(table.to_pyarrow_table().to_pandas().head())Delta Lake propose une image Docker prête à l’emploi : Delta Lake Docker est une image auto-contenue intégrant tous les composants nécessaires pour lire et écrire avec Delta Lake : Python, Rust, PySpark, Apache Spark et Jupyter Notebooks.
docker pull deltaio/delta-docker
docker run -it -p 8888:8888 deltaio/delta-dockerVous obtiendrez un environnement complet, idéal pour tester vos pipelines localement ou dans un atelier de formation.
Apache Iceberg est un format open source de gestion de tables conçu à l’origine par Netflix, puis adopté par la fondation Apache. Son objectif est similaire à celui de Delta Lake : apporter des garanties transactionnelles, du versioning et des métadonnées structurées sur les données d’un data lake.
Cependant, Iceberg met particulièrement l’accent sur :
la performance dans les environnements distribués (notamment sur S3) ;
la portabilité totale entre outils (Spark, Flink, Trino, Dremio, DuckDB, etc.) ;
et une architecture de métadonnées hiérarchisée, plus adaptée aux très grands volumes.
Iceberg organise les informations d’une table en trois niveaux de métadonnées :
Les fichiers de données: Généralement au format Parquet (ou ORC), ils contiennent les lignes de données brutes.
Les fichiers de manifestes: Ils listent les fichiers de données associés à un instant donné de la table.
Les fichiers de métadonnées de table (metadata.json,manifest-list, snapshots…) Ils décrivent :
le schéma de la table,
les partitions,
les snapshots disponibles.
Chaque modification (ajout, suppression ou mise à jour) génère un nouveau snapshot, qui référence un ensemble cohérent de fichiers de données via des manifestes.
Delta Lake peut être vu comme un journal de bord : on suit les changements étape par étape.
Iceberg, lui, fonctionne comme un album photo : chaque snapshot est une photo complète et cohérente de la table à un instant donné.
Vous pouvez revenir à une ancienne “photo” sans rejouer toutes les opérations intermédiaires.
Iceberg est maintenu par la fondation Apache, ce qui garantit son indépendance vis-à-vis de tout éditeur. Il est compatible avec la plupart des moteurs de traitement distribués (Spark, Flink, Trino, Dremio, etc.), et peut s’appuyer sur différents catalogues de métadonnées :
Hive Metastore,
AWS Glue,
Nessie,
ou un catalogue REST autonome.
Cela lui permet d’être intégré dans des architectures très hétérogènes et multi-clouds.
Une table Iceberg ne peut pas exister sans catalogue.
Le catalogue est responsable de :
référencer les tables (par exempledefault.customers) ;
stocker l’emplacement des métadonnées ;
gérer les versions et les snapshots ;
permettre aux moteurs de retrouver les tables.
Dans ce chapitre, pour simplifier l’expérimentation, nous utilisons :
un catalogue SQL local (SQLite) pour stocker les métadonnées ;
un warehouse local situé dans le même dossier que le script Python.
Cela permet d’expérimenter Iceberg sans serveur externe, tout en conservant les concepts fondamentaux.
Installez le module officiel pyiceberg, qui permet d’interagir avec des tables Iceberg directement depuis Python :
pip install pyiceberg pyarrow 'pyiceberg[sql-sqlite]'
Dans ce script :
on crée un dossier iceberg_demo/ à côté du fichier Python (warehouse),
on configure un catalogue SQLite local,
on crée la table default.customers si elle n’existe pas (sinon on la recharge).
from pathlib import Path
import pyarrow as pa
from pyiceberg.schema import Schema
from pyiceberg.types import NestedField, LongType, StringType
from pyiceberg.catalog import load_catalog
from pyiceberg.exceptions import TableAlreadyExistsError
# --- Chemins locaux (à côté du script) ---
base_path = Path(__file__).parent
warehouse_path = base_path / "iceberg_demo"
warehouse_path.mkdir(parents=True, exist_ok=True)
# --- Schéma Iceberg (champs optionnels pour matcher PyArrow par défaut) ---
schema = Schema(
NestedField(1, "id", LongType(), required=False),
NestedField(2, "name", StringType(), required=False),
)
# --- Catalogue local (SQLite) + warehouse local ---
catalog = load_catalog(
"local",
{
"type": "sql","uri": f"sqlite:///{(base_path / 'iceberg_catalog.db').resolve()}","warehouse": warehouse_path.resolve().as_uri(),
},
)
# Namespace (création si besoin)
try:
catalog.create_namespace("default")
except Exception:
pass
identifier = "default.customers"
# Table : create-or-load
try:
table = catalog.create_table(identifier, schema=schema)
print("Table créée :", identifier)
except TableAlreadyExistsError:
table = catalog.load_table(identifier)
print("Table déjà existante, chargée :", identifier)
print("Location :", table.location())
# --- Écriture via PyArrow ---
arrow_table = pa.table(
{
"id": [1, 2, 3] "name": ["Alice", "Bob", "Charlie"],
}
)
table.append(arrow_table)
print("Données ajoutées (nouveau snapshot créé)")
# --- Lecture ---
print("\nContenu de la table :")
for row in table.scan().to_arrow().to_pylist():
print(row)
# --- Snapshots ---
print("\nSnapshots disponibles :")
for snapshot in table.snapshots():
print(snapshot.snapshot_id, snapshot.timestamp_ms)Vous devriez voir l’affichage suivant après exécution:

Une fois le script exécuté, Iceberg matérialise la table dans le warehouse sous forme de dossiers et de fichiers. Tu peux visualiser une structure de ce type :
iceberg_demo/
└── default/
└── customers/
├── data/
└── 00000-*.parquet
└── metadata/
├── v1.metadata.json
├── snap-*.avro
└── manifest-*.avrodata/contient les fichiers de données (souvent Parquet). Ce sont eux qui stockent réellement les lignes.
metadata/contient les métadonnées Iceberg, qui décrivent la table et ses versions :
v1.metadata.jsondécrit la table (schéma, partitions, localisation, snapshots…),
snap-*.avroreprésente les snapshots (versions) successifs,
manifest-*.avroréférence les fichiers de données impliqués dans un snapshot donné.
À chaque écriture (append), Iceberg :
écrit de nouveaux fichiers Parquet dansdata/;
génère un nouveau snapshot ;
met à jour les fichiers de métadonnées pour pointer vers cette nouvelle version.
Fonctionnalité | Delta Lake | Apache Iceberg |
Organisation interne | Journal de transactions (_delta_log, JSON) | Snapshots avec manifests et métadonnées (metadata.avro) |
Mécanisme de versioning | Incrémental (commit par commit) | Par snapshot complet |
Compatibilité moteur | Spark, Databricks, Trino, DuckDB … | Spark, Databricks, Flink, Trino, Dremio, DuckDB … |
Catalogues pris en charge | Local, Hive, REST, Databricks | Hive, Glue, Nessie, REST, Databricks |
Time travel, Transactions ACID, Schéma évolutif | ✅ Oui | ✅ Oui |
Positionnement | Databricks / Linux Foundation | Fondation Apache |
Pendant plusieurs années, le monde de la donnée a été marqué par une compétition entre ces deux formats. Chacun cherchait à devenir le standard du Lakehouse, au même titre qu’Apache Parquet l’est devenu pour le stockage colonne. Mais aujourd’hui, cette opposition s’estompe :
Les deux formats convergent vers des spécifications communes et des interfaces unifiées.
Databricks et les contributeurs d’Iceberg ont lancé Uniform, une initiative d’interopérabilité entre les formats de tables ouvertes.
Et surtout, Databricks a acquis Tabular — la société fondée par les créateurs d’Apache Iceberg — en 2024.
Cette acquisition a marqué un tournant : elle a rapproché les deux projets et aligné leurs roadmaps techniques. En pratique :
Les moteurs analytiques modernes (Trino, Flink, Dremio, Snowflake, Databricks…) savent désormais lire à la fois Delta et Iceberg.
Delta Lake 3.0 a introduit le Universal Format (UniForm), rendant les tables Delta compatibles avec les lecteurs Iceberg et Hudi.
Ainsi, la “guerre des formats” se transforme en standardisation ouverte au bénéfice des utilisateurs.
Après cette exploration des formats ouverts, GreenFarm a tranché. L’équipe data, déjà équipée de Databricks pour ses traitements analytiques et la gouvernance des données, a décidé d’adopter Delta Lake comme format unique au sein de son architecture Lakehouse. Cette décision s’appuie sur la maturité de Delta, son intégration native à Spark et les garanties de performance et de fiabilité offertes par l’écosystème Databricks.

Votre mission est de testez Delta Lake et Iceberg sur un vrai jeu de données ! Vous allez manipuler deux open table formats — Delta Lake et Apache Iceberg — à partir d’un petit jeu de données clients.
Créer une table Delta Lake locale à partir de clients.csv avec la librairie deltalake.
Ajouter deux nouveaux clients dans cette table, puis vérifier le numéro de version.
Utiliser la fonction time travel pour relire la table d’origine (version 0).
Créer une table Iceberg équivalente avec pyiceberg, en ajoutant le même jeu de données.
Lister les snapshots pour comprendre comment Iceberg gère ses versions.
Ces formats garantissent des transactions ACID, un time travel complet et un schéma évolutif.
Delta Lake repose sur un journal de transactions incrémental (_delta_log), parfaitement intégré à Databricks et Spark.
Apache Iceberg gère ses versions via des snapshots complets et se distingue par sa compatibilité avec de nombreux moteurs.
La “guerre des formats” s’estompe : Delta et Iceberg convergent vers un standard interopérable grâce à Uniform et à l’acquisition de Tabular par Databricks.
Cette évolution consacre l’idée d’un écosystème unifié et ouvert pour les données analytiques et le machine learning.
Dans le prochain chapitre, vous découvrirez le fonctionnement interne de Delta Lake : comment il enregistre les transactions, gère ses versions et optimise les tables à grande échelle.