Introduction
Certification Databricks

Data Engineer Associate

Un guide complet pour réviser les concepts, choisir le bon outil selon le scénario et éviter les pièges classiques du QCM.

45 questions 90 minutes 13 chapitres Validité 2 ans

1. L'examen en bref

Élément Valeur
Questions notées 45 QCM
Durée 90 minutes, soit environ 2 minutes par question
Aides Aucune
Prérequis formel Aucun
Expérience recommandée Formation et environ 6 mois de pratique
Validité 2 ans

Le nouveau référentiel ne publie pas de pondération par domaine. Il couvre :

  1. Databricks Data Intelligence Platform ;
  2. ingestion et chargement ;
  3. transformation et modélisation ;
  4. Lakeflow Jobs ;
  5. CI/CD ;
  6. dépannage, supervision et optimisation ;
  7. gouvernance et sécurité.

L'examen teste surtout la capacité à choisir le bon outil pour un scénario, à reconnaître du code correct et à diagnostiquer un problème. Il faut donc connaître les différences entre les solutions, pas seulement mémoriser leur syntaxe.


2. Modèle mental de la plateforme

2.1 Lakehouse et Data Intelligence Platform

Un lakehouse combine :

  • la souplesse et le coût du stockage objet d'un data lake ;
  • les transactions, le schéma, les performances et les usages BI d'un data warehouse ;
  • une gouvernance commune pour les données et les actifs d'IA.

Les briques à retenir sont :

Besoin Brique principale
Gouverner données, fichiers, modèles et fonctions Unity Catalog
Stocker des tables transactionnelles ouvertes Delta Lake ou Apache Iceberg
Ingérer Lakeflow Connect, Auto Loader, COPY INTO, connecteurs
Transformer en batch ou streaming Spark, SQL, Lakeflow Spark Declarative Pipelines
Orchestrer Lakeflow Jobs
Servir SQL et BI Databricks SQL et SQL warehouses
Déployer comme du code Declarative Automation Bundles et Databricks CLI

Le stockage et le calcul sont séparés. Les tables métier résident généralement dans un stockage objet durable ; le compute démarre, lit les données, calcule, puis peut s'arrêter sans supprimer les données.

Pour les questions d'architecture :

  • Databricks SQL est le service de data warehousing de la Data Intelligence Platform ; il s'appuie sur des SQL warehouses pour exécuter les requêtes SQL et servir les usages BI ;
  • l'application web du workspace et les services backend managés se trouvent dans le control plane ;
  • le compute plane exécute le traitement des données, sur compute classic ou serverless selon la configuration.

La phrase de QCM « les données client sont entièrement hébergées dans le compte cloud du client » décrit au mieux une architecture classic simplifiée : le classic compute et le workspace storage sont dans le compte cloud du client. Elle n'est pas universelle : le serverless compute s'exécute dans un compute plane géré par Databricks, et certains objets workspace ou résultats sont gérés dans le control plane/default storage. Si l'énoncé précise l'architecture classic ou oppose le stockage métier au control plane, « compte cloud du client » reste la réponse attendue ; si l'énoncé inclut le serverless, il faut nuancer. Toujours distinguer où les tables sont stockées, où le code est conservé et où il s'exécute.

Où est stocké un notebook ?

Le source et les métadonnées d'un notebook Databricks sont des objets du workspace stockés dans le control plane, chiffrés au repos comme données de services managés. Le notebook envoie son code au compute, mais il n'est pas stocké dans les workers du compute plane avec les tables métier.

Lorsqu'un notebook est déclenché par un Job, son code s'exécute dans le compute plane, sur le compute associé à la tâche : cluster Spark classic, jobs compute ou compute serverless. Mémo : notebook stocké dans le control plane ; commandes exécutées dans le compute plane.

Ne pas généraliser cette réponse aux résultats : les petits résultats interactifs peuvent aussi être stockés dans le control plane, tandis que les résultats volumineux et ceux des notebooks exécutés comme Jobs sont généralement placés dans le stockage du workspace. L'emplacement physique de ce stockage dépend notamment du type de workspace — compte cloud client en classic, default storage géré en serverless — et de sa configuration. Pour la question « où est stocké le notebook ? », la réponse attendue est bien control plane, comme objet workspace sécurisé et chiffré.

2.2 Choisir le compute

Compute Cas d'usage Caractéristiques et coût
Serverless notebook Exploration et développement interactif Démarrage rapide, infrastructure gérée, mise à l'échelle automatique
Serverless jobs/pipelines ETL de production Aucun cluster à administrer, dimensionnement et optimisation gérés
All-purpose classic compute Développement interactif nécessitant de la personnalisation Réutilisable et flexible, mais coûte tant qu'il tourne ; éviter pour les jobs récurrents
Job classic compute Tâche automatisée avec configuration particulière Créé pour le run puis arrêté ; meilleure isolation et reproductibilité
SQL warehouse SQL, BI, dashboards, analystes Moteur optimisé pour SQL, concurrence et Photon ; serverless recommandé lorsqu'il est disponible
Single-node Petit volume, tests simples non distribués Le driver fait aussi office de worker ; pas adapté aux gros traitements
Multi-node Traitement distribué Plusieurs executors ; adapté aux volumes importants

Points de décision :

  • Serverless : préférence lorsque la compatibilité du workload le permet ; Databricks gère l'infrastructure.
  • Classic : contrôle du runtime, des types de machines, des bibliothèques et du réseau ; l'infrastructure se trouve dans le compte cloud du client.
  • All-purpose : interactif et partagé.
  • Job compute : automatisé et éphémère.
  • SQL warehouse : SQL/BI, pas un cluster Spark généraliste.
  • Photon : moteur vectorisé natif qui accélère de nombreuses requêtes SQL et DataFrame sans réécriture du code.
  • Le coût total dépend des DBU, de la durée et, pour le classic compute, des ressources du fournisseur cloud. Éteindre ou auto-terminer les ressources inactives.

Sur un compute classic, le même type d'instance avec Photon consomme des DBU à un taux différent du runtime non-Photon, souvent supérieur selon le cloud et le SKU. Ce n'est toutefois pas un « surcoût Photon » universel : Photon est déjà activé/inclus sur les SQL warehouses et les computes serverless. Comparer le coût total du run, pas seulement les DBU par heure : une exécution plus chère à l'heure peut coûter moins si Photon réduit suffisamment sa durée. Vérifier les tarifs du contrat et les données de facturation plutôt que mémoriser un multiplicateur fixe.

Access modes : Standard ou Dedicated

L'access mode détermine l'isolation des utilisateurs et les fonctionnalités disponibles sur un compute classic gouverné par Unity Catalog :

Mode Utilisation typique Limites/particularités
Standard Workloads SQL/Python/Scala multi-utilisateurs compatibles Isolation par utilisateur ; certaines API bas niveau, certains runtimes et R ne sont pas pris en charge
Dedicated Compute attribué à un utilisateur ou groupe Isolation plus forte ; prend en charge les workloads qui exigent notamment R, les RDD ou certains runtimes/bibliothèques non compatibles avec Standard

Scénario de QCM : code exclusivement en R + Unity Catalog + packages système installés par un init script cluster-scoped → Dedicated. R suffit déjà à exclure Standard. Le script peut être stocké dans un volume Unity Catalog et attaché dans Compute > Advanced > Init Scripts ; il s'exécute au démarrage sur chaque nœud du cluster. Avec Standard, les init scripts et bibliothèques concernés doivent en plus respecter l'allowlist, ce qui ne résout de toute façon pas l'absence de support de R.

Les init scripts stockés dans DBFS root sont en fin de vie et ne fonctionnent plus. Préférer un volume Unity Catalog, un workspace file ou le stockage cloud selon le mode d'accès ; un init script issu d'un workspace file n'est notamment pas pris en charge en mode Standard. Utiliser d'abord les fonctions natives de la plateforme ou les bibliothèques déclarées, et réserver les init scripts aux dépendances réellement système.

Dedicated est le nouveau nom de l'ancien mode Single user et peut être affecté à un utilisateur ou, avec les prérequis de runtime, à un groupe. Ne pas le choisir systématiquement : utiliser Standard pour le partage compatible et serverless lorsque le workload le permet.

Instances Spot/Preemptible

Les workers Spot coûtent moins cher, mais le fournisseur cloud peut les reprendre avec peu de préavis. Le meilleur cas est donc un batch non critique, relançable et idempotent, avec retries et checkpointing si nécessaire. Éviter de leur confier seuls un flux continu, une tâche interactive ou un workload à SLA strict qui ne tolère pas la perte d'un worker.

Sur AWS, lorsque l'option Spot standard du compute est activée, le driver reste on-demand et les workers suivants peuvent être Spot. La tolérance aux pannes réduit le risque mais ne rend pas le compute Spot aussi fiable que l'on-demand.

Instance pools pour le compute classic

Un instance pool, désormais souvent appelé simplement pool, conserve un ensemble d'instances cloud inactives et prêtes à être affectées à des clusters. Un cluster classic rattaché au pool peut réutiliser ces machines au lieu d'attendre leur provisionnement : cela réduit son startup time et le délai d'autoscaling.

TEXT
Pool d'instances prêtes
        ├── cluster Job A démarre et prend des instances
        ├── cluster Job B scale out et en prend d'autres
        └── à l'arrêt, les instances retournent au pool

Réglages à comprendre :

  • Minimum idle instances : nombre d'instances maintenues prêtes ; plus il est élevé, plus le démarrage est rapide mais plus le coût cloud au repos augmente ;
  • Maximum capacity : nombre maximal d'instances, actives et inactives, que le pool peut contenir ;
  • Idle instance auto termination : durée après laquelle une instance inactive au-delà du minimum peut être libérée ;
  • Preloaded Databricks Runtime : runtime préchargé sur les instances inactives pour réduire davantage le démarrage.

Si aucune instance idle compatible n'est disponible, le pool doit en demander une nouvelle au fournisseur cloud : le gain de démarrage diminue. Les instances inactives ne consomment pas de DBU, mais restent facturées par le fournisseur cloud. Les pools concernent le compute classic ; lorsqu'un workload est compatible, Databricks recommande généralement le serverless, dont l'infrastructure est gérée automatiquement.

Auto Stop d'un SQL warehouse

Auto Stop arrête le warehouse après une durée configurée sans requête active afin d'éviter de facturer un compute inactif. Une nouvelle requête, un Job, une connexion JDBC/ODBC ou l'ouverture d'un dashboard peut le redémarrer automatiquement, avec un éventuel délai de démarrage.

  • Serverless : 10 minutes par défaut dans l'UI, minimum 5 minutes dans l'UI.
  • Pro/classic : 45 minutes par défaut, minimum 10 minutes.
  • Ne pas confondre Auto Stop, qui arrête un warehouse inactif, avec l'autoscaling, qui adapte le nombre de clusters à la concurrence pendant son fonctionnement.

Un délai court réduit le coût des usages sporadiques ; un délai plus long peut réduire les redémarrages pour un trafic intermittent. Le réglage se trouve dans SQL Warehouses > Edit > Auto Stop.

2.3 Unity Catalog : namespace et objets de données

Le nom complet d'une table suit la hiérarchie à trois niveaux :

TEXT
catalog.schema.object

Exemple :

SQL
SELECT * FROM production.sales.orders;
  • Le metastore est le conteneur de gouvernance de plus haut niveau.
  • Un catalog sert souvent de frontière métier, d'environnement ou d'isolation.
  • Un schema organise les objets d'un catalogue.
  • Une table, une vue, un volume, une fonction ou un modèle est un objet sécurisable.
  • Un volume gouverne des fichiers non tabulaires. Son chemin est :
TEXT
/Volumes/<catalog>/<schema>/<volume>/<chemin>

Utiliser un nom de table pour les données tabulaires et un volume pour les fichiers. Éviter l'accès direct par chemin aux tables managées, car il contournerait la gouvernance Unity Catalog.

2.3.1 Storage credentials et external locations

Une storage credential est un objet sécurisable du metastore qui représente le mécanisme d'authentification au stockage cloud, par exemple un rôle IAM AWS, sans exposer ses secrets dans les notebooks. Elle ne désigne aucun chemin.

Une external location associe une URL/prefixe de stockage à une storage credential. Elle borne donc où cette identité peut être utilisée et permet à Unity Catalog d'appliquer des privilèges sur ce chemin. Elle ne copie pas les fichiers et n'est pas elle-même une table.

Relation à mémoriser :

TEXT
Identité cloud (IAM role / managed identity / service account)
        -> Storage Credential (comment s'authentifier)
        -> External Location (quel chemin gouverner)
        -> External Table ou External Volume (quel objet de données exposer)

Une table gouverne des lignes interrogeables ; un volume gouverne des fichiers accessibles par chemin. Choisir un external volume lorsqu'un système externe dépose déjà des réponses REST, JSON, images ou autres fichiers non tabulaires dans un bucket géré par le client. Unity Catalog gouverne alors les accès effectués via /Volumes, mais un outil qui accède directement à l'URI cloud reste soumis aux contrôles IAM du cloud.

La storage credential est créée par un administrateur via Catalog Explorer, CLI, SDK ou API selon le cloud. L'external location et les objets de données s'expriment naturellement en DDL SQL :

SQL
CREATE EXTERNAL LOCATION IF NOT EXISTS sales_landing
URL 's3://company-data/sales'
WITH (STORAGE CREDENTIAL company_sales_credential)
COMMENT 'Zone de données Sales gouvernée par Unity Catalog';

Pour une API REST, un external volume peut constituer une bonne zone de fichiers bruts ; son exemple de création est placé avec les autres objets externes en 2.3.2.2. Si le JSON doit ensuite être filtré, joint ou agrégé, l'ingérer dans une table Delta Bronze/Silver. Un volume ne remplace donc pas une table tabulaire.

Il n'existe pas d'alternative DataFrame PySpark pour créer ces objets de gouvernance ; utiliser le DDL SQL, éventuellement envoyé par spark.sql(...). PySpark devient pertinent pour lire ou écrire un DataFrame dans une table déjà autorisée, comme dans la section suivante. Le droit cloud de la credential ne suffit pas : le principal Databricks doit aussi recevoir les privilèges Unity Catalog appropriés sur l'external location et les objets.

Sur Azure, le rôle Storage Blob Data Contributor est couramment attribué à l'access connector ou à l'identité managée pour permettre la lecture et l'écriture dans ADLS. Ce rôle n'est toutefois pas une option d'Auto Loader : c'est un droit du stockage. Une ingestion strictement en lecture peut recevoir un rôle plus restreint, tandis que la création de notifications/file events requiert aussi les permissions Azure Event Grid et de file events prévues par la configuration. Toujours distinguer droits cloud, storage credential et grants Unity Catalog.

2.3.2 Créer les catalogues, schémas et tables

Le type est déterminé au niveau de la table, pas du schéma : un même schéma standard Unity Catalog peut contenir des tables managées et externes. Une table étrangère appartient, elle, à un foreign catalog qui reflète une source distante.

2.3.2.1 Table managée

Une table managée nécessite un managed storage disponible au niveau du schéma, du catalogue ou du metastore. Si l'on déclare explicitement MANAGED LOCATION, son URL doit être couverte par une external location et le créateur doit y posséder CREATE MANAGED STORAGE.

SQL
-- Hypothèse : « company_managed_storage » couvre s3://company-uc/.
GRANT CREATE MANAGED STORAGE
ON EXTERNAL LOCATION company_managed_storage
TO `data_platform_admins`;

-- Définit le stockage par défaut des objets managés du catalogue.
CREATE CATALOG IF NOT EXISTS production
MANAGED LOCATION 's3://company-uc/production';

-- Hérite de la managed location du catalogue (sinon : MANAGED LOCATION 's3://company-uc/production/sales').
CREATE SCHEMA IF NOT EXISTS production.sales;

-- Sans LOCATION : Unity Catalog choisit le chemin, donc table managée.
CREATE TABLE production.sales.orders (
  order_id BIGINT,
  order_date DATE,
  amount DECIMAL(12, 2)
) USING DELTA;

CREATE CATALOG crée automatiquement default et information_schema, mais aucun schéma métier ni aucune table métier : production.sales et production.sales.orders doivent donc être créés explicitement. Si le metastore possède déjà un managed storage, le catalogue peut lui-même l'hériter et être créé sans MANAGED LOCATION.

Pour isoler les futurs objets de production.sales, on pourrait déclarer ce schéma avec MANAGED LOCATION 's3://company-uc/production/sales' à la place de la création par héritage présentée ci-dessus.

Ordre de résolution : schéma, sinon catalogue, sinon metastore. Unity Catalog génère des sous-répertoires internes uniques. Modifier ultérieurement une managed location ne déplace pas les objets existants ; le nouvel emplacement ne concerne que les nouveaux objets.

Version PySpark lorsque le DataFrame existe déjà :

PYTHON
orders_df.write.mode("overwrite").saveAsTable("production.sales.orders")
2.3.2.2 Table externe

Une table externe utilise un LOCATION explicite couvert par une external location, ici sales_landing créée en 2.3.1. Le catalogue et le schéma restent des objets standard ; leur éventuelle managed location n'est pas utilisée par cette table. La création suppose aussi USE CATALOG, USE SCHEMA et CREATE TABLE sur les parents.

SQL
-- Les droits sur le catalogue et le schéma sont supposés déjà accordés.
-- READ FILES est utile si l'équipe doit aussi lire directement les fichiers.
GRANT CREATE EXTERNAL TABLE, READ FILES
ON EXTERNAL LOCATION sales_landing TO `data_engineers`;

CREATE TABLE production.sales.orders_external (
  order_id BIGINT,
  order_date DATE,
  amount DECIMAL(12, 2)
) USING DELTA
LOCATION 's3://company-data/sales/orders';

Version PySpark :

PYTHON
(orders_df.write
          .format("delta")
          .mode("overwrite")
          .option("path", "s3://company-data/sales/orders")
          .saveAsTable("production.sales.orders_external"))

Le chemin externe ne doit pas chevaucher le managed storage du catalogue ou du schéma. DROP TABLE supprimera l'enregistrement Unity Catalog, mais conservera ces fichiers.

Un external volume gouverne des fichiers non tabulaires et non des lignes. Outre USE CATALOG et USE SCHEMA, il requiert CREATE VOLUME sur le schéma et CREATE EXTERNAL VOLUME sur l'external location :

SQL
CREATE SCHEMA IF NOT EXISTS production.raw;

GRANT CREATE VOLUME
ON SCHEMA production.raw TO `data_engineers`;

GRANT CREATE EXTERNAL VOLUME
ON EXTERNAL LOCATION sales_landing TO `data_engineers`;

CREATE EXTERNAL VOLUME production.raw.api_payloads
LOCATION 's3://company-data/sales/api-payloads';

Les consommateurs utilisent ensuite /Volumes/production/raw/api_payloads/... et reçoivent READ VOLUME ou WRITE VOLUME sur le volume selon leur besoin. READ FILES sur l'external location et READ VOLUME sur le volume sont des privilèges distincts.

2.3.2.3 Table étrangère

Une table étrangère n'est généralement pas créée comme une table locale. Pour Lakehouse Federation, une CONNECTION décrit la source et ses credentials, puis un FOREIGN CATALOG expose en lecture les schémas et tables qui existent dans le système distant :

SQL
CREATE CONNECTION operational_postgres
TYPE POSTGRESQL
OPTIONS (
  host 'postgres.example.com',
  port '5432',
  user secret ('federation', 'postgres_user'),
  password secret ('federation', 'postgres_password')
);

CREATE FOREIGN CATALOG operational
USING CONNECTION operational_postgres
OPTIONS (database 'orders');

SELECT * FROM operational.public.orders;

La CONNECTION ne copie aucune donnée et ne suffit pas à exposer les tables. Le foreign catalog reflète les objets de la source ; les foreign tables issues de Lakehouse Federation sont en lecture seule. Il n'existe pas d'équivalent DataFrame PySpark à ces DDL de gouvernance, mais un programme Python peut les exécuter avec spark.sql(...).

Synthèse :

Propriété Table managée Table externe Table étrangère/fédérée
Création CREATE TABLE sans LOCATION CREATE TABLE ... LOCATION CONNECTION puis FOREIGN CATALOG
Emplacement choisi par Unity Catalog Le client via une external location Le système externe
Cycle de vie des fichiers Unity Catalog Le client Le système externe
Optimisations automatiques Oui Limitées Non
Effet de DROP TABLE Métadonnées puis données supprimées selon la rétention Métadonnées supprimées, fichiers conservés Référence supprimée, source conservée
Accès Lecture/écriture Lecture/écriture selon le format Généralement lecture depuis la source

Pour déclarer précisément les objets de gouvernance, SQL est généralement plus explicite. PySpark est naturel lorsque le DataFrame à écrire existe déjà dans le traitement.

Renommer une table managée ou externe est une opération de métadonnées : les données ne sont pas recopiées.

SQL
ALTER TABLE production.sales.orders_draft
RENAME TO production.sales.orders;

Dans Unity Catalog, la cible du renommage doit rester dans le même catalogue. Cette commande ne sert donc pas à déplacer une table entre catalogues ; mettre à jour les consommateurs qui utilisent l'ancien nom.

2.3.3 Convertir avec SET MANAGED, UNSET MANAGED, MOVE et COPY

Databricks recommande les tables managées pour les nouveaux workloads, car Unity Catalog peut gérer leur layout et leur maintenance. Choisir une table externe lorsque des données existantes doivent rester à un chemin précis ou être accédées directement par un système qui n'utilise pas les interfaces gouvernées.

Pour une table externe Delta éligible, Databricks recommande la conversion conservant le nom, l'historique, les permissions et les dépendances :

SQL
-- Nécessite Serverless ou Databricks Runtime 17.3 LTS ou supérieur.
ALTER TABLE production.sales.orders_external SET MANAGED;

SET MANAGED sans option s'applique à une table externe. Une table foreign éligible exige au contraire MOVE ou COPY :

SQL
-- Désactive la table du catalogue externe après la conversion.
ALTER TABLE foreign_catalog.sales.orders SET MANAGED MOVE;

-- Conserve la source externe et crée une copie managée distincte.
ALTER TABLE foreign_catalog.sales.orders SET MANAGED COPY;
Source et commande Effet
Table externe : SET MANAGED Copie vers le managed storage, bascule l'objet UC et peut rediriger temporairement les anciens accès par chemin compatibles
Table foreign : SET MANAGED MOVE Convertit en table managée et désactive l'accès par le catalogue externe ou par chemin ; les consommateurs doivent utiliser le nom UC
Table foreign : SET MANAGED COPY Crée une table managée sans désactiver la source ; les deux copies deviennent distinctes et la bascule reste à organiser

Toutes les tables foreign ne sont pas éligibles : cette conversion vise les tables Delta fédérées via les intégrations prises en charge, notamment Hive metastore ou AWS Glue. La section 2.3.5 montre comment vérifier le type après l'opération.

La conversion externe se déroule en deux phases : copie initiale pendant laquelle lecteurs et écrivains continuent, puis courte bascule pendant laquelle les derniers commits sont transférés et les écritures brièvement bloquées. C'est pourquoi les deux avantages attendus dans le QCM sont : minimiser l'indisponibilité des lecteurs/écrivains et gérer les écritures concurrentes pendant la conversion.

Deux autres avantages à reconnaître :

  • les lectures et écritures Databricks utilisant l'ancien chemin peuvent être redirigées vers la nouvelle table managée pour laisser fonctionner temporairement le code legacy compatible ; migrer malgré tout vers l'accès par nom catalog.schema.table ;
  • la conversion d'une table externe peut être annulée avec UNSET MANAGED pendant la fenêtre de rollback.

Après la conversion, les streams de lecture ou d'écriture existants s'arrêtent volontairement avec DELTA_STREAMING_INTERRUPTED_BY_MANAGED_TABLE_CONVERSION. Il faut redémarrer le stream avec la même configuration et le même checkpoint : il reprend après le dernier offset/micro-batch validé et utilise la table convertie ou la redirection compatible. Ne pas supprimer le checkpoint, ce qui transformerait la reprise en nouvelle requête et pourrait retraiter des données. Vérifier aussi que le runtime prend en charge le mode d'accès utilisé, notamment pour un ancien accès par chemin.

Pendant la fenêtre de rollback d'une table externe convertie, actuellement 14 jours par défaut, UNSET MANAGED permet de revenir à l'emplacement externe d'origine :

SQL
ALTER TABLE production.sales.orders_external UNSET MANAGED;

La commande s'appelle bien UNSET MANAGED, et non SET UNMANAGED. C'est un rollback de conversion, pas une commande générale pour transformer n'importe quelle table managée en table externe. Après un foreign MOVE, il faut d'abord exécuter UNSET MANAGED, puis supprimer l'objet redevenu externe pour permettre sa refédération. Après un foreign COPY, la source n'ayant pas été désactivée, supprimer la copie managée suffit pour revenir à la table foreign lors de la synchronisation suivante. Respecter les procédures de rollback : supprimer directement une table issue d'un MOVE sans UNSET MANAGED peut entraîner une perte de données ou des incohérences.

Pour migrer une table managée ordinaire vers un stockage externe, créer explicitement une nouvelle table dans une external location, valider les données et les consommateurs, puis basculer. Ne pas déplacer manuellement les fichiers d'une table managée.

2.3.4 Copier avec CTAS, DEEP CLONE et SHALLOW CLONE

Un CTAS réalise une copie logique du résultat d'une requête. Comme la cible suivante ne déclare pas de LOCATION, elle est managée :

SQL
CREATE TABLE production.sales.orders_managed
AS SELECT * FROM production.sales.orders_external;

La nouvelle table contient les lignes et le schéma produits par le SELECT, mais elle ne reprend pas automatiquement l'historique Delta, le partitionnement, les propriétés, les contraintes, les tags ou les permissions propres à la source. CTAS permet en revanche de filtrer, joindre, agréger, renommer ou caster pendant la copie. La source et la cible sont ensuite indépendantes et aucune synchronisation continue n'est créée.

Un clone crée un nouvel objet ; il ne convertit pas la table source et ne conserve donc pas son identité Unity Catalog.

Aspect CTAS DEEP CLONE SHALLOW CLONE
Source Résultat de n'importe quelle requête Snapshot d'une table à sa version courante ou choisie Même principe que deep clone
Données Lit le résultat puis écrit de nouveaux fichiers Copie les données et les métadonnées de table Ne copie pas les fichiers ; les référence
Schéma et configuration Dérivés du SELECT ; partitionnement, contraintes et propriétés à redéfinir Copie notamment schéma, partitionnement, invariants, nullabilité et TBLPROPERTIES Copie ces métadonnées, sans les fichiers
Transformations pendant la copie Oui : filtres, joins, agrégations, casts ou projection Non : copie fidèle du snapshot choisi Non : copie fidèle du snapshot choisi
Historique et gouvernance de la source Non copiés Historique Delta, description, tags UC, commit metadata et grants non copiés Même limite
Dépendance après création Aucune Aucune pour lire les données copiées Dépend de la disponibilité des fichiers source
Actualisation Rejouer la requête et gérer le remplacement ou l'upsert Peut être resynchronisé incrémentalement avec un nouveau clone vers la même cible Recrée un snapshot léger selon les limitations UC
Usage typique Nouvelle table transformée ou combinaison de plusieurs sources Copie fidèle indépendante, archive, test ou reprise après sinistre Test rapide et économique

Mémo : CTAS copie un résultat logique et peut le transformer ; DEEP CLONE copie fidèlement l'état et la configuration d'une table, mais pas son historique ni ses droits Unity Catalog. Les deux créent une nouvelle table indépendante ; pour convertir l'objet existant sans changer son identité, utiliser SET MANAGED lorsqu'il est éligible.

SQL
CREATE TABLE dev.sandbox.orders_deep
DEEP CLONE production.sales.orders;

CREATE TABLE dev.sandbox.orders_shallow
SHALLOW CLONE production.sales.orders VERSION AS OF 12;

Version Python avec l'API Delta :

PYTHON
from delta.tables import DeltaTable

source = DeltaTable.forName(spark, "production.sales.orders")
source.clone(
    target="dev.sandbox.orders_deep",
    isShallow=False,
    replace=False,
)
source.cloneAtVersion(
    version=12,
    target="dev.sandbox.orders_shallow",
    isShallow=True,
    replace=False,
)

Chaque clone possède ensuite son propre historique. Modifier le clone ne modifie pas la source. Un shallow clone est rapide et peu coûteux, mais les fichiers référencés doivent rester disponibles ; pour les tables hors gestion UC, un VACUUM de la source peut notamment casser ces références. Un deep clone coûte plus de stockage et de temps à créer, mais il est autonome.

Pour convertir une table externe existante, préférer SET MANAGED à un deep clone :

Critère ALTER TABLE ... SET MANAGED Nouvelle table avec DEEP CLONE
Résultat Conversion de la table existante Nouvelle copie indépendante
Nom, permissions, vues et historique Conservés Bascule des consommateurs à organiser
Écritures pendant la copie initiale Gérées et rattrapées Synchronisation et cutover à gérer
Indisponibilité Minimisée ; courte bascule finale Dépend de la stratégie de bascule
Rollback UNSET MANAGED pendant la fenêtre prévue Aucun rollback de conversion ; la source existe toujours

Utiliser un deep clone lorsque l'objectif est réellement une seconde table autonome, pas pour changer le type de la table existante.

2.3.5 Inspecter avec DESCRIBE et information_schema

Les commandes ne répondent pas à la même question :

Commande Question à laquelle elle répond Principaux résultats
DESCRIBE CATALOG EXTENDED catalog Quel est le réglage du catalogue ? Owner, propriétés, managed location et période de récupération
DESCRIBE SCHEMA EXTENDED catalog.schema Quel est le réglage du schéma ? Owner, propriétés, managed location et période de récupération effective
DESCRIBE TABLE table Quelles sont les colonnes ? Noms, types et commentaires de colonnes
DESCRIBE TABLE EXTENDED table Comment l'objet est-il enregistré et protégé ? Colonnes, owner, Type, provider, emplacement, row filter et column masks éventuels
DESCRIBE DETAIL table Comment la table Delta/Iceberg est-elle physiquement organisée ? Format, identifiant, emplacement, partitions, clustering, nombre/taille des fichiers, propriétés et table features
SQL
DESCRIBE CATALOG EXTENDED production;
DESCRIBE SCHEMA EXTENDED production.sales;

DESCRIBE TABLE production.sales.orders;
DESCRIBE TABLE EXTENDED production.sales.orders;
DESCRIBE TABLE EXTENDED production.sales.orders AS JSON;
DESCRIBE DETAIL production.sales.orders;

DATABASE est un ancien synonyme de SCHEMA, mais SCHEMA est préférable :

SQL
DESCRIBE DATABASE db_hr;
DESCRIBE SCHEMA EXTENDED db_hr;

Dans DESCRIBE TABLE EXTENDED, chercher la ligne Type pour distinguer MANAGED et EXTERNAL. Sur les runtimes récents, la variante AS JSON expose notamment les champs structurés row_filter et column_masks lorsqu'ils existent ; elle est préférable à l'analyse du rapport texte pour une automatisation. DESCRIBE SCHEMA EXTENDED décrit l'emplacement par défaut du schéma, pas le type de ses tables. DESCRIBE DETAIL renvoie une seule ligne structurée : son champ location fournit le chemin physique attribué et son champ format = 'delta' décrit le format, pas le statut managé ou externe.

Pour automatiser le contrôle de plusieurs objets, utiliser information_schema :

SQL
SELECT table_name, table_type
FROM system.information_schema.tables
WHERE table_catalog = 'production'
  AND table_schema = 'sales';

Une table étrangère constitue un cas particulier : certaines fédérations reproduisent le comportement du Hive metastore et DESCRIBE EXTENDED peut afficher EXTERNAL. Pour distinguer sûrement FOREIGN, utiliser Catalog Explorer ou system.information_schema.tables, dont table_type vaut FOREIGN. En Python, ces inspections SQL peuvent être exécutées avec spark.sql(...) ; il n'existe pas d'intérêt à dupliquer chaque DDL avec une API DataFrame.

2.3.6 Supprimer et récupérer avec DROP et UNDROP

L'effet de DROP TABLE dépend du propriétaire des fichiers :

Type Effet sur l'objet Unity Catalog Effet sur les données
Managée Objet supprimé, mais récupérable pendant la période prévue Fichiers supprimés après expiration de la période de récupération
Externe Métadonnées de table supprimées Fichiers conservés dans l'external location
Foreign Référence fédérée supprimée Source distante conservée

Après le DROP d'une table managée Unity Catalog, les données ne disparaissent donc pas immédiatement. La table est récupérable avec UNDROP TABLE pendant 7 jours par défaut ; à l'expiration, Databricks supprime ensuite les fichiers sous-jacents du cloud dans un délai maximal annoncé de 48 heures.

SQL
DROP TABLE production.sales.orders;

-- Possible tant que la période de récupération n'est pas expirée.
UNDROP TABLE production.sales.orders;

-- Fonctionnalité actuelle en Public Preview : 0, ou une durée de 7 à 30 jours.
ALTER SCHEMA production.sales RETAIN DROPPED TO 14 DAYS;
DESCRIBE SCHEMA EXTENDED production.sales;

Le réglage du schéma prend le pas sur celui du catalogue et ne s'applique qu'aux tables supprimées après sa modification. Avec une rétention configurée à 0, la table n'est pas récupérable par UNDROP.

UNDROP TABLE restaure un objet managé supprimé. Ne pas le confondre avec RESTORE TABLE, qui crée une nouvelle version d'une table Delta existante à partir de son historique, ni avec le Time Travel, qui lit une ancienne version sans modifier l'état courant.

2.4 Delta Lake

Une table Delta est principalement constituée de :

  • fichiers de données, généralement en Parquet ;
  • journal de transactions _delta_log ;
  • métadonnées et statistiques utilisées par le moteur.

Delta Lake ne remplace pas le stockage sous-jacent. C'est un format et une couche transactionnelle qui stocke ses fichiers de données et son journal dans un système durable, généralement du cloud object storage comme S3, ADLS ou GCS. Mémo de QCM : object storage + fichiers Parquet + _delta_log = table Delta.

Le journal fournit :

  • transactions ACID ;
  • concurrence optimiste entre lecteurs et écrivains ;
  • contrôle et évolution du schéma ;
  • DML (Data Manipulation Language, langage de manipulation des données) : INSERT, UPDATE, DELETE, MERGE ;
  • historique, time travel et restauration ;
  • traitement batch et streaming sur la même table.

Chaque commit Delta publie atomiquement une nouvelle version de la table. Un lecteur commencé sur une version continue de voir un snapshot cohérent, sans mélange partiel avec un commit concurrent : c'est la snapshot isolation, souvent expliquée comme un mécanisme MVCC (Multi-Version Concurrency Control). Les écritures utilisent en plus une concurrence optimiste ; un conflit incompatible peut faire échouer une transaction. Mémo : lecteur = snapshot versionné ; écrivain = nouveau commit atomique.

SQL
DESCRIBE DETAIL production.sales.orders;
DESCRIBE HISTORY production.sales.orders;

SELECT *
FROM production.sales.orders VERSION AS OF 12;

SELECT *
FROM production.sales.orders TIMESTAMP AS OF '2026-07-01T10:00:00Z';

RESTORE TABLE production.sales.orders TO VERSION AS OF 12;

Version PySpark avec l'API Delta :

PYTHON
from delta.tables import DeltaTable

orders = DeltaTable.forName(spark, "production.sales.orders")
orders.detail().show(truncate=False)
orders.history().show(truncate=False)

old_version_df = (
    spark.read.option("versionAsOf", 12)
         .table("production.sales.orders")
)
orders.restoreToVersion(12)

SQL est plus direct pour l'inspection interactive ; l'API PySpark est utile pour intégrer l'historique ou la restauration à un programme contrôlé.

À retenir : RESTORE ne rembobine pas le journal. Il crée une nouvelle version dont l'état correspond à l'ancienne. VACUUM peut supprimer les fichiers nécessaires aux anciennes versions et donc rendre certains time travels impossibles.

Un overwrite ou un CREATE OR REPLACE TABLE Delta crée lui aussi un nouveau commit : il remplace l'état courant, mais n'efface pas immédiatement l'historique. Tant que le journal et les anciens fichiers requis sont retenus, un time travel ou un RESTORE reste possible. VACUUM peut ensuite supprimer physiquement les fichiers devenus des tombstones et rendre ces versions illisibles.

2.5 Architecture médaillon

Couche Finalité Opérations typiques
Bronze Conserver fidèlement les données sources et leur traçabilité Ingestion append, métadonnées de fichier, schéma permissif
Silver Nettoyer, typer, dédupliquer, joindre et appliquer la qualité Casts, gestion des NULL, règles métier, CDC (Change Data Capture)
Gold Fournir des données orientées consommation Agrégats, KPI, dimensions/faits, vues ou vues matérialisées pour la BI

La couche Bronze n'est pas « mauvaise », la Silver n'est pas nécessairement agrégée et la Gold ne contient pas forcément une seule table. Chaque couche a un contrat et un public différents.


3. Ingestion et chargement

3.1 Choisir le mode d'ingestion

Situation Outil à privilégier Pourquoi
Petit fichier local, action ponctuelle Upload UI vers un volume ou une table Simple, manuel, non automatisé
Import batch ponctuel piloté par code CTAS ou lecture DataFrame Crée une table à partir d'une requête
Nouveaux fichiers réguliers, volume modéré COPY INTO Incrémental, relançable et idempotent
Gros volume ou forte fréquence de fichiers cloud Connecteur standard Auto Loader Découverte scalable, checkpoint, schéma et reprise
Bus événementiel Kafka, faible latence Connecteur Kafka standard via Structured Streaming/Pipelines Lecture continue, offsets et reprise par checkpoint
Source standard nécessitant du code sur mesure Connecteur Lakeflow Connect standard Davantage de personnalisation dans Spark/Pipelines
SaaS ou base d'entreprise prise en charge Connecteur Lakeflow Connect managé Authentification, CDC, retries, évolution de schéma et monitoring gérés
Source non prise en charge Connecteur partenaire, JDBC/ODBC ou REST Intégration personnalisée, généralement orchestrée par un Job

Toujours partir de la couche la plus managée qui satisfait le besoin, puis descendre vers une solution personnalisée si nécessaire.

3.2 Ingestion ponctuelle : Upload UI et CTAS

L'Upload UI est ponctuel et manuel. CTAS signifie Create Table As Select et crée la table à partir du résultat d'une requête.

SQL
CREATE OR REPLACE TABLE dev.bronze.orders  -- Incorrect : CREATE TABLE dev.silver.orders_typed (order_id BIGINT)
AS
SELECT
  *,
  CAST(order_id AS BIGINT) AS order_id,
  CAST(order_ts AS TIMESTAMP) AS order_ts,
  _metadata.file_name AS source_file,
  _metadata.file_modification_time AS source_modified_at,
  current_timestamp() AS ingested_at
FROM read_files(
  '/Volumes/dev/landing/raw/orders',
  format => 'csv',
  header => true,
  inferSchema => true
);

CTAS relit la source quand la commande est relancée. Ce n'est pas, à lui seul, un mécanisme d'ingestion incrémentale qui mémorise les fichiers déjà traités.

Avec CTAS, le SELECT détermine les noms et types des colonnes. On ne peut pas fournir simultanément une déclaration manuelle de colonnes et AS SELECT : Pour imposer un contrat séparément, créer d'abord la table avec son schéma, puis utiliser INSERT, COPY INTO ou MERGE. CREATE TABLE LIKE copie seulement la définition d'une table existante.

Équivalent PySpark :

PYTHON
from pyspark.sql import functions as F

orders_df = (
    spark.read
         .format("csv")
         .option("header", True)
         .option("inferSchema", True)
         .load("/Volumes/dev/landing/raw/orders")
         .withColumn("source_file", F.col("_metadata.file_name"))
         .withColumn("ingested_at", F.current_timestamp())
)

(orders_df.write
          .format("delta")
          .mode("overwrite")  # append
          .saveAsTable("dev.bronze.orders"))

mode("overwrite") remplace les données ; mode("append") ajoute des lignes. Le choix doit être explicite et compatible avec l'idempotence attendue.

Enregistrer directement des fichiers CSV comme table externe

SQL
CREATE TABLE dev.raw.my_table (
  col1 STRING,
  col2 STRING
)
USING CSV
OPTIONS (
  header = 'true',
  delimiter = ';'
)
LOCATION 's3://company-data/input/my_table';

Cette commande enregistre les fichiers existants à cet emplacement, avec le schéma et les options CSV indiqués. Elle crée une table externe CSV ; elle ne copie pas les données et ne les convertit pas en Delta. Il n'y a donc pas les mêmes garanties ACID, DML et time travel qu'avec une table Delta.

3.3 Ingestion incrémentale SQL : COPY INTO

COPY INTO charge dans une table Delta les fichiers qui n'ont pas déjà été enregistrés comme traités. La commande est relançable et idempotente : sans nouveau fichier, un nouveau run n'ajoute normalement rien.

SQL
CREATE TABLE IF NOT EXISTS dev.bronze.orders (
  order_id BIGINT,
  customer_id BIGINT,
  order_ts TIMESTAMP,
  amount DECIMAL(12, 2)
);

COPY INTO dev.bronze.orders
FROM '/Volumes/dev/landing/raw/orders'
FILEFORMAT = CSV  -- JSON
PATTERN = '*.csv' -- Glob facultatif : limite les fichiers éligibles par leur nom/chemin.
FORMAT_OPTIONS (
  'header' = 'true',
  'delimiter' = ',',
  'inferSchema' = 'true',
  'mode' = 'FAILFAST'
  -- , 'modifiedAfter' = '2026-08-01T00:00:00Z'
  )
COPY_OPTIONS (
  'mergeSchema' = 'true'
  -- , 'force' = 'true'
  );
-- VALIDATE ALL;
-- VALIDATE 100 ROWS;

mergeSchema=true : nouvelles colonnes peuvent être ajoutées à la cible.

PATTERN est un glob de sélection de fichiers, pas une condition sur leur contenu ; l'omettre charge tous les fichiers éligibles sous la source. Ici, mode=FAILFAST est une option du lecteur CSV placée dans FORMAT_OPTIONS : la première ligne mal formée fait échouer le chargement au lieu d'être tolérée ou ignorée.

Pour nommer quelques fichiers exacts plutôt qu'un glob, remplacer PATTERN par FILES = ('orders_001.csv', 'archive/orders_002.csv'). La liste est limitée à 1 000 fichiers ; les noms sont relatifs au chemin de FROM. Utiliser FILES ou PATTERN, pas les deux.

modifiedAfter est une option de lecture du format, d'où sa présence dans FORMAT_OPTIONS. Elle filtre les fichiers sur leur métadonnée de modification ; elle ne filtre pas une colonne métier contenue dans le JSON. L'historique de COPY INTO continue ensuite à empêcher le rechargement des fichiers déjà traités, sauf force=true.

force=true désactive l'idempotence de COPY INTO et recharge les fichiers éligibles même s'ils figurent déjà dans l'historique de chargement. L'utiliser pour un replay explicitement contrôlé, avec une cible vidée/dédupliquée ou une stratégie d'upsert ; sinon il crée facilement des doublons.

VALIDATE permet de vérifier les fichiers et le schéma sans écrire dans la table cible : ALL valide toutes les lignes éligibles ; n ROWS limite le résultat de validation. C'est utile avant un premier chargement ou après un changement de format. Il s'agit d'une clause SQL de COPY INTO, sans équivalent propre dans DataFrameWriter.

À retenir :

  • la destination doit être une table Delta ;
  • les sources usuelles sont S3, ADLS, GCS ou un volume Unity Catalog ;
  • COPY INTO convient à une ingestion incrémentale simple ;
  • le pattern sûr suppose des fichiers source immuables et un nom unique par livraison : un fichier déjà chargé est ignoré lors des runs suivants, même si son contenu a été modifié ; seul force=true demande explicitement son rechargement ;
  • pour des millions de fichiers, une faible latence ou un schéma complexe, préférer Auto Loader.

Les privilèges Unity Catalog nécessaires dépendent notamment d'un accès par external location ou par volume ; ils sont regroupés avec le modèle de droits en 8.2.

COPY INTO est une commande SQL sans équivalent natif dans l'API DataFrame. Une lecture puis un append PySpark n'aurait pas le même suivi des fichiers déjà ingérés ; utiliser au besoin spark.sql("COPY INTO ..."), sans considérer cela comme une seconde implémentation.

Si les chargements créent beaucoup de petits fichiers, optimized writes et auto compaction peuvent en limiter la création côté table Delta ; ce ne sont pas des COPY_OPTIONS. Pour compacter des petits fichiers déjà écrits, lancer OPTIMIZE comme en 7.5.3.

3.4 Lakeflow Connect : connecteurs standards

Un connecteur standard identifie la source et ses capacités ; Structured Streaming est l'un des moteurs permettant de l'utiliser. Selon le niveau de contrôle recherché :

Niveau Contrôle et automatisation Interfaces usuelles
Structured Streaming Contrôle maximal du trigger, du checkpoint et du code Python ou Scala
Lakeflow Pipelines Déclarations, orchestration, qualité et monitoring gérés Python ou SQL
Databricks SQL Expérience SQL plus automatisée pour les sources prises en charge SQL

Avec Structured Streaming, une source incrémentale est généralement traitée sous forme de micro-batches successifs et sa progression est conservée dans un checkpoint. Deux sources standards sont particulièrement importantes pour l'examen :

Source Données découvertes Progression mémorisée
Auto Loader, format("cloudFiles") Nouveaux fichiers dans un stockage objet ou un volume Fichiers découverts/traités et commits dans le checkpoint
Kafka, format("kafka") Nouveaux messages des partitions du topic Offsets Kafka validés dans le checkpoint

Le trigger détermine quand les micro-batches sont lancés ; le sink reçoit leur résultat ; le checkpoint permet la reprise après une panne. Les transformations stateful, watermarks, output modes, foreachBatch et streaming joins sont détaillés dans la partie 4.

3.4.1 Auto Loader : ingestion incrémentale de fichiers

Auto Loader est la source Structured Streaming cloudFiles. Il découvre progressivement les fichiers et s'appuie sur un checkpoint pour mémoriser leur traitement.

Lecture rapide : le minimum commun est source Auto Loader + format des fichiers + chemin source + sink. Pour lire du JSON/CSV/XML avec Auto Loader PySpark autonome, ajouter aussi un schéma ou schemaLocation ; pour une exécution durable et reprenable, ajouter le checkpoint.

Version SQL dans un pipeline Lakeflow : read_files sous STREAM utilise Auto Loader ; Lakeflow gère schemaLocation, le checkpoint, le trigger et l'écriture dans la cible.

SQL
-- Sink (destination) de la requête
CREATE OR REFRESH STREAMING TABLE dev.bronze.orders
AS
SELECT *, _metadata.file_name AS source_file
FROM STREAM read_files(
  -- Source Auto Loader et format réel des fichiers
  '/Volumes/dev/landing/raw/orders',
  format => 'json',

  -- Deviner schéma selon échantillon
  inferColumnTypes => true,
  -- Indiquer schéma partiellement
  schemaHints => 'event_id BIGINT, customer.address.zip STRING, items.element.price DECIMAL(12,2), _corrupt_record STRING',
  -- Expliciter schéma (pas inferColumnTypes, schemaHints, addNewColumns) : .schema("event_id BIGINT, event_ts TIMESTAMP, payload STRING")
  -- schema => 'event_id BIGINT, event_ts TIMESTAMP, payload STRING',

  -- Si nouvelle colonne : addNewColumns, failOnNewColumns, rescue ou none
  schemaEvolutionMode => 'addNewColumns',
  -- Si schemaEvolutionMode=rescue
  rescuedDataColumn => '_rescued_data',

  -- Inclure les fichier existants (valable la 1ere fois)
  includeExistingFiles => true,

  -- Si lignes d'un json/csv mal formées : PERMISSIVE (isoler), DROPMALFORMED (supprime ligne), FAILFAST (arrête lecture)
  -- Si PERMISSIVE (mettre schemaHints : _corrupt_record STRING)
  mode => 'PERMISSIVE',
  columnNameOfCorruptRecord => '_corrupt_record',

  -- Reprendre des fichiers source modifiés
  allowOverwrites => true,

  -- Pour File notification/file events
  useManagedFileEvents => true,

  -- Si problème de débit/mémoire/durée des batchs 
  maxFilesPerTrigger => 500,
  maxBytesPerTrigger => '10g'
);

-- Alternative contrôlée hors gestion automatique Lakeflow :
-- ALTER TABLE dev.bronze.orders ADD COLUMNS (phone STRING);

Version PySpark Structured Streaming autonome :

PYTHON
raw_df = (
    spark.readStream
         # Source Auto Loader et format réel des fichiers
         .format("cloudFiles")
         .option("cloudFiles.format", "json")
         .option("cloudFiles.schemaLocation", "/Volumes/dev/checkpoints/orders_schema")

         # Deviner schéma selon échantillon
         .option("cloudFiles.inferColumnTypes", "true")
         # Indiquer schéma partiellement
         .option(
             "cloudFiles.schemaHints",
             "event_id BIGINT, customer.address.zip STRING, "
             "items.element.price DECIMAL(12,2), _corrupt_record STRING",
         )
         # Expliciter schéma (pas inferColumnTypes, schemaHints, addNewColumns) : .schema("event_id BIGINT, event_ts TIMESTAMP, payload STRING")

         # Si nouvelle colonne : addNewColumns, failOnNewColumns, rescue ou none
         .option("cloudFiles.schemaEvolutionMode", "addNewColumns")
         ## Si schemaEvolutionMode=rescue
         .option("rescuedDataColumn", "_rescued_data")

         # Inclure les fichier existants (valable la 1ere fois)
         .option("cloudFiles.includeExistingFiles", "true")

         # Si lignes d'un json/csv mal formées : PERMISSIVE (isoler), DROPMALFORMED (supprime ligne), FAILFAST (arrête lecture)
         ## Si PERMISSIVE (mettre schemaHints : _corrupt_record STRING)
         .option("mode", "PERMISSIVE")
         .option("columnNameOfCorruptRecord", "_corrupt_record")

         # Reprendre des fichiers source modifiés
         .option("cloudFiles.allowOverwrites", "true")

         # Pour File notification/file events
         .option("cloudFiles.useManagedFileEvents", "true")

         # Si problème de débit/mémoire/durée des batchs 
         .option("cloudFiles.maxFilesPerTrigger", 500)
         .option("cloudFiles.maxBytesPerTrigger", "10g")

         # Chemin source (placé à la fin par la syntaxe DataFrame)
         .load("/Volumes/dev/landing/raw/orders")
)

query = (
    raw_df.writeStream
          # Chemin du checkpoint
          .option("checkpointLocation", "/Volumes/dev/checkpoints/orders_data")
          # Exécution : AvailableNow ou processingTime="1 minute"
          .trigger(availableNow=True)
          # Si la table Delta doit évoluer
          .option("mergeSchema", "true")
          # Sink (destination) de la requête
          .toTable("dev.bronze.orders")
)

1. Définir la source et son format

.format("cloudFiles") sélectionne Auto Loader, cloudFiles.format indique le format réel et .load(...) fournit le chemin source. Les formats usuels sont JSON, CSV, XML, Parquet, Avro, ORC, texte et binaryFile. En SQL Lakeflow, les équivalents sont STREAM read_files(path, format => ...) et CREATE OR REFRESH STREAMING TABLE.

2. Construire et conserver le schéma lu

schemaLocation conserve le schéma découvert et son historique d'évolution, pas la progression des fichiers. Il doit être durable et propre à la lecture ; Lakeflow en gère automatiquement l'emplacement.

Pour construire le schéma du DataFrame source, choisir une stratégie cohérente :

  • un schema complet constitue un contrat strict et remplace l'inférence ;
  • sans schéma explicite, Auto Loader PySpark infère par défaut les colonnes JSON, CSV et XML en STRING ; cloudFiles.inferColumnTypes=true demande une inférence typée sur un échantillon ;
  • schemaHints impose seulement les types critiques, y compris dans une structure imbriquée (customer.address.zip) ou un élément d'array (items.element.price). Un hint guide la lecture ; ce n'est pas un CAST métier.

L'inférence reste un échantillonnage, pas un contrat. Parquet porte déjà ses types. Lorsqu'un champ connu du schéma unifié manque dans un document, sa valeur est NULL pour cette ligne.

3. Choisir la politique d'évolution et les rescued data

Choisir un seul cloudFiles.schemaEvolutionMode :

Mode Nouvelle colonne détectée
addNewColumns L'ajoute au schéma de schemaLocation, arrête le stream, puis un redémarrage reprend avec le schéma élargi
failOnNewColumns Échoue sans modifier le schéma ; un retry seul échoue à nouveau
rescue Continue et place la colonne dans _rescued_data
none Ignore la colonne, sauf si une rescued data column est configurée

Avec addNewColumns, les anciennes lignes valent NULL pour la nouvelle colonne ; le mode n'élargit pas arbitrairement les types existants. _rescued_data peut aussi conserver des champs hors schéma, des différences de casse ou des valeurs incompatibles avec le type attendu. Une incompatibilité imbriquée se traite avec un schéma explicite, des schemaHints, les rescued data ou une normalisation Silver.

4. Décider du chargement des fichiers déjà présents

cloudFiles.includeExistingFiles n'est évalué qu'au premier démarrage associé au checkpoint : true charge le backlog puis les nouveaux fichiers ; false ne traite que les fichiers arrivant après l'initialisation. Modifier cette option en conservant le checkpoint n'a aucun effet.

5. Capturer les documents mal formés

mode concerne les documents JSON ou CSV réellement mal formés :

Mode de parsing Comportement
PERMISSIVE Conserve la ligne ; son contenu brut va dans _corrupt_record, les champs non récupérables valent généralement NULL
DROPMALFORMED Écarte la ligne mal formée
FAILFAST Arrête immédiatement la lecture

Pour utiliser columnNameOfCorruptRecord='_corrupt_record', cette colonne doit être déclarée en STRING, comme dans les schemaHints de l'exemple. Ne pas la confondre avec _rescued_data : la première vise une syntaxe illisible, la seconde des données lisibles mais hors schéma ou hors type. badRecordsPath peut isoler les erreurs dans un répertoire, mais une colonne de corruption conserve plus simplement le lien avec la ligne ingérée.

6. Décider du retraitement des fichiers écrasés

Auto Loader suppose normalement des fichiers immuables et cloudFiles.allowOverwrites vaut false par défaut. Avec true, un fichier modifié au même chemin peut être relu en entier. Un sink append ne supprime pas son ancienne version : il peut donc produire des doublons et nécessite une déduplication ou un MERGE idempotent.

7. Choisir le mode de découverte des fichiers

Mode Usage
Directory listing Simple ; reliste le stockage et devient plus coûteux à très grande échelle
Managed file events Recommandé pour les gros volumes et la faible latence lorsque l'external location prend en charge les événements
Classic file notification File dédiée à chaque stream ; davantage de ressources cloud à administrer

cloudFiles.useManagedFileEvents=true utilise le service de file events mutualisé d'une external location sur laquelle ces événements ont été activés. cloudFiles.useNotifications=true choisit au contraire le mode classique, avec une infrastructure de notification propre au stream ; ne pas combiner les deux options.

Le mode classique nécessite les permissions cloud permettant de créer, configurer et consommer les ressources d'événements : SNS/SQS sur AWS, Event Grid/Queue Storage sur Azure ou topics/subscriptions Pub/Sub sur GCP. Sur AWS, le chemin est événement S3 → SNS → SQS → Auto Loader : SNS (Simple Notification Service) diffuse l'événement et SQS (Simple Queue Service) le conserve jusqu'à sa consommation. La notification contient les métadonnées du fichier, pas son contenu.

Si des fichiers manquent, vérifier les préfixes et suffixes configurés, SNS/SQS, IAM et la région. Un retry SQS ne doit pas, à lui seul, dupliquer les lignes avec un checkpoint stable ; contrôler plutôt le checkpoint, allowOverwrites, l'immuabilité de la source et l'idempotence du sink.

8. Régler l'admission de chaque micro-batch

Ces options sont secondaires et se règlent après observation d'un problème de débit, de mémoire ou de durée :

  • maxFilesPerTrigger est une borne dure sur le nombre de fichiers ;
  • maxBytesPerTrigger est une borne souple sur leur volume et peut être dépassée pour admettre au moins un fichier ;
  • si les deux sont définies, le premier seuil atteint borne le batch.

Sur une streaming table exécutée par un SQL warehouse serverless, laisser de préférence l'admission dynamique dimensionner les batches.

9. Conserver la progression dans le checkpoint

checkpointLocation répond à « jusqu'où la requête est-elle arrivée ? ». Il conserve les fichiers ou offsets lus, les commits validés dans le sink et l'état des opérations stateful. Après une panne, Spark reprend après le dernier micro-batch validé.

  • Chaque requête doit avoir son propre checkpoint durable.
  • Le supprimer ou changer son chemin crée une nouvelle requête, avec un risque de retraitement et de doublons.
  • Changer de source, de sink ou la structure d'une opération stateful exige généralement un nouveau checkpoint ; modifier un filtre, un trigger ou une limite d'admission est généralement compatible.
  • Un checkpoint stable et un sink Delta transactionnel permettent une application exactly-once des fichiers découverts. foreachBatch reste at-least-once par défaut et doit être idempotent.

Ne pas confondre les deux états persistés :

État Contenu Effet d'une suppression ou d'un changement de chemin
schemaLocation Schéma inféré et historique d'évolution Réinférence du schéma ; ne réinitialise pas directement le suivi des fichiers
checkpointLocation Progression, commits et état du stream Nouvelle requête et possible retraitement des données disponibles

Ils peuvent partager une racine, mais doivent utiliser des sous-répertoires distincts. Des doublons après une évolution ne se corrigent donc pas en supprimant schemaLocation : vérifier d'abord le checkpoint, allowOverwrites et l'idempotence du sink.

10. Choisir le trigger PySpark

Une requête utilise au plus un trigger explicite :

  • .trigger(availableNow=True) draine le backlog présent au démarrage en autant de micro-batches que nécessaire, en respectant les limites d'admission, puis s'arrête. Les fichiers arrivés ensuite attendent le prochain run ;
  • .trigger(processingTime="1 minute") laisse la requête active et tente de lancer un micro-batch chaque minute. Si un batch dure davantage, le suivant démarre dès que possible ;
  • sans .trigger(...), Spark lance par défaut les micro-batches aussi vite que possible.

Ces appels concernent un writeStream autonome. En SQL Lakeflow, choisir un pipeline triggered ou continuous, puis configurer sa planification dans le pipeline ou Lakeflow Jobs.

11. Faire évoluer le schéma cible et écrire dans le sink

Auto Loader et la table Delta cible gèrent deux schémas indépendants :

TEXT
schema / schemaHints / schemaEvolutionMode
        → schéma lu par Auto Loader et produit dans le DataFrame

ALTER TABLE / mergeSchema
        → schéma de la table Delta cible à l'écriture

Delta applique le schema enforcement à l'écriture. Une colonne inconnue ou un type non convertible fait échouer l'opération. Si Auto Loader ajoute une colonne, préparer la cible avec ALTER TABLE ou autoriser son évolution avec mergeSchema=true. Omettre mergeSchema lorsque le contrat doit rester strict ; il est inutile si les schémas sont déjà compatibles ou si Lakeflow gère directement sa streaming table. Il ne corrige jamais une erreur de parsing en amont.

Le sink est la destination de la requête. Dans l'exemple, .toTable("dev.bronze.orders") écrit dans une table Delta. Choisir SQL Lakeflow pour une ingestion déclarative ; choisir Structured Streaming autonome lorsqu'il faut contrôler directement le checkpoint, le trigger ou une logique programmatique. Dans les deux cas, Auto Loader ne garantit pas l'ordre de découverte des fichiers.

3.4.2 Fichiers non structurés avec binaryFile

Pour les fichiers non structurés, binaryFile crée une ligne par fichier avec notamment son chemin, sa date de modification, sa longueur et son contenu binaire. C'est utile pour gouverner des images ou documents avant extraction.

Version SQL dans un pipeline :

SQL
CREATE OR REFRESH STREAMING TABLE dev.bronze.invoice_images AS
SELECT *
FROM STREAM read_files(
  's3://shop/raw/invoices/',
  format => 'binaryFile',
  pathGlobFilter => '*.jpg'
);

Version PySpark :

PYTHON
images_df = (
    spark.readStream
         .format("cloudFiles")
         .option("cloudFiles.format", "binaryFile")
         .option("pathGlobFilter", "*.jpg")
         .load("s3://shop/raw/invoices/")
)

pathGlobFilter sélectionne les noms de fichiers sans parcourir le contenu. Conserver la colonne content seulement si elle est réellement nécessaire, car elle peut être volumineuse.

3.4.3 Kafka : ingestion événementielle

Kafka est une source événementielle lue avec Structured Streaming. Contrairement à Auto Loader, qui découvre des fichiers, Kafka fournit des messages ordonnés à l'intérieur de chaque partition et suivis par offsets.

Version SQL dans un pipeline :

SQL
CREATE OR REFRESH STREAMING TABLE dev.bronze.kafka_events AS
SELECT *
FROM STREAM read_kafka(
  bootstrapServers => 'host:port',
  subscribe => 'events_topic',
  startingOffsets => 'latest'
);

Version PySpark :

PYTHON
events_stream = (
    spark.readStream
         .format("kafka")
         .option("kafka.bootstrap.servers", "host:port")
         .option("subscribe", "events_topic")
         .option("startingOffsets", "latest")
         .load()
)

La source contient notamment key et value en BINARY, ainsi que topic, partition, offset et timestamp. Il faut généralement décoder value, puis appliquer from_json ou parse_json lors de la transformation Bronze vers Silver. startingOffsets = latest ne lit que les nouveaux messages lors du premier démarrage ; après création du checkpoint, un redémarrage reprend les offsets enregistrés et ignore cette option initiale. Utiliser exactement une stratégie parmi subscribe, subscribePattern et assign.

Pour un topic à fort débit — par exemple 10 000 événements/s — avec une latence attendue inférieure à une minute, choisir Structured Streaming de Kafka vers Delta, avec un trigger adapté et un checkpoint durable. Une lecture batch JDBC, COPY INTO ou un Job quotidien ne répond pas à ce besoin. Dimensionner ensuite les partitions Kafka et le compute d'après le débit réel ; le seul choix de l'API ne garantit pas le SLA.

3.5 Lakeflow Connect : connecteurs managés

Un connecteur managé crée un pipeline d'ingestion depuis l'UI, une API ou un bundle pour une source prise en charge. Databricks gère davantage :

  • l'authentification et les particularités de la source ;
  • la capture incrémentale ou le CDC lorsqu'il est proposé ;
  • les retries, la reprise et le cycle de vie du pipeline ;
  • l'évolution de schéma prise en charge ;
  • la maintenance des API, les cas limites et le monitoring.

Au premier run, le connecteur effectue généralement un snapshot initial des objets sélectionnés, puis charge les changements de façon incrémentale lorsque la source et l'objet le permettent. La technique exacte dépend du connecteur : CDC/change tracking pour certaines bases, ou curseur/high-water mark pour certaines API. Il ne faut donc pas généraliser « tous les connecteurs font du CDC ».

Pour un connecteur SaaS/API pris en charge, Databricks encapsule notamment la pagination, les limites d'appel, les retries et les évolutions de schéma que ce connecteur sait gérer. Ce bénéfice ne s'applique pas automatiquement à une API REST personnalisée ni de façon identique à toutes les sources.

Les connecteurs managés couvrent notamment des applications SaaS et des bases d'entreprise. Il existe aussi des connecteurs de streaming managés : par exemple, le connecteur Kafka managé, actuellement en bêta, utilise une CONNECTION Unity Catalog et écrit dans des streaming tables.

Le connecteur PostgreSQL managé peut exploiter la réplication logique et le WAL pour capturer les changements en CDC et les appliquer à des streaming tables, lorsqu'il est disponible dans le workspace.

Lakeflow Connect ou Lakehouse Federation

  • Lakeflow Connect copie les données dans des tables Delta Unity Catalog afin de les historiser, transformer et servir localement.
  • Lakehouse Federation interroge la source distante sans l'ingérer. Une CONNECTION Unity Catalog conserve le type de source, l'hôte et les informations d'authentification ; un foreign catalog expose ensuite ses objets dans le namespace à trois niveaux.

La création de la CONNECTION, du FOREIGN CATALOG et l'exposition des tables étrangères sont détaillées en 2.3.2. Mémo : Connect ingère une copie locale ; Federation interroge la source sans la déplacer.

3.6 Sources personnalisées : JDBC, ODBC et REST

Spark peut lire une base relationnelle en JDBC :

PYTHON
jdbc_df = (
    spark.read
         .format("jdbc")
         .option("url", "jdbc:postgresql://host:5432/app")
         .option("dbtable", "public.orders")
         .option("user", dbutils.secrets.get("ingestion", "user"))
         .option("password", dbutils.secrets.get("ingestion", "password"))
         .option("driver", "org.postgresql.Driver")
         .load()
)

jdbc_df.write.mode("append").saveAsTable("dev.bronze.orders_jdbc")

Le nom passé à option("driver", ...) désigne une classe qui doit être disponible sur le classpath. Si le runtime ne fournit pas le pilote, l'ajouter à la task Lakeflow Job dans Dependent libraries — de préférence par coordonnées Maven, ou comme JAR — afin qu'il soit installé avant l'exécution. Avec un compute standard, le chemin ou les coordonnées peuvent devoir figurer dans l'allowlist ; sur serverless, configurer Environment and Libraries ou utiliser une connexion JDBC Unity Catalog lorsqu'elle est prise en charge. Une ClassNotFoundException sur le driver indique d'abord une dépendance absente, pas une panne réseau.

Le connecteur Spark JDBC standard est une source batch : il utilise spark.read, pas spark.readStream. Déclarer sa sortie comme streaming table ne rend pas la source JDBC continue. Dans un pipeline déclaratif, une lecture JDBC standard alimente donc plutôt une materialized view ou une étape batch planifiée.

Une erreur connection timeout signifie qu'aucune connexion n'a été établie dans le délai ; elle oriente d'abord vers le réseau ou une adresse incorrecte, pas vers le mot de passe. Vérifier le host, le port, le DNS, les routes, le peering/VPN et les security groups/NSG/firewalls autorisant le trafic depuis le compute ou la passerelle Databricks vers MySQL, généralement sur le port 3306. Selon l'architecture, il faut autoriser les plages IP adaptées ou mettre en place une connectivité privée ; « ajouter l'IP du workspace » n'est donc pas une règle universelle. Une erreur Access denied relève au contraire des credentials ou privilèges MySQL.

Une ingestion JDBC personnalisée peut devenir incrémentale par runs batch en conservant un curseur, par exemple updated_at, puis en ne demandant que les lignes postérieures au dernier curseur validé. La requête exécutée par la base ressemble à :

SQL
SELECT order_id, amount, updated_at
FROM public.orders
WHERE updated_at > TIMESTAMP '2026-08-16 10:00:00'
ORDER BY updated_at, order_id;
PYTHON
last_cursor = "2026-08-16 10:00:00"
incremental_query = f"""
  SELECT order_id, amount, updated_at
  FROM public.orders
  WHERE updated_at > TIMESTAMP '{last_cursor}'
"""

changes_df = (
    spark.read.format("jdbc")
         .option("url", "jdbc:postgresql://host:5432/app")
         .option("query", incremental_query)
         .option("user", dbutils.secrets.get("ingestion", "user"))
         .option("password", dbutils.secrets.get("ingestion", "password"))
         .load()
)

Persister le nouveau curseur seulement après une écriture cible réussie et idempotente. Un timestamp seul peut avoir des ex æquo : utiliser au besoin un curseur composite (updated_at, order_id), prévoir un petit chevauchement puis un MERGE, et traiter explicitement les suppressions. Lorsqu'un connecteur Lakeflow managé avec CDC ou cursor_column existe, il est préférable à ce polling artisanal.

Si la source ne possède aucune colonne incrémentale fiable, le fallback est un snapshot batch complet vers une table de staging, une déduplication déterministe par clé, puis un MERGE dans la cible. Cette solution est correcte mais relit toute la source ; elle doit donc rester un compromis. Les suppressions nécessitent en plus une information CDC ou, avec beaucoup de prudence, une synchronisation par WHEN NOT MATCHED BY SOURCE THEN DELETE :

SQL
MERGE INTO dev.silver.orders AS target
USING dev.staging.orders_snapshot_deduplicated AS source
ON target.order_id = source.order_id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *
WHEN NOT MATCHED BY SOURCE THEN DELETE;

En PySpark, lire le snapshot avec spark.read.format("jdbc"), le dédupliquer avec une fenêtre comme en 4.1.4, puis appliquer le même MERGE via DeltaTable. Ne jamais ajouter la clause de suppression si le snapshot peut être partiel.

Sans options de partitionnement, une lecture JDBC Spark utilise une seule partition et une seule connexion : elle peut devenir le goulot d'étranglement d'une grosse extraction. Pour paralléliser, fournir ensemble une colonne numérique/date/timestamp et ses bornes (partitionColumn, lowerBound, upperBound, numPartitions). Les bornes calculent les plages mais ne filtrent pas les lignes ; numPartitions borne aussi le nombre de connexions JDBC concurrentes. Trop de partitions peuvent surcharger la base source.

ODBC est surtout utilisé par des applications clientes et outils BI pour se connecter à un SQL warehouse. Un client Python ODBC peut aussi extraire une petite source et convertir les résultats en DataFrame, mais JDBC distribué ou un connecteur Lakeflow est généralement préférable pour les gros volumes.

Pour une API REST propriétaire sans connecteur et un petit payload quotidien — par exemple moins de 1 Mo — un script Python exécuté dans un notebook ou une task Job est pragmatique. L'appel HTTP se fait sur le driver : valider le statut et le schéma, puis écrire la réponse brute ou normalisée en Bronze.

PYTHON
import requests
from pyspark.sql import types as T

orders_schema = T.StructType([
    T.StructField("order_id", T.LongType(), False),
    # Bronze : type simple et robuste ; convertir en DECIMAL dans Silver si nécessaire.
    T.StructField("amount", T.DoubleType(), True),
])

token = dbutils.secrets.get("ingestion", "crm_api_token")
response = requests.get(
    "https://api.example.com/orders",
    headers={"Authorization": f"Bearer {token}"},
    timeout=30,
)
response.raise_for_status()

orders_df = spark.createDataFrame(response.json()["orders"], orders_schema)
(orders_df.write
          .format("delta")
          .mode("append")
          .saveAsTable("dev.bronze.orders_api"))

Pour la production, gérer pagination, rate limits, backoff exponentiel, validation et idempotence dans Lakeflow Jobs. Ne jamais mettre le token en clair dans le notebook, Git ou les logs. Si les volumes deviennent importants, l'API doit être extraite par pages vers du stockage objet plutôt que parallélisée aveuglément depuis les executors.


4. Transformation et modélisation

4.1 De Bronze à Silver

L'objectif est de transformer une donnée brute et traçable en donnée fiable, typée et réutilisable. Les fondamentaux Spark ci-dessous s'appliquent aussi aux autres couches ; ils sont présentés ici car Bronze → Silver constitue généralement la première transformation substantielle.

4.1.1 Fondamentaux Spark et choix de l'interface

  • Un DataFrame est distribué et immuable : une transformation retourne un nouveau DataFrame.
  • Les transformations sont généralement lazy. Elles construisent un plan ; une action comme count, collect, write ou display déclenche l'exécution.
  • Les fonctions Spark natives sont préférables aux UDF Python : elles sont visibles par l'optimiseur et évitent souvent des coûts de sérialisation.
  • collect() est une action qui ramène toutes les lignes du DataFrame sous forme d'objets Python dans la mémoire du driver. toPandas() fait de même dans un DataFrame pandas. Les réserver à de petits résultats explicitement bornés, sinon le driver peut subir un OOM.
  • Un shuffle est une redistribution des données entre partitions et executors, généralement par clé. Il implique réseau, sérialisation et parfois disque ; groupBy, distinct, les tris et la plupart des joins peuvent en provoquer un.

Imports usuels :

PYTHON
from pyspark.sql import functions as F
from pyspark.sql import types as T
from pyspark.sql.window import Window

Raccourci de choix : SQL est généralement le plus lisible pour les transformations déclaratives, agrégations, vues et objets de pipeline ; PySpark est préférable pour modulariser en fonctions, tester, utiliser boucles/conditions ou des bibliothèques Python. Les deux utilisent le moteur Spark et peuvent coexister dans un pipeline, dans des fichiers distincts.

Fonctions natives et UDF

Une UDF (User-Defined Function, fonction définie par l'utilisateur) encapsule une logique réutilisable lorsque les fonctions intégrées ne suffisent pas. Une fonction SQL Unity Catalog peut être gouvernée et appelée comme une fonction native :

SQL
CREATE OR REPLACE FUNCTION dev.utils.plus_one(value INTEGER)
RETURNS INTEGER
RETURN value + 1;

SELECT dev.utils.plus_one(5);  -- 6

Les opérations arithmétiques propagent NULL : plus_one(NULL) retourne donc NULL. Préférer une expression native telle que value + 1 lorsqu'elle n'a pas besoin d'être centralisée, car les fonctions natives sont plus simples à optimiser. En PySpark, le même calcul ne nécessite pas de Python UDF :

PYTHON
result_df = source_df.withColumn("value_plus_one", F.col("value") + 1)

Pour exécuter une fonction Unity Catalog, un principal a besoin de USE CATALOG, USE SCHEMA et EXECUTE sur la fonction.

coalesce() ou repartition()
Opération Shuffle Peut augmenter le nombre de partitions ? Usage
df.coalesce(n) Généralement non, transformation étroite Non en pratique Réduire rapidement le nombre de partitions après un gros filtrage
df.repartition(n, *cols) Oui, redistribution complète Oui Rééquilibrer, augmenter/réduire le parallélisme ou répartir par clé

Version SQL avec des hints de partitionnement :

SQL
SELECT /*+ COALESCE(20) */ *
FROM dev.silver.orders;

SELECT /*+ REPARTITION(80, customer_id) */ *
FROM dev.silver.orders;

Version PySpark :

PYTHON
# 200 -> 20 partitions, peu coûteux mais potentiellement déséquilibré.
fewer_df = filtered_df.coalesce(20)

# Shuffle vers 80 partitions réparties selon customer_id.
balanced_df = filtered_df.repartition(80, "customer_id")

Attention au vocabulaire : df.coalesce(20) agit sur les partitions ; la fonction SQL coalesce(country, 'UNKNOWN') retourne la première valeur non NULL. Même nom, objectif différent. Éviter aussi coalesce(1)/repartition(1) sur un gros volume : une seule task devient le goulot d'étranglement.

4.1.2 Nettoyage, typage et normalisation

Nettoyer Bronze vers Silver

Version SQL :

SQL
CREATE OR REPLACE TABLE dev.silver.orders AS
SELECT
  CAST(order_id AS BIGINT) AS order_id,
  to_timestamp(order_ts) AS order_ts,
  CAST(amount AS DECIMAL(12, 2)) AS amount,
  upper(trim(coalesce(country, 'UNKNOWN'))) AS country,
  CASE
    WHEN amount >= 1000 THEN 'HIGH'
    WHEN amount >= 100 THEN 'MEDIUM'
    ELSE 'LOW'
  END AS amount_band,
  customer AS customer_id
FROM dev.bronze.orders
WHERE order_id IS NOT NULL;

Version PySpark :

PYTHON
silver_df = (
    spark.table("dev.bronze.orders")
         .filter(F.col("order_id").isNotNull())
         .withColumn("order_id", F.col("order_id").cast("long"))
         .withColumn("order_ts", F.to_timestamp("order_ts"))
         .withColumn("amount", F.col("amount").cast("decimal(12,2)"))
         .withColumn("country", F.upper(F.trim("country")))
         .withColumn(
             "amount_band",
             F.when(F.col("amount") >= 1000, F.lit("HIGH"))
              .when(F.col("amount") >= 100, F.lit("MEDIUM"))
              .otherwise(F.lit("LOW"))
         )
         .drop("temporary_column")
         .withColumnRenamed("customer", "customer_id")
)

(silver_df.write
          .format("delta")
          .mode("overwrite")
          .saveAsTable("dev.silver.orders"))

Pour normaliser un e-mail, appliquer TRIM avant LOWER ; les deux fonctions conservent NULL :

SQL
SELECT
  user_id,
  lower(trim(email)) AS email
FROM dev.bronze.users;
PYTHON
from pyspark.sql import functions as F

users_df = spark.table("dev.bronze.users")
clean_users_df = users_df.withColumn(
    "email",
    F.lower(F.trim(F.col("email"))),
)

Pour construire une chaîne avec un séparateur, concat_ws ignore automatiquement les arguments NULL. Il évite donc un séparateur inutile lorsque, par exemple, le deuxième prénom est absent ; une chaîne vide reste toutefois différente de NULL.

SQL
SELECT concat_ws(
         ' ',
         trim(first_name),
         trim(middle_name),
         trim(last_name)
       ) AS full_name
FROM dev.bronze.users;
PYTHON
users_df = users_df.withColumn(
    "full_name",
    F.concat_ws(
        " ",
        F.trim("first_name"),
        F.trim("middle_name"),
        F.trim("last_name"),
    ),
)

Pour remplacer ou supprimer des caractères individuels, translate évite une expression régulière : chaque caractère de la deuxième chaîne correspond à celui de même position dans la troisième ; si cette dernière est plus courte, les caractères sans correspondance sont supprimés.

SQL
SELECT translate(phone, ' ()-+', '') AS phone_digits
FROM dev.bronze.customers;
PYTHON
customers_df = customers_df.withColumn(
    "phone_digits",
    F.translate("phone", " ()-+", ""),
)

Utiliser plutôt replace pour une sous-chaîne exacte et regexp_replace pour un motif.

Dans l'exemple de nettoyage précédent, withColumn ajoute une colonne ou remplace celle qui porte déjà le même nom ; when(...).otherwise(...) est l'équivalent DataFrame de CASE WHEN ... ELSE ... END. Pour renommer toutes les colonnes d'un coup, toDF applique les nouveaux noms par position et exige exactement autant de noms que le DataFrame possède de colonnes :

SQL
SELECT *, 'CRM_PROD' AS source_system
FROM dev.bronze.customers;
PYTHON
tagged_df = customers_df.withColumn(
    "source_system",
    F.lit("CRM_PROD"),
)

F.lit("CRM_PROD") construit une valeur constante pour chaque ligne ; F.col("CRM_PROD") chercherait au contraire une colonne portant ce nom.

SQL
SELECT
  id AS order_id,
  customer AS customer_id,
  total AS amount
FROM dev.bronze.orders;
PYTHON
renamed_df = source_df.toDF("order_id", "customer_id", "amount")

Pour une seule colonne, préférer withColumnRenamed ou un alias dans select, plus explicite et moins fragile si l'ordre du schéma change.

selectExpr permet d'écrire des expressions SQL sous forme de chaînes dans une transformation DataFrame. Il est pratique pour une projection courte avec casts et alias :

SQL
SELECT CAST(reading_value AS DOUBLE) AS sensor_value
FROM dev.bronze.sensors;
PYTHON
sensors_df = (
    spark.read.table("dev.bronze.sensors")
         .selectExpr("CAST(reading_value AS DOUBLE) AS sensor_value")
)

Comme SELECT, selectExpr ne conserve que les colonnes énumérées. Utiliser plutôt .select(F.col("reading_value").cast("double").alias("sensor_value")) lorsque l'on souhaite une API PySpark typée, et TRY_CAST/try_cast si une valeur mal formée doit devenir NULL au lieu de faire échouer le traitement.

Gestion des valeurs manquantes :

PYTHON
# Supprime les lignes sans identifiant ; remplit uniquement certaines colonnes.
clean_df = (
    source_df.na.drop(subset=["order_id"])
             .na.fill({"country": "UNKNOWN", "quantity": 0})
)

# Autre règle possible : conserver les lignes ayant au moins 8 valeurs définies.
complete_enough_df = source_df.na.drop(thresh=8)

dropna et na.drop sont des alias. dropna(thresh=8) conserve uniquement les lignes qui possèdent au moins huit valeurs non nulles ; thresh prend le pas sur how. Le paramètre subset=[...] limite les colonnes examinées. En SQL, il n'existe pas de méthode dropna : écrire explicitement le prédicat WHERE correspondant. NULL n'est ni zéro ni chaîne vide ; utiliser IS NULL, IS NOT NULL et coalesce.

Conversions tolérantes avec TRY_CAST

CAST fait normalement échouer la requête si une valeur ne peut pas être convertie. TRY_CAST retourne plutôt NULL lorsque la combinaison de types est autorisée, mais que la valeur est mal formée ou hors plage :

SQL
SELECT TRY_CAST('100$' AS INT) AS amount;  -- NULL
SELECT TRY_CAST('100' AS INT)  AS amount;  -- 100

Équivalent dans une transformation PySpark :

PYTHON
typed_df = raw_df.withColumn(
    "amount",
    F.expr("TRY_CAST(raw_amount AS INT)"),
)

Tester ensuite amount IS NULL permet de mettre les lignes invalides en quarantaine. TRY_CAST ne nettoie pas la chaîne : retirer explicitement un symbole avec regexp_replace seulement si le contrat métier l'autorise.

Timestamps : utiliser TIMESTAMP et normaliser en UTC

Stocker un instant en type TIMESTAMP, pas en chaîne formatée, et normaliser les sources en UTC. Le type reste triable, comparable et utilisable dans les fenêtres, watermarks et agrégations temporelles. Un TIMESTAMP Spark représente un instant ; son affichage dépend du fuseau de la session. Fixer la session en UTC rend les conversions et les résultats reproductibles :

SQL
SET TIME ZONE 'UTC';

SELECT
  CAST('2026-08-10T14:30:00Z' AS TIMESTAMP) AS event_ts_utc,
  to_utc_timestamp(
    to_timestamp(local_event_ts, 'yyyy-MM-dd HH:mm:ss'),
    'Europe/Paris'
  ) AS normalized_event_ts
FROM dev.bronze.events;
PYTHON
from pyspark.sql import functions as F

spark.conf.set("spark.sql.session.timeZone", "UTC")

events_typed_df = events_df.withColumn(
    "event_ts",
    F.to_utc_timestamp(
        F.to_timestamp("local_event_ts", "yyyy-MM-dd HH:mm:ss"),
        "Europe/Paris",
    ),
)

Si la chaîne source contient déjà un offset ou Z, la caster directement en TIMESTAMP suffit généralement : ne pas appliquer une seconde conversion de fuseau. Utiliser TIMESTAMP_NTZ seulement pour une heure civile volontairement sans instant universel, par exemple « ouverture du magasin à 09:00 », et non pour un événement distribué.

4.1.3 Données semi-structurées et collections

Décoder les données semi-structurées

Le parcours Bronze → Silver est : identifier le type source → décoder si nécessaire → choisir STRUCT, VARIANT ou une extraction ponctuelle → typer les champs métier.

1. Identifier la source et choisir la stratégie Silver
Type source Besoin Traitement Type obtenu
BINARY — par exemple value depuis Kafka Obtenir le texte JSON decode(value, 'UTF-8') JSON STRING à traiter selon les lignes suivantes
JSON STRING Schéma stable, plusieurs champs from_json, puis accès struct.field Type complexe, puis type déclaré du champ
JSON STRING Schéma flexible à conserver et réinterroger Persister parse_json ou try_parse_json en Silver VARIANT réutilisable
JSON STRING Schéma flexible avec quelques champs souvent utilisés Colonnes typées + document complet en VARIANT Colonnes typées + VARIANT
JSON STRING Un ou deux champs dans une requête ponctuelle payload_json:field ou get_json_object STRING ; parsing implicite à chaque requête
VARIANT Extraire et typer un champ payload:field, puis cast VARIANT, puis type métier demandé
STRUCT, ARRAY ou MAP Exploiter une donnée déjà typée Accès direct aux champs Type déclaré dans le schéma
SQL
SELECT typeof(payload) FROM dev.bronze.events LIMIT 1;
SELECT decode(value, 'UTF-8') AS payload_json
FROM dev.bronze.kafka_events;
PYTHON
from pyspark.sql import functions as F

events_df = spark.table("dev.bronze.events")
events_df.printSchema()
kafka_json_df = kafka_df.select(
    F.decode("value", "UTF-8").alias("payload_json")
)

Pour un contrat stable, parser une seule fois vers un STRUCT. Pour une exploration ponctuelle, : est plus concis. Il ne supprime toutefois pas le parsing : Databricks doit interpréter le JSON STRING à chaque requête. Un VARIANT n'apporte un gain de réutilisation que s'il est persisté, pas s'il est créé dans une CTE puis immédiatement supprimé. Le type VARIANT est disponible à partir de Databricks Runtime 15.3.

2A. Schéma stable : schema_of_json, puis from_json

schema_of_json aide à découvrir un schéma DDL sur un exemple ; schema_of_json_agg peut combiner plusieurs documents. En production, relire et versionner un schéma explicite.

SQL
-- Exploration uniquement.
SELECT schema_of_json(
  '{"order_id":101,"customer":{"id":7,"address":{"city":"Paris","country":"FR"}},"items":[{"sku":"A1","qty":2},{"sku":"B2","qty":1}]}'
);

-- Parsing de production avec contrat explicite.
CREATE OR REPLACE TABLE dev.silver.order_items AS
WITH parsed AS (
  SELECT from_json(
    payload_json,
    'order_id BIGINT,
     customer STRUCT<
       id: BIGINT,
       address: STRUCT<city: STRING, country: STRING>
     >,
     items ARRAY<STRUCT<sku: STRING, qty: INT>>'
  ) AS order_data
  FROM dev.bronze.orders
)
SELECT
  order_data.order_id,
  order_data.customer.id AS customer_id,
  order_data.customer.address.city AS city,
  item.sku,
  item.qty
FROM parsed
LATERAL VIEW OUTER explode(order_data.items) exploded AS item;
PYTHON
from pyspark.sql import types as T

json_sample = """
{
  "order_id": 101,
  "customer": {
    "id": 7,
    "address": {"city": "Paris", "country": "FR"}
  },
  "items": [
    {"sku": "A1", "qty": 2},
    {"sku": "B2", "qty": 1}
  ]
}
"""

schema_ddl = (
    spark.range(1)
         .select(F.schema_of_json(F.lit(json_sample)).alias("schema"))
         .first()["schema"]
)
print(schema_ddl)

order_schema = T.StructType([
    T.StructField("order_id", T.LongType()),
    T.StructField("customer", T.StructType([
        T.StructField("id", T.LongType()),
        T.StructField("address", T.StructType([
            T.StructField("city", T.StringType()),
            T.StructField("country", T.StringType()),
        ])),
    ])),
    T.StructField("items", T.ArrayType(T.StructType([
        T.StructField("sku", T.StringType()),
        T.StructField("qty", T.IntegerType()),
    ]))),
])

parsed_df = (
    spark.table("dev.bronze.orders")
         .withColumn("order_data", F.from_json("payload_json", order_schema))
         .withColumn("item", F.explode_outer("order_data.items"))
         .select(
             "order_data.order_id",
             F.col("order_data.customer.id").alias("customer_id"),
             F.col("order_data.customer.address.city").alias("city"),
             "item.sku",
             "item.qty",
         )
)

(
    parsed_df.write
             .mode("overwrite")
             .saveAsTable("dev.silver.order_items")
)

Dans le schéma, un dictionnaire JSON devient un STRUCT, une liste devient un ARRAY et chaque objet de la liste devient ici un STRUCT. L'accès utilise . pour les champs imbriqués ; explode_outer transforme chaque élément de items en ligne tout en conservant les commandes dont la liste est NULL ou vide.

Par défaut, from_json utilise un mode permissif et renvoie NULL pour une chaîne non parsable. L'option map('mode', 'FAILFAST') en SQL — ou options={"mode": "FAILFAST"} en PySpark — fait au contraire échouer le traitement. Les noms du schéma doivent respecter la casse des champs JSON. Conserver la donnée brute en Bronze et isoler les erreurs si nécessaire.

2B. Schéma flexible : VARIANT

L'objectif est de convertir le JSON brut une fois pendant le traitement Bronze → Silver, puis de stocker son encodage VARIANT pour les lectures suivantes.

SQL
CREATE OR REPLACE TABLE dev.silver.iot_events_variant AS
SELECT
  event_id,
  try_parse_json(payload_json) AS payload
FROM dev.bronze.iot_events;

-- Requête ultérieure : payload est déjà stocké en VARIANT.
SELECT
  payload:device_id::STRING AS device_id,
  TRY_CAST(payload:temperature AS DOUBLE) AS temperature
FROM dev.silver.iot_events_variant;
PYTHON
variant_silver_df = (
    spark.table("dev.bronze.iot_events")
         .withColumn("payload", F.try_parse_json("payload_json"))
         .select("event_id", "payload")
)

(
    variant_silver_df.write
                     .mode("overwrite")
                     .saveAsTable("dev.silver.iot_events_variant")
)

# Lecture ultérieure du VARIANT déjà stocké.
result_df = (
    spark.table("dev.silver.iot_events_variant")
         .select(
             F.expr("payload:device_id::STRING").alias("device_id"),
             F.expr("try_cast(payload:temperature AS DOUBLE)")
              .alias("temperature"),
         )
)

parse_json échoue sur un JSON invalide ; try_parse_json renvoie NULL, ce qui facilite la mise en quarantaine. Sur un VARIANT, : renvoie une valeur VARIANT à caster et les chemins sont sensibles à la casse. Créer ce VARIANT dans une simple CTE ne dispense pas de le reconstruire à la prochaine requête.

2C. Requête ponctuelle dans un JSON STRING

Syntaxe : concise en Databricks SQL :

SQL
SELECT
  payload_json:device_id AS device_id,
  TRY_CAST(payload_json:temperature AS DOUBLE) AS temperature
FROM dev.bronze.iot_events;

Alternative avec get_json_object et un chemin JSON $ :

SQL
SELECT
  get_json_object(payload_json, '$.device_id') AS device_id,
  TRY_CAST(
    get_json_object(payload_json, '$.temperature') AS DOUBLE
  ) AS temperature
FROM dev.bronze.iot_events;

En PySpark, get_json_object évite d'écrire une expression SQL :

PYTHON
few_fields_df = (
    spark.table("dev.bronze.iot_events")
         .select(
             F.get_json_object("payload_json", "$.device_id")
              .alias("device_id"),
             F.expr("""
                 try_cast(
                   get_json_object(payload_json, '$.temperature')
                   AS DOUBLE
                 )
             """)
              .alias("temperature"),
         )
)

Sur un JSON STRING, les deux méthodes extraient une représentation STRING sans matérialiser de VARIANT. Le parsing est implicite et sera refait lors d'une autre requête. : est généralement plus lisible en Databricks SQL ; get_json_object fournit une fonction explicite en SQL et dans l'API PySpark, et renvoie NULL si le JSON est invalide. Ce choix convient à l'exploration ou à une requête isolée, pas à la matérialisation répétée de nombreux champs Silver.

2D. Modèle hybride recommandé

Pour une Silver flexible, matérialiser les champs fréquemment filtrés, joints ou agrégés en colonnes typées et conserver le document complet en VARIANT pour les champs rares ou évolutifs :

SQL
CREATE OR REPLACE TABLE dev.silver.iot_events AS
SELECT
  payload_json:device_id AS device_id,
  TRY_CAST(payload_json:temperature AS DOUBLE) AS temperature,
  try_parse_json(payload_json) AS payload
FROM dev.bronze.iot_events;
PYTHON
hybrid_silver_df = (
    spark.table("dev.bronze.iot_events")
         .select(
             F.expr("payload_json:device_id").alias("device_id"),
             F.expr("try_cast(payload_json:temperature AS DOUBLE)")
              .alias("temperature"),
             F.try_parse_json("payload_json").alias("payload"),
         )
)

(
    hybrid_silver_df.write
                    .mode("overwrite")
                    .saveAsTable("dev.silver.iot_events")
)

Une colonne VARIANT ne peut pas être utilisée directement pour partitionner, ZORDER, définir une clé de clustering, comparer, grouper ou trier. Extraire les champs fréquents en colonnes typées permet ces opérations et améliore le layout ; le VARIANT conserve la flexibilité du reste du document.

En pratique : 2A pour un schéma stable ; 2B pour un document flexible persisté ; 2C pour une requête ponctuelle ; 2D pour combiner performances sur les champs fréquents et flexibilité. Conserver aussi le STRING brut en Bronze pour l'audit et le rejeu.

Manipuler les colonnes et les tableaux

Version SQL :

SQL
SELECT DISTINCT
  id,
  split(full_name, ' ') AS name_parts,
  tag
FROM dev.bronze.people
LATERAL VIEW OUTER explode(tags) exploded AS tag
WHERE tag <> 'obsolete';

Version PySpark :

PYTHON
result_df = (
    source_df
      .select("id", "full_name", "tags")
      .withColumn("name_parts", F.split("full_name", " "))
      .withColumn("tag", F.explode_outer("tags"))
      .filter(F.col("tag") != "obsolete")
      .drop("tags")
      .dropDuplicates(["id", "tag"])
)
explode() ou flatten()
  • explode(array) transforme chaque élément d'un tableau en une nouvelle ligne : le nombre de lignes augmente. explode_outer conserve une ligne avec NULL pour un tableau nul ou vide.
  • posexplode(array) fait la même expansion et retourne aussi la position d'origine de chaque élément (pos, indexé à partir de zéro).
  • flatten(array_of_arrays) fusionne un niveau de tableaux imbriqués en un seul tableau : le nombre de lignes ne change pas.
SQL
SELECT order_id, explode(items) AS item
FROM dev.bronze.orders;

SELECT order_id, pos, item
FROM dev.bronze.orders,
LATERAL posexplode(items) AS p(pos, item);

SELECT order_id, flatten(item_batches) AS all_items
FROM dev.bronze.orders;
PYTHON
exploded_df = orders_df.select("order_id", F.explode("items").alias("item"))
positioned_df = orders_df.select(
    "order_id",
    F.posexplode("items").alias("pos", "item"),
)
flattened_df = orders_df.select(
    "order_id", F.flatten("item_batches").alias("all_items")
)

Mémo : explode = plus de lignes ; posexplode = lignes + positions ; flatten = tableau moins imbriqué.

Filtrer les éléments d'un tableau avec une lambda

La fonction higher-order FILTER conserve la ligne et retourne un nouveau tableau contenant uniquement les éléments qui satisfont le prédicat. Elle ne doit pas être confondue avec la clause WHERE, qui filtre des lignes, ni avec explode, qui crée des lignes :

SQL
SELECT
  faculty_id,
  students,
  FILTER(students, student -> student.total_courses < 3) AS few_courses_students
FROM dev.silver.faculties;
PYTHON
from pyspark.sql import functions as F

faculties_df.select(
    "faculty_id",
    "students",
    F.filter(
        "students",
        lambda student: student.total_courses < 3,
    ).alias("few_courses_students"),
)

Le résultat reste un ARRAY<STRUCT<...>>; pour obtenir une ligne par étudiant, appliquer ensuite explode ou explode_outer.

4.1.4 Déduplication déterministe

Pour supprimer des lignes entièrement identiques, appliquer DISTINCT à toutes les colonnes :

SQL
SELECT DISTINCT *
FROM dev.bronze.orders;
PYTHON
unique_rows_df = source_df.distinct()

distinct() compare toutes les colonnes et peut provoquer un shuffle. dropDuplicates(["id"]) déduplique seulement selon la clé indiquée, mais ne permet pas de choisir explicitement la ligne conservée. Pour garder, par exemple, la version la plus récente de chaque clé, utiliser une fenêtre déterministe :

Version SQL :

SQL
SELECT * EXCEPT (rn)
FROM (
  SELECT *, row_number() OVER (
    PARTITION BY order_id
    ORDER BY updated_at DESC, ingested_at DESC
  ) AS rn
  FROM dev.bronze.orders
)
WHERE rn = 1;

Version PySpark :

PYTHON
w = Window.partitionBy("order_id").orderBy(
    F.col("updated_at").desc(),
    F.col("ingested_at").desc()
)

latest_df = (
    source_df.withColumn("rn", F.row_number().over(w))
             .filter(F.col("rn") == 1)
             .drop("rn")
)

4.1.5 Écriture et évolution des tables Delta (DDL et DML)

DDL — Data Definition Language (langage de définition des données) définit les objets et leur structure : CREATE, ALTER, DROP, TRUNCATE, CREATE VIEW.

DML — Data Manipulation Language (langage de manipulation des données) lit ou modifie les lignes : SELECT, INSERT, UPDATE, DELETE, MERGE. Selon les classifications, SELECT est parfois rangé à part dans le DQL ; pour l'examen, retenir surtout que le DML agit sur les données, le DDL sur la structure.

SQL
-- Remplace la définition et crée une table vide.
CREATE OR REPLACE TABLE dev.silver.employees (
  employee_id STRING,
  start_date DATE,
  avg_rating DOUBLE
) USING DELTA;

INSERT INTO dev.silver.employees VALUES ('a1', DATE '2026-01-06', 9.4);

UPDATE dev.silver.employees
SET avg_rating = 9.7
WHERE employee_id = 'a1';

DELETE FROM dev.silver.employees
WHERE employee_id = 'a1';

Différence importante :

  • CREATE TABLE IF NOT EXISTS ne modifie rien si la table existe ;
  • CREATE OR REPLACE TABLE remplace la définition et les données de la table ;
  • CTAS crée/remplace une table avec le résultat d'un SELECT.

Le mode d'écriture PySpark ignore est proche de IF NOT EXISTS : si la table ou le chemin existe déjà, l'écriture entière est silencieusement ignorée.

SQL
CREATE TABLE IF NOT EXISTS dev.silver.orders_snapshot
USING DELTA
AS SELECT * FROM dev.silver.orders;
PYTHON
(orders_df.write
          .format("delta")
          .mode("ignore")
          .saveAsTable("dev.silver.orders_snapshot"))

Ce mode ne déduplique pas les lignes et ne vérifie pas que la table existante est complète ou conforme : il convient à la création idempotente d'un objet, pas à un chargement incrémental. Pour un upsert par clé, utiliser MERGE.

Le DDL reste principalement SQL. Pour les DML programmatiques, l'API DeltaTable expose update, delete et merge ; la section suivante montre le cas merge, le plus important pour l'examen.

Colonnes générées

Une colonne GENERATED ALWAYS AS (expression) est calculée par Delta à l'écriture, puis stockée physiquement, à partir d'autres colonnes de la même ligne. Elle évite de fournir manuellement une dérivation déterministe, par exemple la date issue d'un timestamp :

SQL
CREATE TABLE dev.silver.orders (
  order_id INT,
  order_time TIMESTAMP,
  order_date DATE GENERATED ALWAYS AS (CAST(order_time AS DATE)),
  units INT
) USING DELTA;

-- order_date est calculée automatiquement à partir de order_time.
INSERT INTO dev.silver.orders (order_id, order_time, units)
VALUES (1, TIMESTAMP '2026-08-18 09:30:00', 4);

Version Python avec le builder Delta :

PYTHON
from delta.tables import DeltaTable
from pyspark.sql.types import DateType

(DeltaTable.create(spark)
 .tableName("dev.silver.orders")
 .addColumn("order_id", "INT")
 .addColumn("order_time", "TIMESTAMP")
 .addColumn(
     "order_date",
     DateType(),
     generatedAlwaysAs="CAST(order_time AS DATE)",
 )
 .addColumn("units", "INT")
 .execute())

À retenir : le type de la colonne générée est déclaré explicitement ; l'expression doit être déterministe et ne peut pas être une agrégation, une sous-requête ou une UDF. Une écriture peut omettre order_date ; si elle fournit cette colonne, sa valeur doit respecter l'expression, sinon la transaction échoue. La colonne peut servir au partitionnement, mais pour une nouvelle table managée il faut également considérer liquid clustering et éviter un partitionnement excessif.

Évolution du schéma avec mergeSchema et overwriteSchema

Ces options concernent le schéma de la table Delta écrite, et non le schéma d'entrée conservé dans cloudFiles.schemaLocation. Elles s'appliquent indépendamment du fait que le DataFrame ait été produit en batch ou par une ingestion streaming.

Option Effet Cas typique
mergeSchema=true Fusionne le schéma entrant avec la cible, surtout en ajoutant de nouvelles colonnes compatibles Append avec évolution additive
overwriteSchema=true Remplace le schéma de la cible pendant un mode("overwrite") et réécrit la table Suppression, renommage, réordonnancement ou changement de type contrôlé

Version SQL, avec une évolution explicite ou un remplacement contrôlé :

SQL
-- Évolution additive explicite, sans réécrire les lignes existantes.
ALTER TABLE dev.silver.orders ADD COLUMNS (sales_channel STRING);

-- Remplacement complet : données et schéma deviennent ceux du SELECT.
CREATE OR REPLACE TABLE dev.silver.orders AS
SELECT CAST(order_id AS BIGINT) AS order_id,
       CAST(amount AS DECIMAL(14,2)) AS amount
FROM dev.silver.orders_retyped;

Version PySpark avec les options d'écriture DataFrame :

PYTHON
# Ajout de colonnes compatibles sans remplacer les lignes déjà présentes.
(new_orders_df.write
              .format("delta")
              .mode("append")
              .option("mergeSchema", "true")
              .saveAsTable("dev.silver.orders"))

# Remplacement volontaire des données ET du schéma de la table.
(retyped_orders_df.write
                  .format("delta")
                  .mode("overwrite")
                  .option("overwriteSchema", "true")
                  .saveAsTable("dev.silver.orders"))

À retenir : mergeSchema élargit de façon additive ; overwriteSchema accompagne un overwrite destructif. Ni l'un ni l'autre ne remplace un contrat de données relu. Pour un MERGE INTO, utiliser plutôt WITH SCHEMA EVOLUTION ou .withSchemaEvolution() dans la section suivante.

La configuration de session suivante active l'évolution de schéma pour toutes les écritures Delta compatibles de la SparkSession :

SQL
-- Consulter la valeur de la session.
SET spark.databricks.delta.schema.autoMerge.enabled;

-- L'activer pour les écritures Delta compatibles de cette session.
SET spark.databricks.delta.schema.autoMerge.enabled = true;
PYTHON
# Consulter, puis modifier la valeur pour la SparkSession courante.
spark.conf.get("spark.databricks.delta.schema.autoMerge.enabled", "false")
spark.conf.set("spark.databricks.delta.schema.autoMerge.enabled", "true")

spark.databricks.delta.schema.autoMerge.enabled est un réglage global à la session, pas une propriété d'une seule table ni une option Auto Loader. Il peut donc faire évoluer involontairement plusieurs cibles. Databricks le considère comme une approche legacy et recommande en production d'autoriser l'évolution au niveau de l'opération concernée : .option("mergeSchema", "true"), INSERT WITH SCHEMA EVOLUTION, MERGE WITH SCHEMA EVOLUTION ou .withSchemaEvolution(). L'option ou la syntaxe locale est prioritaire sur la configuration de session.

Pour imposer une table insert-only, activer la propriété Delta appendOnly :

SQL
ALTER TABLE dev.bronze.events SET TBLPROPERTIES (
  'delta.appendOnly' = 'true'
);
PYTHON
# Cette propriété de table reste un DDL SQL, même depuis un notebook Python.
spark.sql("""
  ALTER TABLE dev.bronze.events SET TBLPROPERTIES (
    'delta.appendOnly' = 'true'
  )
""")

Les INSERT et appends restent permis ; les opérations qui modifient ou suppriment des lignes existantes (UPDATE, DELETE, MERGE concerné) échouent. Cette propriété protège un journal immuable, mais ne déduplique pas les inserts.

Nuance de QCM : ce n'est pas le mot-clé MERGE qui est interdit. Un insert-only merge, ne comportant que WHEN NOT MATCHED THEN INSERT, reste compatible avec appendOnly puisqu'il ne modifie aucune ligne existante. Un MERGE comportant un UPDATE ou un DELETE échoue.

Contraintes NOT NULL et CHECK Delta Lake

Pour rendre non nullable une colonne existante :

SQL
-- À contrôler/corriger avant la modification.
SELECT COUNT(*) AS null_ids
FROM dev.silver.transactions
WHERE transaction_id IS NULL;

ALTER TABLE dev.silver.transactions
ALTER COLUMN transaction_id SET NOT NULL;

SET NOT NULL est pris en charge pour une table Delta. Databricks vérifie toutes les lignes existantes avant d'ajouter la contrainte : si au moins une valeur est NULL, l'ALTER TABLE échoue et il faut corriger, supprimer ou mettre en quarantaine ces lignes. Les écritures futures contenant un NULL font ensuite échouer toute leur transaction. DROP NOT NULL rend à nouveau la colonne nullable :

SQL
ALTER TABLE dev.silver.transactions
ALTER COLUMN transaction_id DROP NOT NULL;

Une contrainte CHECK est une règle appliquée à chaque écriture. Databricks vérifie aussi les lignes existantes lorsqu'on ajoute la contrainte. Si une ligne viole la règle, toute la transaction échoue : aucune écriture partielle n'est validée.

SQL
ALTER TABLE dev.silver.orders
ADD CONSTRAINT valid_amount CHECK (amount >= 0);

-- Échec atomique si une ligne a amount < 0.
INSERT INTO dev.silver.orders VALUES (1001, DATE '2026-08-07', -5);

ALTER TABLE dev.silver.orders DROP CONSTRAINT valid_amount;

En PySpark, la contrainte reste définie en DDL SQL, puis elle s'applique automatiquement aux écritures DataFrame :

PYTHON
spark.sql("""
  ALTER TABLE dev.silver.orders
  ADD CONSTRAINT valid_amount CHECK (amount >= 0)
""")

# Cette écriture échoue entièrement si invalid_df contient amount < 0.
invalid_df.write.mode("append").saveAsTable("dev.silver.orders")

Les définitions des contraintes CHECK et NOT NULL font partie des métadonnées transactionnelles enregistrées dans le Delta log ; chaque commit les applique atomiquement. Elles sont donc enforced. Les clés primaires et étrangères Unity Catalog sont informatives : ne pas leur attribuer le même comportement de rejet.

4.1.6 Upsert avec MERGE

SQL
MERGE INTO dev.silver.customers AS target
USING dev.bronze.customer_changes AS source
ON target.customer_id = source.customer_id

WHEN MATCHED AND source.operation = 'DELETE' THEN DELETE

WHEN MATCHED AND source.operation = 'UPDATE' THEN
  UPDATE SET
    target.email = source.email,
    target.country = source.country,
    target.updated_at = source.updated_at

WHEN NOT MATCHED AND source.operation <> 'DELETE' THEN
  INSERT (customer_id, email, country, updated_at)
  VALUES (source.customer_id, source.email, source.country, source.updated_at);

Version PySpark avec l'API Delta :

PYTHON
from delta.tables import DeltaTable

target = DeltaTable.forName(spark, "dev.silver.customers")
source_df = spark.table("dev.bronze.customer_changes")

(target.alias("target")
 .merge(source_df.alias("source"),
        "target.customer_id = source.customer_id")
 .whenMatchedDelete(condition="source.operation = 'DELETE'")
 .whenMatchedUpdate(
     condition="source.operation = 'UPDATE'",
     set={
         "email": "source.email",
         "country": "source.country",
         "updated_at": "source.updated_at",
     },
 )
 .whenNotMatchedInsert(
     condition="source.operation <> 'DELETE'",
     values={
         "customer_id": "source.customer_id",
         "email": "source.email",
         "country": "source.country",
         "updated_at": "source.updated_at",
     },
 )
 .execute())

Avec évolution de schéma compatible :

SQL
MERGE WITH SCHEMA EVOLUTION INTO dev.silver.customers AS target
USING dev.bronze.customer_changes AS source
ON target.customer_id = source.customer_id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;

Équivalent PySpark à partir de Databricks Runtime 15.4 LTS :

PYTHON
(target.alias("target")
 .merge(source_df.alias("source"),
        "target.customer_id = source.customer_id")
 .withSchemaEvolution()
 .whenMatchedUpdateAll()
 .whenNotMatchedInsertAll()
 .execute())

SQL rend généralement un MERGE plus facile à relire. L'API Delta PySpark est pratique lorsque la source est déjà un DataFrame ou que les clauses sont construites dans du code modulaire.

À retenir :

  • MERGE est la primitive d'upsert et de CDC sur une table Delta ;
  • WITH SCHEMA EVOLUTION autorise le MERGE à faire évoluer le schéma cible pour les colonnes source compatibles, notamment avec UPDATE SET * et INSERT * ;
  • sans cette clause, une colonne source absente de la cible provoque normalement une erreur ou doit être ignorée explicitement ;
  • l'évolution du schéma du MERGE est distincte de celle d'Auto Loader : la première concerne la table cible pendant l'upsert, la seconde le schéma découvert à la lecture de fichiers ;
  • la condition ON exprime la clé métier ;
  • plusieurs lignes source correspondant à une même ligne cible peuvent rendre le merge ambigu : dédupliquer la source d'abord ;
  • une clé de MERGE doit être stable et unique au niveau métier : plusieurs lignes source pour une même clé peuvent provoquer une erreur si elles modifient la même cible, et plusieurs lignes source nouvelles portant la même clé peuvent toutes être insérées ;
  • l'évolution de schéma n'est pas une validation métier : contrôler les colonnes autorisées.

Pour un MERGE qui lit trop de fichiers cible, commencer par vérifier la condition ON et le plan. Ajouter un prédicat borné sur une partition ou une date seulement s'il est sémantiquement correct, puis améliorer le layout sur la clé souvent recherchée :

SQL
-- Le filtre de date est valide ici parce que le micro-batch ne contient que ce jour.
MERGE INTO dev.silver.events AS target
USING dev.staging.events_day AS source
ON  target.event_id = source.event_id
AND target.event_date = source.event_date
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;

-- Table non liquid-clustered : rapproche les mêmes clés et améliore le data skipping.
OPTIMIZE dev.silver.events ZORDER BY (event_id);
PYTHON
from delta.tables import DeltaTable

(DeltaTable.forName(spark, "dev.silver.events")
 .alias("target")
 .merge(
     source_df.alias("source"),
     "target.event_id = source.event_id "
     "AND target.event_date = source.event_date",
 )
 .whenMatchedUpdateAll()
 .whenNotMatchedInsertAll()
 .execute())

Le Z-order n'est pas une garantie de pruning et ne corrige pas une mauvaise clé : il rend les statistiques min/max plus sélectives afin que le data skipping automatique ignore davantage de fichiers. Pour une nouvelle table managée, considérer d'abord liquid clustering. Les petits fichiers produits par des MERGE fréquents relèvent plutôt des optimized writes/auto compaction, puis d'OPTIMIZE pour l'existant ; voir 7.5.3.

4.1.7 Data quality expectations

Les expectations sont des règles de qualité propres aux Lakeflow Spark Declarative Pipelines, présentés plus largement en 4.3. Elles sont souvent appliquées pendant Bronze → Silver pour mesurer, écarter ou bloquer les données invalides.

SQL
CREATE OR REFRESH STREAMING TABLE dev.silver.orders (
  CONSTRAINT valid_id
    EXPECT (order_id IS NOT NULL) ON VIOLATION DROP ROW,
  CONSTRAINT valid_amount
    EXPECT (amount >= 0),
  CONSTRAINT recent_date
    EXPECT (order_ts >= TIMESTAMP '2020-01-01') ON VIOLATION FAIL UPDATE
)
COMMENT 'Commandes nettoyées'
TBLPROPERTIES ('quality' = 'silver')
AS
SELECT
  CAST(order_id AS BIGINT) AS order_id,
  CAST(order_ts AS TIMESTAMP) AS order_ts,
  CAST(amount AS DECIMAL(12,2)) AS amount
FROM STREAM(dev.bronze.orders);

Version Python avec les trois comportements :

PYTHON
from pyspark import pipelines as dp

@dp.table(name="dev.silver.orders")
@dp.expect("valid_amount", "amount >= 0")
@dp.expect_or_drop("valid_id", "order_id IS NOT NULL")
@dp.expect_or_fail("recent_date", "order_ts >= TIMESTAMP '2020-01-01'")
def silver_orders():
    return spark.readStream.table("dev.bronze.orders")

Pour réutiliser plusieurs règles avec le même comportement, passer un dictionnaire à expect_all :

PYTHON
from pyspark import pipelines as dp

valid_products = {
    "valid_product_id": "product_id IS NOT NULL",
    "positive_quantity": "quantity > 0",
    "non_negative_price": "price >= 0",
}

@dp.table
@dp.expect_all(valid_products)
def silver_sales():
    return spark.readStream.table("bronze_sales")

expect_all conserve les lignes invalides et enregistre les métriques, comme plusieurs @dp.expect. Utiliser expect_all_or_drop pour les retirer ou expect_all_or_fail pour faire échouer l'update. Attention à la syntaxe : une table se lit avec spark.readStream.table("bronze_sales"), pas spark.readStream("bronze_sales").

Les expressions des décorateurs Python restent des prédicats SQL sous forme de chaînes. Python apporte surtout la réutilisation de dictionnaires d'expectations et les variantes expect_all*.

SQL Décorateur Python Effet d'une ligne invalide
EXPECT (...) @dp.expect / @dp.expect_all La ligne est conservée ; la violation est comptée dans les métriques
ON VIOLATION DROP ROW @dp.expect_or_drop / @dp.expect_all_or_drop La ligne invalide est écartée ; l'update continue
ON VIOLATION FAIL UPDATE @dp.expect_or_fail / @dp.expect_all_or_fail L'update du flow échoue atomiquement

Les résultats sont consultables dans l'event log Delta du pipeline. Pour les événements flow_progress, la colonne JSON details contient flow_progress.data_quality : passed_records et failed_records se trouvent dans chaque élément de expectations, tandis que dropped_records est un total au niveau data_quality.

SQL
SELECT
  timestamp,
  details:flow_progress:data_quality:dropped_records::BIGINT
    AS dropped_records,
  details:flow_progress:data_quality:expectations
    AS expectation_metrics
FROM event_log('<pipeline-id>')
WHERE event_type = 'flow_progress'
ORDER BY timestamp DESC;

Ces compteurs mesurent la qualité et ne décrivent pas à eux seuls l'action choisie : consulter la définition de l'expectation pour savoir si la ligne est conservée ou supprimée. Avec FAIL UPDATE, l'update s'arrête et les métriques ne sont généralement pas enregistrées.

Une validation de format ou de liste s'exprime par un prédicat SQL, associé à l'un des comportements expect, drop ou fail :

SQL
CREATE OR REFRESH STREAMING TABLE dev.silver.events (
  CONSTRAINT iso_8601_timestamp EXPECT (
    event_ts_raw RLIKE '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$'
    AND TRY_CAST(event_ts_raw AS TIMESTAMP) IS NOT NULL
  ),
  CONSTRAINT allowed_status EXPECT (
    status IN ('CREATED', 'PAID', 'CANCELLED')
  )
)
AS SELECT * FROM STREAM(dev.bronze.events);
PYTHON
validation_rules = {
    "iso_8601_timestamp": (
        "event_ts_raw RLIKE "
        "'^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}"
        "([.][0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$' "
        "AND TRY_CAST(event_ts_raw AS TIMESTAMP) IS NOT NULL"
    ),
    "allowed_status": "status IN ('CREATED', 'PAID', 'CANCELLED')",
}

@dp.table(name="dev.silver.events")
@dp.expect_all(validation_rules)
def validated_events():
    return spark.readStream.table("dev.bronze.events")

Une quarantaine est un pattern de branchement, pas un quatrième comportement d'expectation : calculer un indicateur, publier les lignes valides dans Silver et les invalides dans une table distincte. Les expect_all* peuvent parallèlement fournir les métriques du pipeline.

SQL
CREATE TEMPORARY VIEW classified_events AS
SELECT *,
       event_id IS NOT NULL
       AND status IN ('CREATED', 'PAID', 'CANCELLED') AS is_valid
FROM STREAM(dev.bronze.events);

CREATE OR REFRESH STREAMING TABLE dev.silver.events_valid AS
SELECT * EXCEPT (is_valid)
FROM STREAM(classified_events)
WHERE is_valid;

CREATE OR REFRESH STREAMING TABLE dev.quarantine.events_invalid AS
SELECT * EXCEPT (is_valid)
FROM STREAM(classified_events)
WHERE NOT COALESCE(is_valid, false);
PYTHON
from pyspark import pipelines as dp
from pyspark.sql import functions as F

@dp.temporary_view
def classified_events():
    return spark.readStream.table("dev.bronze.events").withColumn(
        "is_valid",
        F.col("event_id").isNotNull()
        & F.col("status").isin("CREATED", "PAID", "CANCELLED"),
    )

@dp.table(name="dev.silver.events_valid")
def valid_events():
    return spark.readStream.table("classified_events").filter("is_valid").drop("is_valid")

@dp.table(name="dev.quarantine.events_invalid")
def invalid_events():
    return (
        spark.readStream.table("classified_events")
             .filter("NOT COALESCE(is_valid, false)")
             .drop("is_valid")
    )

Hors pipeline déclaratif, effectuer des contrôles explicites et produire des métriques/quarantaines :

PYTHON
invalid_count = silver_df.filter(
    F.col("order_id").isNull() | (F.col("amount") < 0)
).count()

assert invalid_count == 0, f"{invalid_count} lignes invalides"

4.1.8 CDC, CDF et SCD : synchroniser et historiser les changements

But et moment d'exécution

Le but est de maintenir une cible synchronisée avec une source mutable sans la recharger entièrement à chaque exécution. Cette cible peut conserver seulement l'état courant ou l'historique des versions.

Notion Rôle Moment
CDC — Change Data Capture (capture des changements de données) Transporte les insertions, mises à jour et suppressions produites par une source, généralement avec une opération et une séquence Dès qu'un changement est émis par la base ou le système source
CDF — Change Data Feed Rend lisibles les changements ligne par ligne entre les versions d'une table Delta Après chaque commit de la table source ; lecture ensuite en batch ou en streaming
SCD — Slowly Changing Dimension (dimension à évolution lente) Définit comment ces changements sont matérialisés dans la cible Pendant la transformation qui applique le flux CDC à la table cible

Déroulé typique :

TEXT
Source mutable ou table Delta
        -> flux CDC ou CDF
        -> Bronze : changements bruts et traçables
        -> MERGE ou AUTO CDC pendant Bronze -> Silver
        -> cible SCD 1 (état courant) ou SCD 2 (historique)
        -> consommation ou modèles Gold
  • SCD type 1 remplace l'ancienne valeur et conserve uniquement l'état courant. Le choisir lorsque l'historique n'est pas utile.
  • SCD type 2 ajoute une nouvelle version et conserve l'historique, avec des bornes telles que __START_AT et __END_AT. Le choisir pour les analyses « tel qu'à cette date », l'audit ou la traçabilité métier.

L'application du CDC se situe donc généralement dans Bronze → Silver, pour produire une entité fiable et à jour. Une dimension SCD destinée directement à la BI peut aussi être publiée ou consommée en Gold : les couches médaillon sont un choix d'architecture, pas une contrainte de l'API.

Ce pattern est pertinent pour des clients, produits ou comptes dont les lignes changent. Pour un journal d'événements immuable traité uniquement en append, il n'est généralement pas nécessaire. Lors d'une première réplication, effectuer d'abord une hydratation initiale des lignes existantes, puis traiter les changements suivants en mode triggered ou continu.

Raccourci : CDF expose, CDC transporte, SCD matérialise.

Change Data Feed (CDF) avec Structured Streaming

Le CDF permet à un traitement incrémental, un audit ou une réplication de ne lire que les lignes modifiées. La forme historique à connaître pour l'examen est le legacy CDF, activé table par table avant les changements à capturer :

SQL
ALTER TABLE dev.silver.customers SET TBLPROPERTIES (
  delta.enableChangeDataFeed = true
);

-- Lecture batch des changements à partir de la version 12.
SELECT * FROM table_changes('dev.silver.customers', 12);

Cette activation n'est pas rétroactive : une version antérieure à l'activation n'est pas lisible par le legacy CDF. Les runtimes récents proposent aussi un automatic CDF sous conditions, sans propriété par table ; les deux approches utilisent les mêmes interfaces de lecture table_changes() et readChangeFeed=true. Pour une question ne mentionnant pas explicitement l'automatic CDF, reconnaître la propriété delta.enableChangeDataFeed=true.

Une lecture incrémentale mémorise automatiquement les versions traitées dans son checkpoint :

PYTHON
customer_changes = (
    spark.readStream
         .option("readChangeFeed", "true")
         .table("dev.silver.customers")
)

(customer_changes.writeStream
                 .option("checkpointLocation", "/Volumes/dev/checkpoints/customer_cdf")
                 .trigger(availableNow=True)
                 .toTable("dev.bronze.customer_change_archive"))

Colonnes à connaître : _change_type (insert, delete, update_preimage, update_postimage), _commit_version et _commit_timestamp. Sans startingVersion, le premier démarrage du stream émet le snapshot courant comme des inserts, puis les changements suivants. Une lecture CDF directe n'est pas une archive permanente : ses anciennes versions suivent la rétention du journal et des fichiers Delta ; matérialiser le flux dans une autre table si l'historique doit être conservé.

Le CDF n'applique pas lui-même les changements à une cible et ne choisit pas entre SCD 1 et SCD 2 : il fournit seulement le flux d'entrée à un MERGE, à AUTO CDC ou à un autre consommateur.

RESTORE TABLE ne rembobine ni le Delta log ni un curseur CDF. Il crée une nouvelle version courante qui remet la table dans l'état d'une ancienne version ; cette opération est elle-même un changement de données. Un consommateur CDF/streaming peut donc recevoir, au commit de restauration, des changements correspondant aux fichiers restaurés et retraiter des lignes déjà vues. Le restore ne recrée pas les versions CDF expirées : pour un audit durable, archiver le CDF dans une autre table et rendre le consommateur idempotent.

Pour la reproductibilité ML, le moyen direct de relire le même dataset est de mémoriser sa version Delta puis d'utiliser le time travel, et non simplement d'activer le CDF :

SQL
SELECT *
FROM dev.silver.training_features VERSION AS OF 247;
PYTHON
training_df = (
    spark.read.option("versionAsOf", 247)
         .table("dev.silver.training_features")
)

Conserver avec l'expérience la version de table, le code et les paramètres. Le CDF sert plutôt à suivre ou archiver les changements — notamment les couples update_preimage/update_postimage — et seulement tant que toutes les versions nécessaires restent disponibles.

Appliquer le CDC en SCD avec AUTO CDC

AUTO CDC s'exécute lors d'une mise à jour Lakeflow Pipeline. Il lit les nouveaux événements disponibles depuis la progression précédente, les ordonne avec SEQUENCE BY, puis insère, met à jour, supprime ou historise les lignes de la cible selon la stratégie SCD.

Pour conserver uniquement la dernière version logique de chaque clé, utiliser un SCD type 1. KEYS identifie l'entité et SEQUENCE BY désigne l'ordre métier des changements : un événement arrivé tardivement avec un timestamp plus ancien ne doit pas écraser l'état plus récent.

SQL
CREATE OR REFRESH STREAMING TABLE dev.silver.customers_current;

CREATE FLOW customers_current_cdc
AS AUTO CDC INTO dev.silver.customers_current
FROM STREAM(dev.bronze.customer_changes)
KEYS (customer_id)
APPLY AS DELETE WHEN operation = 'DELETE'
SEQUENCE BY modified_at
COLUMNS * EXCEPT (operation)
STORED AS SCD TYPE 1;

Version Python :

PYTHON
from pyspark import pipelines as dp
from pyspark.sql import functions as F

dp.create_streaming_table(name="dev.silver.customers_current")

dp.create_auto_cdc_flow(
    target="dev.silver.customers_current",
    source="dev.bronze.customer_changes",
    keys=["customer_id"],
    sequence_by="modified_at",
    apply_as_deletes=F.expr("operation = 'DELETE'"),
    except_column_list=["operation"],
    stored_as_scd_type="1",
)

Les inserts et updates sont upsertés par défaut. Une suppression n'est appliquée que si sa représentation dans la source est traduite par APPLY AS DELETE WHEN/apply_as_deletes. Si plusieurs changements d'une même clé peuvent partager le même timestamp, ajouter un départage déterministe, par exemple SEQUENCE BY STRUCT(modified_at, sequence_id) en SQL ou sequence_by=F.struct("modified_at", "sequence_id") en Python.

Dans les anciens supports DLT, cette opération est appelée APPLY CHANGES INTO. Cette API reste disponible, mais Databricks recommande désormais son nom de remplacement AUTO CDC INTO ; pour un QCM historique, reconnaître que les deux désignent le même mécanisme fondé sur une cible, une source, KEYS et SEQUENCE BY.

Pour conserver au contraire l'historique de chaque clé, choisir un SCD type 2 :

SQL
CREATE OR REFRESH STREAMING TABLE dev.silver.customers;

CREATE FLOW customers_cdc
AS AUTO CDC INTO dev.silver.customers
FROM STREAM(dev.bronze.customer_changes)
KEYS (customer_id)
APPLY AS DELETE WHEN operation = 'DELETE'
SEQUENCE BY sequence_number
COLUMNS * EXCEPT (operation, sequence_number)
STORED AS SCD TYPE 2;

Version Python :

PYTHON
from pyspark import pipelines as dp
from pyspark.sql import functions as F

dp.create_streaming_table(name="dev.silver.customers")

dp.create_auto_cdc_flow(
    target="dev.silver.customers",
    source="dev.bronze.customer_changes",
    keys=["customer_id"],
    sequence_by="sequence_number",
    apply_as_deletes=F.expr("operation = 'DELETE'"),
    except_column_list=["operation", "sequence_number"],
    stored_as_scd_type="2",
)

AUTO CDC existe dans les interfaces SQL et Python. Si la source ne fournit pas de flux CDC mais seulement des snapshots successifs, utiliser AUTO CDC FROM SNAPSHOT, pris en charge en Python : le pipeline compare les snapshots pour déterminer les changements avant d'appliquer la même logique SCD.

KEYS identifie l'entité, SEQUENCE BY ordonne les changements — y compris ceux arrivés hors ordre — et la clause de suppression traduit les tombstones de la source. Une clé donnée ne doit avoir qu'un changement par valeur de séquence, et la séquence ne doit pas être NULL.

Un SCD type 2 peut aussi être construit manuellement avec MERGE : expirer la version courante (is_current=false, valid_to=...), puis insérer la nouvelle version. Cette logique demande souvent plusieurs étapes et une gestion rigoureuse des événements hors ordre ; dans un Lakeflow pipeline, préférer AUTO CDC ... STORED AS SCD TYPE 2, qui exprime directement ce besoin.

4.2 De Silver à Gold

L'objectif est de construire des données prêtes pour la consommation : enrichissements, regroupements, indicateurs et modèles analytiques. Les joins et agrégations peuvent aussi être utilisés en Silver ; leur classement ici représente leur rôle fréquent dans la préparation de sorties Gold, pas une limitation technique.

4.2.1 Joins, unions et combinaison de datasets

Les joins combinent les colonnes de plusieurs datasets selon une condition de correspondance ; les unions empilent leurs lignes lorsque leurs schémas sont compatibles.

Joins

Version SQL :

SQL
SELECT o.*, c.segment
FROM dev.silver.orders AS o
LEFT JOIN dev.silver.customers AS c
 ON o.customer_id = c.customer_id
 AND o.country = c.country;

Version PySpark :

PYTHON
orders = spark.table("dev.silver.orders").alias("o")
customers = spark.table("dev.silver.customers").alias("c")

# Plusieurs clés et left join : conserve toutes les commandes.
enriched = orders.join(
    customers,
    on=[
        F.col("o.customer_id") == F.col("c.customer_id"),
        F.col("o.country") == F.col("c.country"),
    ],
    how="left",
)
Join Résultat
inner Seulement les clés présentes des deux côtés
left Toutes les lignes de gauche, NULL si pas de correspondance à droite
right Symétrique du left join
full Toutes les lignes des deux côtés
left_semi Lignes de gauche ayant au moins une correspondance, sans colonnes de droite
left_anti Lignes de gauche sans correspondance
cross Produit cartésien ; nombre de lignes potentiellement explosif
Égalité null-safe et produit cartésien

L'opérateur SQL <=> est une égalité null-safe : il retourne true si les deux valeurs sont égales ou si elles sont toutes deux NULL, et false si une seule est NULL. À l'inverse, NULL = NULL retourne NULL, donc ne matche pas dans une condition de join.

SQL
SELECT a.*, b.status
FROM dev.silver.orders AS a
JOIN dev.silver.order_status AS b
  ON a.order_id <=> b.order_id;

Version PySpark :

PYTHON
joined_df = orders_df.alias("a").join(
    status_df.alias("b"),
    F.col("a.order_id").eqNullSafe(F.col("b.order_id")),
    "inner",
)

Attention : si plusieurs lignes ont une clé NULL de chaque côté, elles correspondent toutes entre elles. Utiliser <=> seulement lorsque deux clés nulles doivent réellement être considérées comme identiques.

Un cross join ne possède aucune condition et produit nombre_gauche × nombre_droite lignes :

SQL
SELECT * FROM colors CROSS JOIN sizes;
PYTHON
joined_df = colors_df.crossJoin(sizes_df)

Il est adapté à la génération volontaire de toutes les combinaisons, mais dangereux entre deux grandes relations.

Un broadcast join copie intégralement la petite relation sur chaque executor. Chaque partition de la grande table peut alors effectuer le join localement, sans redistribuer la grande table par shuffle. L'utiliser seulement si la dimension est réellement petite :

Version SQL :

SQL
SELECT /*+ BROADCAST(c) */ o.*, c.segment
FROM dev.silver.orders AS o
LEFT JOIN dev.silver.customers AS c
  ON o.customer_id = c.customer_id;

Version PySpark :

PYTHON
from pyspark.sql.functions import broadcast

enriched = orders.join(
    broadcast(customers.select("customer_id", "segment")),
    "customer_id",
    "left",
)

Dans les deux langages, laisser d'abord l'optimiseur choisir et forcer le broadcast seulement si la dimension est effectivement petite.

Ne jamais broadcaster une table trop grande : sa copie sur chaque executor peut créer un OOM. Le choix automatique dépend notamment de spark.sql.autoBroadcastJoinThreshold.

union, UNION et UNION ALL
Opération Association des colonnes Doublons
PySpark df1.union(df2) Par position Conservés
PySpark unionByName Par nom Conservés
SQL UNION ALL Par position compatible Conservés
SQL UNION Par position compatible Supprimés, donc shuffle possible

Version SQL lorsque les schémas sont déjà alignés :

SQL
SELECT order_id, country, amount FROM dev.silver.orders_france
UNION ALL
SELECT order_id, country, amount FROM dev.silver.orders_spain;

Version PySpark :

PYTHON
all_orders = france_df.unionByName(spain_df, allowMissingColumns=True)

allowMissingColumns=True n'a pas d'équivalent direct dans un UNION ALL SQL classique : aligner explicitement les colonnes et ajouter les NULL nécessaires.

Piège fréquent : la méthode DataFrame union se comporte comme UNION ALL, pas comme UNION SQL.

4.2.2 Agrégations et métriques

Version SQL :

SQL
SELECT
  billing_date,
  sum(amount_billed) AS total_revenue,
  count(DISTINCT billing_id) AS unique_invoices,
  approx_count_distinct(patient_id, 0.02) AS approx_patients,
  count_if(amount_billed > 1000) AS large_invoices,
  avg(amount_billed) AS mean_invoice_amount
FROM dev.silver.billing
GROUP BY billing_date;

Version PySpark :

PYTHON
daily_revenue_df = (
    billing_df.groupBy("billing_date")
              .agg(
                  F.sum("amount_billed").alias("total_revenue"),
                  F.count_distinct("billing_id").alias("unique_invoices"),
                  F.approx_count_distinct("patient_id", 0.02).alias("approx_patients"),
                  F.count_if(F.col("amount_billed") > 1000).alias("large_invoices"),
                  F.mean("amount_billed").alias("mean_invoice_amount"),
              )
)
  • COUNT(*) compte toutes les lignes, même si toutes leurs colonnes métier valent NULL ; COUNT(column) compte seulement les valeurs non NULL de cette colonne. COUNT(DISTINCT column) compte les valeurs distinctes non nulles.
  • En PySpark, utiliser respectivement F.count("*"), F.count("column") et F.count_distinct("column").
  • count_distinct est exact et plus coûteux.
  • approx_count_distinct utilise HyperLogLog++ et échange un peu de précision contre de meilleures performances. Son second argument est la déviation standard relative maximale autorisée (relativeSD/rsd) : 0.02 vise 2 %, sans constituer une borne d'erreur absolue garantie pour chaque résultat ; la valeur par défaut est 0.05.
  • count_if(condition) retourne le nombre de lignes pour lesquelles l'expression booléenne vaut TRUE ; FALSE et NULL ne sont pas comptés. Il remplace lisiblement SUM(CASE WHEN condition THEN 1 ELSE 0 END).
  • summary() ou describe() fournit des statistiques descriptives ; ce n'est pas un groupBy métier.

En SQL, choisir explicitement les statistiques souhaitées, notamment les percentiles approximatifs :

SQL
SELECT
  COUNT(amount_billed) AS count,
  AVG(amount_billed) AS mean,
  STDDEV(amount_billed) AS stddev,
  MIN(amount_billed) AS min,
  percentile_approx(amount_billed, ARRAY(0.25, 0.5, 0.75)) AS percentiles,
  MAX(amount_billed) AS max
FROM dev.silver.billing;

En PySpark, summary() accepte une sélection de statistiques et des percentiles arbitraires exprimés comme chaînes :

PYTHON
(billing_df.select("amount_billed")
           .summary("count", "mean", "stddev", "25%", "50%", "90%", "max")
           .show())

Les percentiles de summary() sont approximatifs et son schéma de sortie est destiné à l'exploration, pas à une interface de production stable. Pour des métriques métier versionnées ou groupées, utiliser des agrégations explicites.

describe() retourne l'ensemble fixe count, mean, stddev, min, max. summary() ajoute par défaut des quartiles approximatifs et permet de choisir les statistiques comme ci-dessus. Les deux acceptent des colonnes numériques ou STRING ; pour une chaîne non numérique, mean et stddev valent NULL. Ce sont des outils exploratoires dont le schéma de sortie n'est pas garanti comme contrat stable.

Exemple avec trois lignes, dont deux montants non nuls :

SQL
SELECT
  COUNT(*) AS rows_count,             -- 3
  COUNT(amount) AS known_amounts,     -- 2
  COUNT(DISTINCT amount) AS amounts   -- valeurs distinctes non NULL
FROM VALUES (10), (NULL), (10) AS t(amount);
PYTHON
counts_df = values_df.agg(
    F.count("*").alias("rows_count"),
    F.count("amount").alias("known_amounts"),
    F.count_distinct("amount").alias("distinct_amounts"),
)

collect_list et collect_set agrègent plusieurs lignes dans un tableau. Les deux ignorent les NULL et leur ordre n'est pas déterministe après un shuffle ; appliquer sort_array si l'ordre du résultat compte.

SQL
SELECT customer_id,
       collect_list(product_id) AS products_with_duplicates,
       collect_set(product_id)  AS unique_products
FROM dev.silver.orders
GROUP BY customer_id;
PYTHON
products_df = orders_df.groupBy("customer_id").agg(
    F.collect_list("product_id").alias("products_with_duplicates"),
    F.collect_set("product_id").alias("unique_products"),
)

Mémo : collect_list conserve les doublons ; collect_set les supprime.

4.2.3 Choisir la sortie Gold : tables, vues et objets de pipeline

Objet Données stockées ? Mise à jour Usage
Table Oui Job, DML ou pipeline Contrôle complet, sortie persistée
Vue Non, seulement la requête Résultat recalculé à chaque lecture Logique légère et toujours actuelle
Vue matérialisée Oui REFRESH géré, souvent incrémental lorsque possible Agrégats ou joins BI coûteux
Streaming table Oui Incrémentale depuis une ou plusieurs sources Ingestion/traitement continu ou micro-batch
Vue temporaire Non, limitée à la session/pipeline Recalculée Étape intermédiaire non publiée

Une vue matérialisée n'est pas une simple vue mise en cache. Elle possède un résultat persisté et un mécanisme de rafraîchissement géré.

Pour une materialized view autonome gérée dans Unity Catalog, Databricks SQL peut définir directement sa cadence de refresh dans le DDL avec SCHEDULE [REFRESH] — fonctionnalité actuellement Beta. Les mots-clés SCHEDULE et SCHEDULE REFRESH sont équivalents :

SQL
CREATE OR REPLACE MATERIALIZED VIEW dev.gold.daily_revenue
  SCHEDULE REFRESH CRON '0 0 0 * * ?' AT TIME ZONE 'UTC'
AS
SELECT
  CAST(order_ts AS DATE) AS order_date,
  SUM(amount) AS revenue
FROM dev.silver.orders
GROUP BY CAST(order_ts AS DATE);

-- Refresh ponctuel, indépendant du schedule.
REFRESH MATERIALIZED VIEW dev.gold.daily_revenue;

Le cron est un Quartz cron à six champs : secondes, minutes, heures, jour du mois, mois, jour de la semaine ; ? laisse l'un des deux champs « jour » non spécifié. L'exemple déclenche un refresh quotidien à minuit UTC. Pour une cadence simple, SCHEDULE EVERY 1 DAY est plus lisible. Le schedule crée une orchestration gérée pour l'objet autonome ; lorsqu'une materialized view est définie dans un pipeline explicite, planifier plutôt le pipeline avec Lakeflow Jobs. DESCRIBE EXTENDED permet de vérifier le schedule et le dernier statut de refresh.

Le mot « stockée » peut prêter à confusion : une vue Unity Catalog stocke sa définition, pas les lignes de son résultat.

SQL
-- Vue persistante et gouvernée dans Unity Catalog.
CREATE OR REPLACE VIEW dev.gold.active_customers AS
SELECT customer_id, country
FROM dev.silver.customers
WHERE status = 'ACTIVE';

-- Vue intermédiaire privée à un pipeline.
CREATE TEMPORARY VIEW recent_orders AS
SELECT *
FROM dev.silver.orders
WHERE order_ts >= current_timestamp() - INTERVAL 7 DAYS;

Dans un pipeline, CREATE TEMPORARY VIEW :

  • reste privée au pipeline qui la définit ;
  • n'est pas enregistrée dans Unity Catalog ;
  • persiste pendant la durée de vie du pipeline, pas seulement pendant une cellule ;
  • sert à factoriser une étape intermédiaire réutilisée par d'autres datasets du pipeline.

CREATE VIEW, au contraire, publie une définition interrogeable et gouvernée dans Unity Catalog.

Version Python propre à un Lakeflow pipeline :

PYTHON
from pyspark import pipelines as dp
from pyspark.sql import functions as F

@dp.temporary_view(name="recent_orders")
def recent_orders():
    return spark.read.table("dev.silver.orders").filter(
        F.col("order_ts") >= F.current_timestamp() - F.expr("INTERVAL 7 DAYS")
    )

@dp.temporary_view est l'équivalent pipeline de CREATE TEMPORARY VIEW. Hors pipeline, df.createOrReplaceTempView(...) crée une vue limitée à la SparkSession ; pour publier une vue Unity Catalog persistante, SQL reste l'interface naturelle.

4.3 Lakeflow Spark Declarative Pipelines

Dans un pipeline déclaratif, on décrit les datasets et leurs dépendances ; Databricks construit le graphe, gère l'ordre, le checkpointing, les retries et l'observabilité.

SQL
CREATE OR REFRESH STREAMING TABLE dev.bronze.orders
COMMENT 'Commandes brutes ingérées incrémentalement'
TBLPROPERTIES ('quality' = 'bronze')
AS
SELECT
  *,
  _metadata.file_name AS source_file,
  current_timestamp() AS ingested_at
FROM STREAM read_files(
  '/Volumes/dev/landing/raw/orders',
  format => 'json'
);
SQL
CREATE OR REFRESH MATERIALIZED VIEW dev.gold.daily_revenue
COMMENT 'Chiffre d’affaires journalier'
TBLPROPERTIES ('quality' = 'gold')
AS
SELECT
  CAST(order_ts AS DATE) AS order_date,
  SUM(amount) AS revenue,
  COUNT(DISTINCT order_id) AS order_count
FROM dev.silver.orders
GROUP BY CAST(order_ts AS DATE);

Versions Python dans un pipeline :

PYTHON
from pyspark import pipelines as dp
from pyspark.sql import functions as F

@dp.table(name="dev.bronze.orders", comment="Commandes brutes")
def bronze_orders():
    return (
        spark.readStream.format("cloudFiles")
             .option("cloudFiles.format", "json")
             .load("/Volumes/dev/landing/raw/orders")
    )

@dp.materialized_view(name="dev.gold.daily_revenue")
def daily_revenue():
    return (
        spark.read.table("dev.silver.orders")
             .groupBy(F.to_date("order_ts").alias("order_date"))
             .agg(
                 F.sum("amount").alias("revenue"),
                 F.count_distinct("order_id").alias("order_count"),
             )
    )

SQL convient bien aux déclarations statiques et lisibles. Les décorateurs Python sont plus adaptés lorsque les définitions sont factorisées dans des fonctions ou générées par une logique programmatique.

Raccourci mental : source incrémentale et état qui progresse → streaming table ; résultat d'une requête à maintenir → materialized view.

Updates, full refresh et évolution du pipeline

Une update ordinaire conserve l'état du pipeline :

  • une streaming table lit seulement les nouveaux enregistrements depuis son checkpoint ;
  • une materialized view est rafraîchie à partir de l'état cohérent de ses sources ; Databricks choisit un traitement incrémental lorsqu'il est possible et rentable, sinon peut recalculer le résultat.

Un full refresh repart des sources :

  • pour une streaming table, il tronque la cible, efface la progression du flow et retraite toutes les données source encore disponibles ;
  • pour une materialized view, il recalcule intégralement le résultat.

Un full refresh est donc une opération explicite de pipeline, déclenchée depuis l'UI, un Job ou une API. Il ne constitue pas un type de table et ne s'exécute pas à chaque update. Choisir l'objet selon le traitement attendu : streaming table ou materialized view. Attention : un full refresh peut être impossible ou incomplet si Kafka, le CDF ou les fichiers ont déjà dépassé leur rétention.

Si une colonne d'une table amont est renommée, mettre à jour toutes les définitions downstream qui référencent l'ancien nom. Si le changement est incompatible avec le schéma/checkpoint d'une streaming table, ou si les anciennes lignes doivent être recalculées avec le nouveau nom, valider puis lancer un full refresh contrôlé. Un simple renommage du code amont ne répare pas automatiquement les consommateurs.

Réserver le full refresh aux reconstructions nécessaires :

SQL
REFRESH STREAMING TABLE dev.silver.orders FULL;

Cette forme SQL reconstruit une streaming table autonome. Pour reconstruire l'update d'un pipeline, utiliser l'UI ou la CLI :

BASH
databricks pipelines start-update <pipeline-id> --full-refresh

Cette commande réinitialise et recalcule les tables du pipeline. Elle peut être coûteuse et rompt l'hypothèse d'un traitement uniquement incrémental : ne pas en faire le mode normal de production. Ne pas la confondre avec databricks pipelines run --full-refresh-all, proposée par les commandes de projet plus récentes, ni avec un refresh sélectif de tables.

Pour l'exploitation, distinguer les modes de lancement :

  • Run now depuis l'éditeur privilégie l'itération rapide, réutilise temporairement le compute et désactive les retries du pipeline ;
  • un lancement par Lakeflow Jobs, API ou en mode continuous privilégie la fiabilité, avec retry/restart sur certaines erreurs récupérables et arrêt du compute après un run triggered.

Pour planifier un pipeline triggered, créer dans Lakeflow Jobs une tâche de type Pipeline et lui associer le schedule ou le trigger voulu. Un pipeline continuous reste actif et n'a pas besoin d'un lancement horaire récurrent. En production, préférer ainsi un pipeline versionné et orchestré plutôt que des lancements interactifs depuis l'éditeur.

Lakeflow Pipelines Editor et organisation multi-fichiers

Le Lakeflow Pipelines Editor, anciennement présenté comme Multi-File Editor, est l'éditeur actuel pour développer et déboguer un pipeline composé de plusieurs fichiers :

  • fichiers SQL et Python dans un même pipeline, un seul langage par fichier ;
  • dossier transformations par défaut et organisation libre en sous-dossiers ;
  • dépendances résolues entre les datasets, même s'ils sont définis dans des fichiers différents ;
  • onglets multiples, graphe du pipeline et résultats dans le panneau inférieur ;
  • Dry run pour valider définitions et dépendances sans publier de données ;
  • exécution de tout le pipeline, d'un fichier, d'une sélection de tables ou d'une seule table.

Lancer un seul fichier accélère le débogage, mais les autres fichiers ne sont alors pas réévalués : certaines erreurs ou dépendances obsolètes peuvent être masquées. Avant la production, exécuter ou valider tous les fichiers du pipeline.

Paramètres et Configuration d'un pipeline

Deux mécanismes coexistent :

Mécanisme Usage Référence dans le code
Pipeline Parameters (Beta) Valeurs dynamiques qui changent par update, par exemple source, date ou environnement ; mécanisme actuel pour le SQL :source_path ou IDENTIFIER(:target_schema)
Configuration Propriétés Spark/structurelles ; mécanisme historique, toujours requis pour paramétrer du code Python ${source} en SQL, spark.conf.get("source") en Python

Pour transmettre target_schema d'un Job à une Pipeline task :

  1. définir le job parameter target_schema ; Lakeflow Jobs le transmet automatiquement à la Pipeline task ;
  2. ou ajouter dans Pipeline task > Parameters la clé target_schema et, par exemple, la valeur dynamique {{job.parameters.target_schema}} pour rendre le mapping explicite ;
  3. le référencer dans le code SQL avec :target_schema. Comme un nom de schéma est un identifiant et non une valeur, utiliser IDENTIFIER(...).
SQL
USE SCHEMA IDENTIFIER(:target_schema);

CREATE OR REFRESH STREAMING TABLE clean_sensors
AS SELECT * FROM STREAM(dev.bronze.sensors);

Autre exemple avec un chemin, qui est une valeur et ne nécessite pas IDENTIFIER :

SQL
CREATE OR REFRESH STREAMING TABLE dev.bronze.events
AS
SELECT *
FROM STREAM read_files(:source_path, format => 'json');

Les paramètres nommés sont actuellement réservés au SQL. Pour du code Python de pipeline, renseigner la clé dans le champ Configuration des paramètres du pipeline, puis la lire avec spark.conf.get :

TEXT
Configuration : target_schema = dev_silver
Configuration : source = /Volumes/dev/landing/events
SQL
-- Ancienne syntaxe SQL, encore prise en charge avec Configuration.
CREATE OR REFRESH STREAMING TABLE dev.bronze.events
AS
SELECT *
FROM STREAM read_files('${source}', format => 'json');

En Python :

PYTHON
from pyspark import pipelines as dp

target_schema = spark.conf.get("target_schema")

@dp.table(name=f"{target_schema}.clean_sensors")
def clean_sensors():
    return spark.readStream.table("dev.bronze.sensors")

Pour une question actuelle demandant comment transmettre un paramètre dynamique d'un Job à du SQL dans une Pipeline task, choisir donc Parameters, et non Configuration. Configuration reste la bonne réponse si l'énoncé mentionne du code Python, spark.conf.get, une propriété Spark ou l'ancien mécanisme ${clé}.

Attention à la date du support : les QCM antérieurs à l'introduction des Pipeline Parameters peuvent donner Configuration comme réponse générale. C'est le mécanisme historique encore valide ; dans l'interface actuelle, utiliser Parameters pour une valeur dynamique consommée par le SQL lorsque la fonctionnalité Beta est activée.

Les Pipeline Parameters peuvent être surchargés au lancement, par une Pipeline task de Lakeflow Jobs ou par API. Pour une même clé, l'ordre de priorité est : valeur du run, paramètre du Job, paramètre de la task, puis valeur par défaut du pipeline. Toutes les valeurs sont transmises comme des chaînes.

4.4 Structured Streaming

Structured Streaming exécute généralement une requête en micro-batches. Une transformation stateless, comme un filtre, peut traiter chaque lot sans mémoire du passé. Une agrégation par fenêtre, une déduplication ou un stream-stream join est stateful : Spark conserve un état entre les micro-batches et le sauvegarde dans le checkpoint.

4.4.1 Triggers, checkpoints et garanties de traitement

Le trigger décide quand traiter les données disponibles ; il ne remplace pas le checkpoint :

PYTHON
# Historique : un seul micro-batch, puis arrêt.
once_query = (
    events_df.writeStream
             .option("checkpointLocation", "/Volumes/dev/checkpoints/events_once")
             .trigger(once=True)
             .toTable("dev.bronze.events_once")
)

# Recommandé pour un traitement incrémental fini : un ou plusieurs micro-batches.
available_query = (
    events_df.writeStream
             .option("checkpointLocation", "/Volumes/dev/checkpoints/events_available")
             .trigger(availableNow=True)
             .toTable("dev.bronze.events_available")
)

Trigger.Once traite les données disponibles dans un seul micro-batch, puis s'arrête ; il est déprécié sur les runtimes modernes. AvailableNow traite tout le backlog disponible en un ou plusieurs micro-batches, respecte les limites de débit et s'arrête : c'est le choix à privilégier pour un job incrémental fini. Il n'existe pas de clause SQL Structured Streaming générale pour ces triggers ; un pipeline SQL est déclenché par la configuration Pipeline/Job.

Le checkpoint doit résider dans un stockage durable et rester propre à la requête. Il contient la progression (fichiers ou offsets), les commits et l'état des opérateurs stateful. Le supprimer ne « répare » pas le stream : cela crée une nouvelle requête et peut provoquer un reprocessing complet des données encore visibles par la source. Le checkpoint rend la reprise possible, mais l'exactly-once de bout en bout dépend aussi d'une source rejouable et d'un sink transactionnel/idempotent.

Une streaming table ne garantit pas un ordre global d'arrivée. Auto Loader ne promet pas l'ordre des fichiers ; Kafka ne garantit l'ordre qu'à l'intérieur d'une partition. Un checkpoint garantit la progression/reprise, pas l'ordre métier. Si les événements peuvent arriver hors ordre, conserver un temps d'événement ou un numéro de séquence et utiliser watermark, déduplication ou AUTO CDC ... SEQUENCE BY selon le besoin. L'option withEventTimeOrder répond à un cas particulier de snapshot Delta initial ; elle ne transforme pas tous les streams en flux strictement ordonnés.

4.4.2 État, watermarks et output modes

Watermarks et données en retard

Un watermark indique combien de retard par rapport au temps d'événement Spark accepte avant de considérer qu'un ancien état peut être supprimé. Il borne donc l'état et la mémoire ; ce n'est ni une fenêtre ni une garantie qu'une ligne arrivera à temps.

Version SQL dans un Lakeflow pipeline :

SQL
CREATE OR REFRESH STREAMING TABLE dev.gold.event_counts AS
SELECT window(event_ts, '5 minutes') AS event_window,
       event_type,
       COUNT(*) AS event_count
FROM (
  SELECT * FROM STREAM(dev.silver.events)
  WATERMARK event_ts DELAY OF INTERVAL 10 MINUTES
)
GROUP BY window(event_ts, '5 minutes'), event_type;

Version PySpark :

PYTHON
from pyspark.sql import functions as F

counts_df = (
    events_df
      .withWatermark("event_ts", "10 minutes")
      .groupBy(F.window("event_ts", "5 minutes"), "event_type")
      .count()
)

Un délai court libère l'état plus tôt mais risque d'écarter davantage de données tardives ; un délai long augmente la complétude au prix de plus d'état. Le watermark progresse d'après le plus grand temps d'événement observé, pas d'après l'horloge murale.

Output modes

Le mode de sortie détermine quelles lignes du résultat sont envoyées au sink à chaque trigger :

Mode Lignes émises Usage/limite
append Seulement les nouvelles lignes finalisées ; mode par défaut Naturel pour les événements et obligatoire pour les joins streaming
update Seulement les lignes modifiées depuis le trigger précédent Utile pour un agrégat ; pas pris en charge directement par tous les sinks, notamment une table Delta
complete L'intégralité de la table de résultat à chaque trigger Agrégations uniquement ; coûteux si l'état est grand
PYTHON
query = (
    counts_df.writeStream
             .outputMode("update")       # ou "append" / "complete"
             .format("console")
             .option("checkpointLocation", "/Volumes/dev/checkpoints/event_counts")
             .start()
)

Il n'existe pas de clause SQL générale OUTPUT MODE dans un SELECT : dans un Lakeflow pipeline SQL, la streaming table et le moteur de pipeline gèrent la matérialisation. Dans l'API Structured Streaming, choisir explicitement .outputMode(...) selon l'opérateur et les capacités du sink. Pour appliquer des mises à jour à une table Delta, utiliser souvent foreachBatch avec MERGE.

4.4.3 foreachBatch()

foreachBatch appelle une fonction Python pour chaque micro-batch avec (batch_df, batch_id). Il sert à exécuter une opération batch non disponible comme sink streaming natif, notamment un MERGE, ou à écrire vers plusieurs destinations.

PYTHON
from pyspark.sql import functions as F
from pyspark.sql.window import Window

def upsert_kafka_events(batch_df, batch_id):
    if batch_df.isEmpty():
        return

    # Déduplication déterministe à l'intérieur du micro-batch.
    window = Window.partitionBy("event_id").orderBy(
        F.col("timestamp").desc(),
        F.col("partition").desc(),
        F.col("offset").desc(),
    )
    deduped = (
        batch_df.withColumn("_rn", F.row_number().over(window))
                .filter("_rn = 1")
                .drop("_rn")
    )
    deduped.createOrReplaceTempView("events_micro_batch")

    spark.sql("""
      MERGE INTO dev.silver.events AS t
      USING events_micro_batch AS s
      ON t.event_id = s.event_id
      WHEN MATCHED THEN UPDATE SET *
      WHEN NOT MATCHED THEN INSERT *
    """)

(kafka_events_df.writeStream
           .foreachBatch(upsert_kafka_events)
           .option("checkpointLocation", "/Volumes/dev/checkpoints/events_upsert")
           .start())

Le callback est Python, mais le traitement du micro-batch peut employer SQL comme ci-dessus ou l'API DeltaTable. foreachBatch fonctionne en micro-batch, pas en continuous processing, et fournit une garantie at-least-once. Rendre l'écriture idempotente avec une clé stable, un MERGE, ou batch_id pour éviter les doublons après reprise. Une fonction doit aussi accepter un batch vide et consommer entièrement le DataFrame lorsqu'un opérateur stateful se trouve en amont.

Pour dédupliquer un flux Kafka, décoder une clé métier stable telle que event_id, dédupliquer d'abord les occurrences concurrentes du micro-batch, puis effectuer un MERGE dans foreachBatch. L'offset Kafka identifie un message dans (topic, partition), mais deux publications du même événement métier peuvent avoir deux offsets : le MERGE doit donc utiliser event_id lorsque l'objectif est la déduplication métier. Une cible append sans clé réinsérera les doublons lors d'un retry. Dans cet exemple, renommer idéalement timestamp en kafka_timestamp avant la Silver pour distinguer le temps d'ingestion Kafka du temps métier contenu dans l'événement.

4.4.4 Streaming joins

Trois patterns doivent être distingués.

Stream-static join

Une source streaming est jointe à une table Delta principalement statique, par exemple un flux de commandes avec une dimension clients. Le join est stateless : chaque nouveau micro-batch utilise la version valide la plus récente de la table statique.

SQL
CREATE OR REFRESH STREAMING TABLE dev.silver.enriched_orders
AS
SELECT o.*, c.segment
FROM STREAM(dev.silver.orders) AS o
LEFT JOIN dev.silver.customers AS c
  ON o.customer_id = c.customer_id;

Version PySpark dans un pipeline :

PYTHON
from pyspark import pipelines as dp

@dp.table(name="dev.silver.enriched_orders")
def enriched_orders():
    orders = spark.readStream.table("dev.silver.orders")
    customers = spark.read.table("dev.silver.customers")
    return orders.join(customers, "customer_id", "left")

Si la dimension change après le traitement d'une ancienne commande, cette ancienne sortie n'est pas recalculée automatiquement. Il faut un full refresh pour la réenrichir. Aucun watermark n'est nécessaire pour ce join stateless.

Join via une materialized view

Une vue matérialisée peut joindre deux tables, y compris deux streaming tables déjà matérialisées. La requête s'exprime en sémantique batch, sans STREAM(...), et reflète l'état cohérent des sources lors du refresh :

SQL
CREATE OR REFRESH MATERIALIZED VIEW dev.gold.order_payments
AS
SELECT o.order_id, o.amount, p.payment_status
FROM dev.silver.orders AS o
INNER JOIN dev.silver.payments AS p
  ON o.order_id = p.order_id;

Version PySpark :

PYTHON
from pyspark import pipelines as dp

@dp.materialized_view(name="dev.gold.order_payments")
def order_payments():
    return spark.read.table("dev.silver.orders").join(
        spark.read.table("dev.silver.payments"), "order_id", "inner"
    )

Databricks tente de ne recalculer que les changements nécessaires. Une materialized view n'implique donc pas automatiquement un full refresh à chaque update ; elle est souvent le choix le plus simple pour maintenir un join analytique correct.

Stream-stream join

Deux sources streaming sont jointes directement. Le join est stateful : Spark doit mémoriser les lignes susceptibles de trouver une correspondance future. C'est un pattern avancé.

Version SQL avec état borné :

SQL
CREATE OR REFRESH STREAMING TABLE dev.silver.matched_payments AS
SELECT o.order_id, o.amount, p.payment_status
FROM (SELECT * FROM STREAM(dev.silver.orders))
  WATERMARK order_ts DELAY OF INTERVAL 30 MINUTES AS o
JOIN (SELECT * FROM STREAM(dev.silver.payments))
  WATERMARK payment_ts DELAY OF INTERVAL 30 MINUTES AS p
  ON o.order_id = p.order_id
 AND p.payment_ts BETWEEN o.order_ts AND o.order_ts + INTERVAL 1 HOUR;

Version PySpark :

PYTHON
from pyspark.sql import functions as F

orders = (
    spark.readStream.table("dev.silver.orders")
         .withWatermark("order_ts", "30 minutes")
)
payments = (
    spark.readStream.table("dev.silver.payments")
         .withWatermark("payment_ts", "30 minutes")
)

matched = orders.join(
    payments,
    (orders.order_id == payments.order_id)
    & (payments.payment_ts >= orders.order_ts)
    & (payments.payment_ts <= orders.order_ts + F.expr("INTERVAL 1 HOUR")),
    "inner",
)

Définir des watermarks sur les deux côtés et une contrainte temporelle permet d'évacuer l'ancien état. Sans bornes, l'état peut croître indéfiniment. Les joins outer ajoutent des contraintes de watermark et de délai plus strictes.


5. Orchestration avec Lakeflow Jobs

5.1 Job, tâches, DAG et compute

  • Un Job représente un workflow reproductible.
  • Une Task est une unité d'exécution : notebook, script Python, requête SQL, dashboard, pipeline, JAR, dbt, autre Job, etc.
  • Un Trigger décide quand lancer le Job.
  • Les dépendances forment un DAG : graphe orienté sans cycle.

Formes de DAG à reconnaître :

TEXT
Séquence : ingest -> clean -> aggregate

Fan-out :  ingest -> sales
                   -> inventory
                   -> customers

Funnel : source_A ->
                     join -> publish
         source_B ->

Lakeflow Jobs lance autant de tâches prêtes que possible en parallèle. Deux tâches sont prêtes en même temps si leurs dépendances sont satisfaites ; deux tâches racines, ou deux enfants du même parent, n'ont aucune raison d'attendre l'une l'autre. Une tâche de funnel attend en revanche toutes les dépendances exigées.

TEXT
ingest terminé
   ├── clean_sales       ┐ exécutées en parallèle
   └── clean_inventory   ┘ si le compute le permet
             ↓
      publish (attend les deux)

Le parallélisme réel reste borné par le compute, les quotas et la concurrence configurée. Sur un job cluster partagé, des tâches simultanées se partagent ses ressources ; avec des computes distincts, elles peuvent démarrer séparément mais avec leur propre coût et délai de démarrage. Ne pas ajouter une dépendance uniquement pour ordonner visuellement le DAG : elle sérialise le travail. Cette notion relève de la configuration du Job, pas du code SQL/Python des notebooks.

Affecter le compute aux tâches

La section 2.2 Choisir le compute compare les familles de compute et leurs cas d'usage. Ici, il faut surtout savoir quel moteur exécute chaque tâche du Job :

Type de tâche Compute typique
Notebook/Python Serverless jobs ou job compute
Pipeline Compute configuré par le pipeline, souvent serverless
SQL query/dashboard SQL warehouse
Développement interactif All-purpose compute, pas recommandé pour un job de production récurrent

Privilégier un job compute isolé ou serverless pour la production. Un cluster all-purpose déjà actif peut sembler rapide et économique pour un test, mais il est moins reproductible, mélange les bibliothèques et facture les périodes d'inactivité. Le notebook et les autres tâches Spark s'exécutent dans le compute plane ; leur code reste un objet du workspace stocké dans le control plane.

5.2 Triggers, concurrence et queueing

Choisir un trigger

Trigger Utiliser quand
Scheduled L'heure métier impose l'exécution, même si aucune donnée n'est arrivée
File arrival De nouveaux fichiers dans un emplacement Unity Catalog doivent déclencher le traitement
Table update Le Job dépend de la mise à jour d'une table source
Continuous Le Job doit rester actif et redémarrer après une fin ou un échec selon la configuration
Manuel/API Test, backfill ou orchestration externe

Un trigger data-driven réduit les runs à vide et la latence. Un horaire reste pertinent si le SLA dépend d'une heure précise ou si la source n'offre pas de signal fiable.

Un trigger Table update détecte les commits qui modifient la table surveillée, y compris UPDATE, MERGE et DELETE. Le lancement n'est toutefois pas une réaction garantie à la milliseconde : la détection, l'intervalle minimal entre deux runs et l'éventuel délai d'attente après le dernier changement peuvent retarder le démarrage.

Un orchestrateur externe comme Airflow peut représenter une dépendance explicite vers un Job existant avec DatabricksRunNowOperator, qui appelle l'API jobs/run-now après la réussite de ses tâches amont. L'appel retourne un run_id, mais ne garantit pas que le compute démarre instantanément : queueing, concurrence, capacité et temps de démarrage restent applicables. Si l'appel HTTP est retenté directement, fournir un idempotency_token stable évite de créer deux runs ; DatabricksSubmitRunOperator soumet au contraire une définition de run sans exiger de Job préexistant.

Dans les versions actuelles de Lakeflow Jobs, un même Job peut avoir plusieurs triggers ; les tables système représentent d'ailleurs ce cas par trigger_type = 'MULTIPLE'. L'ancienne règle « un seul trigger par Job, donc créer deux Jobs » ne doit plus être apprise comme une contrainte générale. Si une combinaison n'est pas proposée dans un ancien workspace ou n'est pas compatible avec la configuration visée, deux Jobs appelant une même ressource restent un contournement possible.

Queueing et concurrence des runs

max_concurrent_runs limite le nombre de runs simultanés d'un même Job ; sa valeur par défaut est 1. Si un run planifié arrive alors que cette limite — ou une limite de concurrence du workspace — est atteinte :

  • queueing activé : le run reste en état queued et démarre plus tard lorsqu'une capacité se libère, pendant au plus 48 heures ;
  • queueing désactivé : le run qui dépasse la limite est ignoré/skipped au lieu d'attendre.

Ainsi, plusieurs runs planifiés qui démarrent en retard pendant les heures de pointe indiquent souvent que Queue est activé et qu'une limite de concurrence ou de capacité est atteinte. Vérifier l'état Queued, Advanced settings > Queue, la valeur de concurrent runs et la durée des runs précédents avant d'augmenter la concurrence : des exécutions qui se chevauchent doivent rester sûres pour les mêmes tables cibles.

Exemple dans un bundle :

YAML
resources:
  jobs:
    daily_orders:
      name: daily-orders
      max_concurrent_runs: 1
      queue:
        enabled: true
      # trigger: ...
      # tasks: ...

La syntaxe YAML peut dépendre de la version du schéma bundle ; databricks bundle validate permet de la contrôler. La notion d'examen est surtout : limite atteinte + queueing = retard ; limite atteinte sans queueing = run skipped.

5.3 Dépendances et conditions Run if

Une tâche en aval peut s'exécuter selon le résultat de ses dépendances :

  • All succeeded : toutes ont réussi, comportement usuel par défaut ;
  • At least one succeeded ;
  • None failed : aucune n'a échoué et au moins une a été exécutée ;
  • All done : exécuter même après un échec, utile pour le nettoyage ;
  • At least one failed ou All failed : notifications ou traitement d'erreur.

Une dépendance échouée peut rendre une tâche upstream failed/skipped sans que le code de cette tâche ait été lancé. Commencer le diagnostic par le premier échec en amont.

5.4 Branches, boucles et valeurs de tâches

  • Une tâche If/else évalue une expression et ouvre une branche true ou false.
  • Une tâche For each exécute une tâche imbriquée pour chaque élément d'un tableau, éventuellement en parallèle. Avec un tableau vide [], elle ne lance aucune itération ; ce n'est pas en soi une erreur.
  • Pour que les tâches situées après les deux branches puissent continuer, utiliser une condition Run if cohérente, souvent None failed, plutôt que d'exiger que les deux branches aient réussi alors qu'une seule est exécutée.

Une tâche SQL peut exposer la première ligne de son résultat :

SQL
SELECT count(*) AS record_count
FROM dev.bronze.orders;

Une tâche en aval peut alors utiliser {{tasks.ingest_sql.output.first_row.record_count}}. Pour une valeur JSON arbitraire ou une liste destinée à un For each, préférer taskValues.set() en Python.

Exemple de valeur produite par un notebook Python :

PYTHON
record_count = spark.table("dev.bronze.orders").count()

dbutils.jobs.taskValues.set(
    key="record_count",
    value=record_count,
)

dbutils.jobs.taskValues est disponible uniquement en Python : il n'existe pas de setter SQL équivalent.

Référence dynamique depuis une tâche suivante ou un champ de configuration :

TEXT
{{tasks.ingest.values.record_count}}

La syntaxe exacte contient le namespace pluriel values. {{tasks.ingest.value}} ou {{tasks.ingest.record_count}} ne référence pas une task value.

Exemple de configuration d'une tâche If/else dans l'UI :

TEXT
Condition (gauche) : {{tasks.ingest.values.record_count}}
Opérateur          : GREATER_THAN
Valeur (droite)    : 0

Le champ accepte une référence dynamique via le bouton { }. Les doubles accolades ne calculent pas une formule arbitraire : elles sont remplacées par une valeur. L'opérateur et la valeur de comparaison se configurent séparément dans la tâche If/else.

Pour une condition basée sur l'état plutôt que sur une valeur personnalisée :

TEXT
{{tasks.validate.result_state}} == success

Après les branches true et false, une tâche de jonction utilise souvent None failed afin de poursuivre après la seule branche effectivement exécutée.

5.5 Paramètres et références dynamiques

Les job parameters sont globaux au run et peuvent être propagés aux tâches. Les task parameters configurent une tâche particulière.

Dans un notebook ou une tâche SQL paramétrée, utiliser des marqueurs nommés :

SQL
SELECT *
FROM dev.silver.orders
WHERE order_date = CAST(:run_date AS DATE)
  AND environment = :environment;

Dans un notebook Python, lire les paramètres avec les widgets :

PYTHON
environment = dbutils.widgets.get("environment")
run_date = dbutils.widgets.get("run_date")

Le Job fournit les mêmes clés aux deux langages ; seule la syntaxe de lecture change : dbutils.widgets.get(...) en Python, :nom en SQL.

Références dynamiques courantes :

TEXT
{{job.run_id}}
{{job.start_time.iso_date}}
{{job.parameters.environment}}
{{task.name}}
{{task.retry_count}}
{{tasks.validate.values.record_count}}

Les valeurs dynamiques évitent de coder en dur les dates, chemins, identifiants de run et sorties de tâches.

5.6 Fiabilité : retries, idempotence et repair run

Retry et idempotence

Une retry policy de tâche définit notamment :

  • Maximum retries : nombre maximal de nouvelles tentatives après l'échec initial ;
  • Minimum retry interval : délai minimal entre deux tentatives, utile pour laisser une dépendance récupérer ou respecter un rate limit ;
  • Retry on timeout : indique si un dépassement du timeout peut être rejoué ;
  • éventuellement un timeout propre à la tâche.

Les retries conviennent aux erreurs transitoires : indisponibilité réseau, limite temporaire d'une API, capacité cloud. La retry policy du Job fournit notamment un intervalle minimal ; dans le client d'une API REST personnalisée, utiliser plutôt un backoff exponentiel, idéalement avec un léger aléa (jitter) : le délai augmente après chaque échec pour laisser au service ou à sa fenêtre de rate limit le temps de récupérer.

Ils ne corrigent pas une erreur déterministe de schéma ou de code. Chaque tâche susceptible d'être rejouée doit être idempotente :

  • MERGE sur une clé stable ;
  • overwrite d'une partition déterminée ;
  • COPY INTO ;
  • identifiant de requête source mémorisé ;
  • éviter un append aveugle après une écriture partielle.

Réparer ou relancer

  • Rerun relance normalement le workflow complet.
  • Repair run relance les tâches non réussies d'un run multi-tâches et les tâches qui en dépendent, sans recalculer les tâches déjà réussies et non concernées.

Avant une réparation, corriger la configuration ou le code si nécessaire. Une réparation recommence une tâche échouée depuis le début ; Lakeflow Jobs ne rend pas automatiquement son écriture idempotente.

5.7 Monitoring des Jobs

Pour étudier un Job devenu progressivement plus lent après une mise en production, ouvrir Jobs & Pipelines > Jobs, sélectionner le Job, puis consulter l'onglet Runs. Il présente les runs actifs et terminés, avec un historique conservé pendant 60 jours. La liste unifiée est également accessible par Jobs & Pipelines > Runs ou par l'entrée Runs de la barre latérale lorsqu'elle est activée.

Dans l'UI :

  1. filtrer la période autour du changement de production et comparer durée, statut, erreurs et volume au baseline historique ;
  2. ouvrir le run, puis le DAG ;
  3. localiser la première tâche devenue lente ou réellement échouée ;
  4. lire son output, ses logs et ses métriques de compute ;
  5. comparer paramètres, compute, bibliothèques et version du code avec le dernier run sain ;
  6. réparer le run après correction.

Le tableau et les graphiques de l'onglet Runs permettent d'identifier une dérive de durée ou une hausse récurrente d'un type d'erreur. Ils montrent une corrélation temporelle, pas automatiquement la cause : ouvrir ensuite les tâches et le Spark UI pour confirmer si le changement vient du code, du volume, du plan, du skew, du compute ou d'une dépendance externe. Exporter ou matérialiser les données de monitoring si l'analyse doit couvrir plus de 60 jours.

Si la tâche lente est de type Pipeline, ouvrir son lien depuis le run. Le Pipeline UI affiche le graphe des flows/datasets, l'historique des updates et des métriques par streaming table ou materialized view — lignes, qualité, backlog et durée selon l'objet. Cela localise le dataset où le temps augmente ; confirmer ensuite la cause dans ses détails, l'event log et, si nécessaire, le Spark UI.

Les tables système permettent une analyse transverse :

SQL
SHOW TABLES IN system.lakeflow;

SELECT *
FROM system.lakeflow.job_run_timeline
WHERE period_start_time >= current_timestamp() - INTERVAL 7 DAYS;

Version PySpark pour poursuivre l'analyse dans un DataFrame :

PYTHON
from pyspark.sql import functions as F

runs = spark.table("system.lakeflow.job_run_timeline").filter(
    F.col("period_start_time") >= F.current_timestamp() - F.expr("INTERVAL 7 DAYS")
)

SQL est généralement plus direct pour l'investigation ad hoc ; PySpark devient utile si ces données alimentent une transformation ou une analyse automatisée.

Les noms et colonnes exacts des tables système peuvent évoluer. À l'examen, retenir surtout system.lakeflow pour l'historique des Jobs et pipelines, ainsi que la vue détaillée des runs dans l'UI.

5.8 Notifications : Job, task et combinaisons avancées

Les notifications peuvent cibler une adresse e-mail ou une destination administrée comme Slack, Microsoft Teams, PagerDuty ou un webhook.

Niveau Moment de notification Usage conseillé
Job État global du run Équipe d'exploitation, SLA global, succès/échec final
Task État d'une tâche précise Propriétaire d'une source ou étape critique, diagnostic fin

Événements courants :

  • Start ;
  • Success ;
  • Failure ;
  • Duration warning si le seuil attendu est dépassé ;
  • Streaming backlog si le retard du stream dépasse le seuil configuré.

Dans l'interface actuelle, les notifications globales se configurent dans Job details > Job notifications et celles d'une tâche dans le panneau de la task. Selon la version de l'UI, elles peuvent apparaître près de Schedules & Triggers/Notifications.

Différence importante avec les retries :

  • une notification d'échec au niveau Job n'est normalement envoyée qu'après l'issue globale du run ; elle n'est pas envoyée à chaque tentative échouée ;
  • une notification au niveau Task peut signaler les échecs intermédiaires ; activer Mute notifications until the last retry pour n'alerter qu'après la dernière tentative.

Stratégie raisonnable :

  1. Job Failure vers l'astreinte ou le canal d'exploitation ;
  2. Task Failure seulement pour les étapes critiques, vers leur équipe propriétaire ;
  3. Duration warning sur les workloads ayant un SLA ;
  4. Streaming backlog sur les flux continus ;
  5. Success pour les clôtures métier importantes, pas pour tous les runs fréquents ;
  6. filtrer les runs skipped/canceled si ces notifications n'appellent aucune action ;
  7. utiliser un webhook personnalisé si le consommateur exige un payload stable.

Les pipelines eux-mêmes proposent aussi des e-mails pour succès, échec, échec fatal/non récupérable ou échec d'un flow. Ne pas dupliquer sans raison les alertes du pipeline et celles de la pipeline task du Job.


6. Développement, tests et CI/CD

Fil conducteur : modulariser et tester localement → versionner dans Git → choisir les outils et l'authentification → décrire ressources et environnements avec un bundle → valider, déployer et tester l'intégration → promouvoir en production. Les API et SDK complètent ce cycle lorsqu'une automatisation personnalisée est nécessaire.

6.1 Modularisation, tests et dépendances

6.1.1 Modulariser le code

Séparer les transformations testables de l'orchestration Databricks :

PYTHON
# src/transformations.py
from pyspark.sql import functions as F

def clean_orders(df):
    return (
        df.filter(F.col("order_id").isNotNull())
          .withColumn("amount", F.col("amount").cast("decimal(12,2)"))
    )
PYTHON
# src/pipeline.py ou notebook d'orchestration
from transformations import clean_orders

orders = spark.table("dev.bronze.orders")
clean_orders(orders).write.mode("overwrite").saveAsTable("dev.silver.orders")

Une fonction DataFrame sans lecture, écriture ni widget interne se teste facilement. Garder dans le notebook ou le fichier pipeline l'acquisition des paramètres, les I/O et la déclaration des datasets.

6.1.2 Tester le code localement

Le cycle de validation progresse du code isolé vers le système Databricks complet :

TEXT
Tests unitaires → tests d'intégration → test de pipeline/système
  • Unitaire : fonction pure ou transformation DataFrame sur un petit jeu de données ;
  • intégration : lecture/écriture de tables de test, droits, paramètres et dépendances ;
  • système : workflow complet dans un environnement isolé avec les services Databricks réels.

Le test unitaire se place immédiatement après la modularisation. Exemple de fixture PySpark pour pytest local :

PYTHON
# tests/conftest.py
import pytest
from pyspark.sql import SparkSession

@pytest.fixture(scope="session")
def spark():
    session = (
        SparkSession.builder
        .master("local[2]")
        .appName("orders-tests")
        .getOrCreate()
    )
    yield session
    session.stop()
PYTHON
from pyspark.testing.utils import assertDataFrameEqual
from pyspark.sql import functions as F

def add_status(df):
    return df.withColumn(
        "status",
        F.when(F.col("value") == 0, "NORMAL").otherwise("UNKNOWN"),
    )

def test_add_status(spark):
    actual = add_status(spark.createDataFrame([(0,), (1,)], ["value"]))
    expected = spark.createDataFrame(
        [(0, "NORMAL"), (1, "UNKNOWN")],
        ["value", "status"],
    )
    assertDataFrameEqual(actual, expected)
BASH
pytest -q

Les tests d'intégration et de pipeline interviennent après le déploiement en environnement de développement, en 6.4.8.

6.1.3 Gérer les bibliothèques limitées au notebook avec %pip

Placé au début d'un notebook Python, %pip installe une dépendance dans l'environnement Python propre à ce notebook :

PYTHON
%pip install "my_package==1.2.3"

Après l'installation ou le changement d'une version, un redémarrage de Python peut être nécessaire :

PYTHON
dbutils.library.restartPython()

Les autres notebooks attachés au même compute ne voient pas cette bibliothèque, tandis que les Jobs qui exécutent ce notebook utilisent son environnement. Cette installation ne persiste pas entre les sessions ou après un détachement : conserver la cellule %pip au début du notebook. Pour une production multi-tâches, une wheel versionnée déclarée comme dépendance du Job ou du bundle rend généralement l'environnement plus explicite et évite de réinstaller le même package dans chaque notebook. Sur serverless environment version 5 ou supérieure, %uv pip est une alternative plus rapide qui partage le même environnement notebook.

6.2 Git folders et workflow Git

Les Databricks Git folders, anciennement Repos, synchronisent un dossier du workspace avec un dépôt distant.

Workflow attendu :

  1. créer ou sélectionner une branche de fonctionnalité ;
  2. modifier notebooks et fichiers ;
  3. tester ;
  4. commit puis push ;
  5. ouvrir une pull request dans le fournisseur Git ;
  6. faire relire et fusionner ;
  7. laisser la CI valider et déployer les bundles.

Connecter GitHub à Databricks

Pour GitHub hébergé, Databricks recommande aujourd'hui la Databricks GitHub App avec OAuth :

  1. dans Databricks, ouvrir le menu utilisateur puis Settings > Linked accounts ;
  2. choisir Add Git credential, fournisseur GitHub ;
  3. sélectionner Link Git account et autoriser la GitHub App ;
  4. limiter si possible l'installation aux dépôts nécessaires.

La GitHub App renouvelle les tokens et permet de limiter l'accès par repository. Un GitHub personal access token reste utile lorsque la GitHub App n'est pas disponible, notamment avec certaines configurations GitHub Enterprise Server ou Enterprise Managed Users :

  1. dans GitHub : Settings > Developer settings > Personal access tokens ;
  2. créer un token avec expiration et droits minimaux de lecture/écriture sur les dépôts nécessaires ;
  3. dans Databricks : Settings > Linked accounts > Add Git credential ;
  4. sélectionner le fournisseur et enregistrer le token.

Ne pas confondre le PAT du fournisseur Git avec un PAT Databricks utilisé pour appeler les API de workspace. Ne stocker aucun des deux dans le dépôt, un notebook ou un paramètre visible. Pour la CI/CD, préférer un service principal et OAuth quand le workflow le permet.

Bonnes pratiques :

  • une branche par changement ;
  • ne pas développer directement sur main ;
  • code modulaire dans des fichiers Python plutôt que toute la logique dans des notebooks ;
  • notebooks minces pour orchestrer et explorer ;
  • tests et configuration versionnés ;
  • aucun token, mot de passe ou secret dans Git.

Chaque développeur devrait utiliser son propre Git folder et sa propre branche. Le Git folder est un client Git intégré : il permet clone, pull, création/changement de branche, comparaison des diffs, commit et push. La pull request et la fusion vers la branche principale se font généralement dans l'interface du fournisseur Git.

Lors d'un changement de branche, les modifications non commitées sont reportées sur la nouvelle branche si elles ne sont pas en conflit. Si elles seraient écrasées, le changement est bloqué : les committer sur une branche ou les abandonner explicitement avant de recommencer. Ne pas supposer qu'un stash est disponible dans l'interface. Le changement de branche peut aussi retirer les assets absents de la branche cible ; en revenant, ceux-ci peuvent être recréés avec de nouveaux identifiants et URL.

Mettre à jour un Git folder depuis le dépôt distant

L'opération attendue est Pull. Dans la boîte de dialogue Git du folder, cliquer sur Pull récupère les changements de la branche distante suivie et met à jour les notebooks et fichiers locaux. En cas de conflit avec des modifications locales, il faut résoudre le conflit ; un pull peut aussi effacer l'état d'exécution du notebook, mais pas son code sauvegardé.

La même boîte de dialogue permet d'afficher l'historique de la branche. En cas de conflit pendant un pull, merge ou rebase, elle liste les fichiers concernés et permet d'éditer manuellement, de conserver tous les changements courants ou de prendre tous les changements entrants, puis de marquer le fichier comme résolu.

Ne pas confondre :

  • Pull : distant vers Git folder Databricks ;
  • Commit : enregistre localement un ensemble de changements ;
  • Push : Git folder Databricks vers le dépôt distant ;
  • Clone : crée initialement un nouveau Git folder depuis le dépôt.

Sparse checkout pour un gros dépôt

Le sparse checkout limite le Git folder à certains sous-répertoires d'un monorepo. Les motifs suivent le mode cone : indiquer par exemple etl/orders et shared/python pour synchroniser ces arborescences, tout en conservant les fichiers à la racine et dans les répertoires parents nécessaires.

Il réduit le volume affiché et synchronisé, mais ne crée pas une branche séparée et ne remplace pas les permissions du dépôt. Les motifs d'exclusion préfixés par ! ne sont pas pris en charge. Une fois le sparse checkout activé pour ce Git folder, il ne peut pas être désactivé sur place ; recréer un Git folder complet si le périmètre doit être élargi sans cette contrainte.

6.3 Outils locaux et authentification

6.3.1 Choisir l'outil selon le besoin

Ces technologies sont complémentaires : certaines pilotent les ressources de la plateforme, Databricks Connect exécute du code Spark à distance, et les bundles décrivent un projet déployable.

Besoin Outil Relation avec la plateforme
Intégration HTTP sur mesure REST API Databricks Primitives de bas niveau pour créer, mettre à jour, lancer ou inspecter les objets
Commande ponctuelle, script shell ou CI Databricks CLI Client impératif qui encapsule les REST API
Application ou automatisation typée SDK Databricks pour Python, Java, Go ou R Client programmatique qui encapsule les REST API
Développement et débogage local de code Spark Databricks Connect Client Spark distant : le code général s'exécute localement, mais les opérations DataFrame et les UDF s'exécutent sur un compute Databricks
Projet complet versionné dev/test/prod Declarative Automation Bundle Configuration déclarative déployée avec les commandes databricks bundle de la CLI

Utiliser donc la CLI pour une opération ou un script, le SDK/REST pour piloter la plateforme par une logique personnalisée, Databricks Connect pour développer localement des transformations Spark exécutées sur un compute distant, et un DAB pour promouvoir reproductiblement Jobs, pipelines et autres ressources. Un bundle n'est pas un transport indépendant : son déploiement passe par la CLI, qui appelle les API de la plateforme.

6.3.2 Installer la CLI et configurer Unified Authentication

Étape 1 — Installer la CLI

databricks auth login étant une commande de la CLI, il faut d'abord installer la CLI Databricks moderne et vérifier qu'elle est accessible :

BASH
curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/main/install.sh | sh
databricks version

Si /usr/local/bin n'est pas accessible en écriture, le script fournit une commande versionnée à relancer avec sudo. Dans un environnement sensible, lire le script et épingler sa version avant de lui accorder ces privilèges.

La CLI, les SDK, Databricks Connect et Terraform peuvent ensuite partager des profils de Databricks Unified Authentication enregistrés dans ~/.databrickscfg.

Étape 2 — Créer un profil OAuth interactif, recommandé

Pour un utilisateur qui travaille depuis son poste, utiliser OAuth user-to-machine (U2M) :

BASH
# URL de base du workspace, sans /jobs, /browse ni paramètres.
databricks auth login \
  --host "https://<url-de-base-du-workspace>" \
  --profile DEV_OAUTH

# Contrôler les profils disponibles et l'identité effectivement utilisée.
databricks auth profiles
databricks auth describe --profile DEV_OAUTH
databricks jobs list --profile DEV_OAUTH

La commande ouvre le navigateur. La CLI obtient et renouvelle ensuite les jetons OAuth courts. Avec la CLI moderne, ~/.databrickscfg conserve la configuration non secrète du profil, notamment le host, tandis que le jeton OAuth est placé par défaut dans le stockage sécurisé du système d'exploitation.

Étape 3 — Créer éventuellement un profil PAT

Un personal access token (PAT) est lié à un workspace et constitue une méthode historique. Lorsque OAuth est disponible, le préférer pour un utilisateur interactif ; un PAT reste cependant fréquent dans les questions d'examen et certains outils existants.

  1. Dans le workspace, ouvrir profil utilisateur > Settings > Developer > Access tokens > Manage.
  2. Cliquer sur Generate new token, puis définir son nom, sa durée et, si l'interface le propose, ses scopes minimaux.
  3. Générer le PAT et copier immédiatement sa valeur dans un emplacement sécurisé : elle ne sera plus affichée.
  4. Créer un profil différent du profil OAuth :
BASH
databricks configure --profile DEV_PAT

# Répondre aux invites sans placer le secret dans la commande :
# Databricks Host: https://<url-de-base-du-workspace>
# Personal Access Token: <coller-le-PAT>

databricks jobs list --profile DEV_PAT

Le résultat correspond conceptuellement à cette section de ~/.databrickscfg :

INI
[DEV_PAT]
host  = https://<url-de-base-du-workspace>
token = <personal-access-token>

Contrairement au jeton OAuth placé dans le stockage sécurisé du système, le PAT du profil se trouve dans ce fichier. Ne jamais le committer, le journaliser ou l'insérer dans un script ; restreindre également l'accès au fichier, par exemple avec chmod 600 ~/.databrickscfg sous Linux/macOS.

Étape 4 — Sélectionner le profil et connaître les priorités

Le flag --profile/-p choisit explicitement un profil pour une commande. Pour ne pas le répéter, définir le profil utilisé par les outils compatibles :

BASH
export DATABRICKS_CONFIG_PROFILE="DEV_OAUTH"
databricks jobs list

Les variables d'environnement constituent une alternative au profil PAT et correspondent souvent à la réponse attendue dans un QCM :

BASH
export DATABRICKS_HOST="https://<workspace-url>"
export DATABRICKS_TOKEN="<personal-access-token>"

DATABRICKS_HOST désigne ici le workspace et DATABRICKS_TOKEN le PAT. Les variables d'environnement sont évaluées avant les profils ; une ancienne valeur de DATABRICKS_TOKEN peut donc faire utiliser un PAT alors que l'on pense tester OAuth. Au besoin :

BASH
unset DATABRICKS_HOST DATABRICKS_TOKEN
databricks jobs list --profile DEV_OAUTH

En CI/CD, préférer OAuth machine-to-machine avec un service principal. Le couple DATABRICKS_HOST + DATABRICKS_TOKEN est la réponse attendue pour le scénario PAT, pas la seule méthode possible.

Enfin, ne pas confondre :

  • le host et les credentials, qui authentifient l'outil ;
  • le compute, requis en plus par Databricks Connect pour exécuter Spark ;
  • les Job/Pipeline IDs, utilisés pour cibler une ressource existante, par exemple lors du binding DAB de 6.4.6.

6.3.3 Utiliser CLI, SDK et Databricks Connect depuis un poste local

Après la configuration précédente, les trois outils réutilisent les mêmes credentials, mais à des fins différentes.

CLI : exécuter une commande distante
BASH
databricks jobs list --profile DEV_OAUTH

Le flag global --profile/-p choisit le profil de connexion. Si DATABRICKS_CONFIG_PROFILE=DEV_OAUTH est déjà défini, il peut être omis.

Pour récupérer ponctuellement un objet du workspace sur le poste local, utiliser les commandes workspace de la CLI moderne :

BASH
# Un notebook ou fichier vers un fichier local.
databricks workspace export \
  /Workspace/Users/me@example.com/project/notebook \
  --format SOURCE \
  --file ./notebook.py

# Un dossier complet vers un dossier local.
databricks workspace export-dir \
  /Workspace/Users/me@example.com/project \
  ./project

Pour workspace export, --file désigne le fichier local ; sans ce flag, le contenu est écrit sur la sortie standard. Le flag global -o/--output règle le format d'affichage de la CLI : ce n'est pas un alias de destination. Pour un projet, préférer néanmoins Git et un bundle à des exports manuels répétés.

Pour pousser rapidement un répertoire local vers des workspace files, databricks sync <source_locale> <destination_workspace> effectue une synchronisation à sens unique ; --watch la maintient pendant le développement. Il ne rapatrie pas les modifications distantes et ne remplace ni Git ni un déploiement DAB. Dans un bundle, utiliser plutôt sa configuration sync ou databricks bundle sync.

SDK Python : automatiser par du code

Le client peut nommer le profil explicitement ; il ne faut pas coder le token dans le programme :

PYTHON
from databricks.sdk import WorkspaceClient

w = WorkspaceClient(profile="DEV_OAUTH")
for job in w.jobs.list():
    print(job.job_id, job.settings.name)

Avec WorkspaceClient() sans argument, le SDK résout automatiquement les variables d'environnement ou le profil sélectionné par DATABRICKS_CONFIG_PROFILE.

Databricks Connect : exécuter Spark depuis l'IDE local

Databricks Connect est différent de Git folders : le code et le débogage restent dans l'IDE local, mais les opérations Spark s'exécutent sur un compute Databricks. Il réutilise le profil d'authentification et exige en plus un compute compatible ainsi qu'une version compatible du package databricks-connect.

Lors de la création ou de la reconfiguration du profil, sélectionner une cible de calcul :

BASH
# Compute classique : la commande demande de choisir un cluster et mémorise son ID.
databricks auth login \
  --host "https://<url-de-base-du-workspace>" \
  --profile DEV_OAUTH \
  --configure-cluster

# Alternative serverless, si le workspace et le workload le prennent en charge.
databricks auth login \
  --host "https://<url-de-base-du-workspace>" \
  --profile DEV_OAUTH \
  --configure-serverless

export DATABRICKS_CONFIG_PROFILE="DEV_OAUTH"
databricks-connect test

databricks-connect test vérifie notamment l'authentification, le compute et la compatibilité avant d'exécuter le programme :

PYTHON
from databricks.connect import DatabricksSession

spark = DatabricksSession.builder.getOrCreate()
spark.table("dev.silver.orders").groupBy("country").count().show()

6.4 Declarative Automation Bundles : du projet à la production

Les Declarative Automation Bundles (DAB), anciennement Databricks Asset Bundles, décrivent comme du code un projet et ses ressources Databricks : Jobs, pipelines, notebooks, fichiers, variables, permissions et environnements.

Le DAB est le modèle déclaratif du projet ; les commandes databricks bundle ... de la CLI l'initialisent, le valident, le déploient et exécutent ses ressources.

Le déroulement pratique est : installer et authentifier la CLI en 6.3.2 → vérifier sa version pour le bundle → initialiser le projet → définir targets et ressources → valider et déployer en dev → tester → promouvoir en prod.

Déploiement manuel/traditionnel Bundle déclaratif
Clics difficiles à reproduire YAML versionné et reproductible
Dérive entre dev/test/prod Même code, overrides par target
Revue limitée du changement Pull request et diff
Scripts API ad hoc Cycle standard validate, plan, deploy, run
Rollout difficile à auditer Ressources et état du déploiement suivis

6.4.1 Vérifier la version CLI et distinguer profil et target

Les bundles utilisent la CLI Databricks moderne, et non l'ancienne CLI Python legacy. La documentation d'installation demande aujourd'hui au moins la version 0.205.0; bundle init et bundle schema nécessitent au moins 0.218.0, et les fonctionnalités plus récentes peuvent demander davantage. « v0.200+ » est donc seulement un raccourci historique : vérifier et, si possible, épingler la version réellement testée.

L'installation et l'authentification communes sont réalisées en 6.3.2. Pour un bundle, il reste à vérifier que cette CLI satisfait la version minimale requise par le projet.

BASH
databricks version

Pour les commandes de bundle, --profile/-p sélectionne le host et les credentials, tandis que --target/-t sélectionne la configuration dev/test/prod du bundle. Une target peut référencer workspace.profile, mais profil d'authentification et target de déploiement restent deux concepts distincts.

Le projet peut imposer sa version avec bundle.databricks_cli_version, comme dans le fichier racine de l'étape 6.4.3.

6.4.2 Initialiser une fois le projet et organiser ses fichiers

Pour un nouveau projet seulement :

BASH
databricks bundle init
# Ou avec un template nommé :
databricks bundle init default-python

bundle init crée un squelette ; il ne récupère pas un Job ou un pipeline existant. Un projet fréquent est organisé ainsi :

TEXT
my_project/
├── .gitignore
├── databricks.yml
├── resources/
│   ├── jobs.yml
│   └── pipelines.yml
├── src/
│   ├── ingest.py
│   └── transform.py
└── tests/
    └── test_transform.py

La synchronisation respecte le .gitignore racine. Pour conserver un répertoire dans Git mais ne pas l'envoyer au workspace, utiliser sync.exclude dans databricks.yml :

YAML
sync:
  exclude:
    - data/**

sync.include peut réinclure explicitement un fichier ignoré. Les motifs suivent la syntaxe de .gitignore; .syncignore n'est pas le mécanisme documenté pour les bundles.

Pour obtenir autocomplétion, documentation des champs et validation structurelle dans un IDE utilisant YAML Language Server :

BASH
databricks bundle schema > bundle_config_schema.json
YAML
# yaml-language-server: $schema=bundle_config_schema.json
bundle:
  name: orders_pipeline

Le JSON Schema aide l'IDE, mais ne remplace pas bundle validate, qui résout aussi les substitutions, fichiers inclus et contexte de la target.

6.4.3 Définir le bundle, ses variables et ses targets

Le fichier racine databricks.yml décrit l'identité du projet, inclut les ressources et fixe les environnements :

YAML
bundle:
  name: orders_pipeline
  databricks_cli_version: '>= 0.218.0'

include:
  - resources/*.yml

variables:
  catalog:
    description: Catalogue cible
    default: dev

targets:
  dev:
    default: true
    mode: development
    workspace:
      host: https://<dev-workspace>
    variables:
      catalog: dev

  prod:
    mode: production
    workspace:
      host: https://<prod-workspace>
      root_path: /Workspace/production-bundles/${bundle.name}/${bundle.target}
    variables:
      catalog: production

Concepts à reconnaître :

  • bundle.name identifie le projet ;
  • include, uniquement au niveau racine, fusionne les fichiers YAML correspondant aux globs relatifs ; il modularise la configuration, tandis que targets porte les overrides d'environnement ;
  • resources contient les objets Databricks à créer ou mettre à jour ;
  • variables fournit des valeurs référencées par ${var.name} ;
  • targets représente les contextes dev/test/prod et leurs overrides ;
  • ${bundle.target} et les autres substitutions donnent le contexte de déploiement ;
  • sans --target, la target marquée default: true est utilisée.

workspace.root_path remplace le répertoire de déploiement par défaut pour la target. En production, protéger ce dossier par des ACL et réserver son écriture à l'identité de déploiement ; ne pas choisir /Shared, modifiable par tous les utilisateurs du workspace.

Une variable est une chaîne par défaut. Déclarer type: complex seulement lorsqu'elle doit contenir un objet ou une liste structurée :

YAML
variables:
  job_cluster:
    type: complex
    default:
      num_workers: 2
      spark_conf:
        spark.sql.shuffle.partitions: auto

Cette variable peut ensuite remplacer un bloc YAML compatible avec ${var.job_cluster}. Une valeur scalaire avec type: complex échoue à la validation.

Une variable sans default peut être fournie sans modifier le YAML, par --var ou par une variable d'environnement préfixée BUNDLE_VAR_ :

YAML
variables:
  schema_name:
    description: Schéma cible du déploiement
BASH
BUNDLE_VAR_schema_name=finance databricks bundle deploy --target prod
# Prioritaire sur la variable d'environnement :
databricks bundle deploy --target prod --var="schema_name=marketing"

La référence reste ${var.schema_name} dans le YAML. Le nom correspondant à une variable api_key serait donc BUNDLE_VAR_api_key, mais une bundle variable n'est pas un coffre à secrets : elle évite de modifier le fichier source sans garantir que la valeur ne sera jamais visible dans une configuration résolue, un état ou un log. Pour une clé d'API, stocker le secret dans le gestionnaire de secrets de la CI ou dans Databricks Secrets et déployer seulement la référence scope/key ; le code récupère la valeur à l'exécution avec dbutils.secrets.get(...).

Avec mode: development, le bundle préfixe par défaut les ressources avec l'utilisateur ([dev <utilisateur>]), met en pause schedules/triggers et active le mode développement des pipelines. Certains presets, comme les limites de concurrence ou le verrou de déploiement, sont également assouplis et personnalisables. Ce mode limite les lancements accidentels, mais ne remplace ni l'isolation des données ni les permissions.

Isoler dev, test et prod
Niveau d'isolation Usage Compromis
Workspaces distincts Frontière forte, réglages, réseau et identités séparés Plus d'administration
Catalogues distincts Séparation claire des données et grants dans un même metastore Compute/workspace potentiellement partagés
Schémas distincts Environnement léger ou schéma par développeur/branche Frontière moins forte
Targets de bundle Même code, paramètres et destinations propres à chaque environnement Dépend de la bonne configuration des targets

Un pattern efficace est : une branche et un schéma dev par développeur, un catalogue de test partagé pour l'intégration, puis un catalogue/workspace de production déployé uniquement par la CI. Ne jamais faire pointer une target dev vers des tables prod modifiables.

6.4.4 Décrire les ressources et les artefacts

Une ressource Job peut être placée dans resources/jobs.yml :

YAML
resources:
  jobs:
    orders_job:
      name: ${bundle.target}-orders-job
      tasks:
        - task_key: transform
          job_cluster_key: orders_cluster
          notebook_task:
            notebook_path: ../src/transform.py
            base_parameters:
              catalog: ${var.catalog}

      job_clusters:
        - job_cluster_key: orders_cluster
          new_cluster:
            spark_version: <lts-runtime-version>
            node_type_id: <cloud-node-type>
            num_workers: 2

Une ressource pipeline peut être placée dans resources/pipelines.yml :

YAML
resources:
  pipelines:
    orders_pipeline:
      name: ${bundle.target}-orders-pipeline
      development: false
      continuous: false
      channel: CURRENT
      catalog: ${var.catalog}
      schema: sales

Le réglage historique « Advanced settings > Pipeline user mode > Production » correspond au mode développement/production du pipeline. development: false, valeur par défaut, configure ici le pipeline pour la production ; il reste distinct de targets.prod.mode: production, qui applique les presets de production du bundle. La target development active automatiquement le mode développement du pipeline lors du déploiement en dev.

La section artifacts décrit ce qui sera construit puis téléversé pendant bundle deploy. Une wheel (.whl) est un package Python installable, utile lorsque le projet contient des modules réutilisables et testés plutôt qu'uniquement des notebooks :

YAML
artifacts:
  python_wheel:
    type: whl
    path: .
    build: python -m build --wheel

Ce bloc est facultatif pour un Job composé de notebook_task : les notebooks sont déployés et exécutés directement. Il devient pertinent pour partager un même package entre plusieurs tâches, modulariser le code et le tester avec pytest. Le build ne lance pas implicitement les tests : les ajouter à la CI ou à la commande build si ce contrôle est souhaité. Pour l'examen Associate, retenir surtout le rôle général de artifacts ; la commande de packaging exacte est secondaire.

6.4.5 Définir les permissions et l'identité d'exécution

Les ACL peuvent être définies pour un Job précis :

YAML
resources:
  jobs:
    sales_job:
      name: sales-job
      # tasks: ...
      permissions:
        - group_name: data_engineers
          level: CAN_MANAGE_RUN
        - group_name: business_readers
          level: CAN_VIEW
        - service_principal_name: 00000000-0000-0000-0000-000000000000
          level: IS_OWNER

Niveaux Job à reconnaître :

  • CAN_VIEW : consulter la définition et les runs ;
  • CAN_MANAGE_RUN : lancer, annuler et gérer les runs ;
  • CAN_MANAGE : modifier le Job et ses permissions, sans être owner ;
  • IS_OWNER : être propriétaire de la ressource.

Dans l'UI : Jobs & Pipelines > Job > Job details > Edit permissions, puis ajouter l'utilisateur, le groupe ou le service principal et son niveau. Le champ Run as se modifie séparément avec son icône crayon ; seul un administrateur du workspace peut changer l'owner.

Une section permissions: au niveau supérieur peut appliquer CAN_VIEW, CAN_RUN ou CAN_MANAGE aux ressources compatibles. Ne pas déclarer le même principal à ce niveau et dans les permissions spécifiques d'une ressource : ces définitions ne peuvent pas se chevaucher.

Ne pas confondre les trois contrôles :

  • permissions répond à « qui peut voir ou gérer le Job ? » ;
  • run_as répond à « avec quelle identité s'exécute-t-il ? » ;
  • les grants Unity Catalog répondent à « quelles données cette identité peut-elle utiliser ? ».

En production, l'identité d'exécution peut être fixée sur la target :

YAML
targets:
  prod:
    mode: production
    run_as:
      service_principal_name: 00000000-0000-0000-0000-000000000000

Le déployeur doit pouvoir utiliser cette identité. Le service principal run_as doit posséder les droits runtime sur notebooks, pipelines, compute et données ; il peut être différent du principal qui exécute bundle deploy.

Une fois le projet et ses targets définis, on ajoute les ressources, on paramètre l'environnement, puis on suit toujours le même cycle : validate → plan → deploy → run.

6.4.6 Créer une ressource ou reprendre un Job construit dans l'UI

Pour une nouvelle ressource, écrire son YAML dans resources/, comme en 6.4.4. Pour reprendre un Job existant sans le réécrire, récupérer son ID dans Jobs & Pipelines > Job details, puis lancer depuis la racine du bundle :

BASH
databricks bundle generate job \
  --existing-job-id <dev_job_id> \
  --bind \
  --target dev

La commande :

  1. génère le YAML dans resources/ ;
  2. télécharge dans src/ les fichiers pris en charge, notamment les notebooks ;
  3. lie resources.jobs.<resource_key> au Job existant grâce à --bind.

Un notebook peut être téléchargé au format source .py, mais reste une notebook_task. bundle generate job importe la ressource et ses fichiers référencés pris en charge ; il ne transforme pas le code en package et ne récupère pas automatiquement tous les modules importés ou leurs dépendances. Une wheel artifacts répond donc à un autre besoin : construire reproductiblement un package à partir du projet local.

Le binding est essentiel : sans lui, un déploiement crée normalement un nouveau Job au lieu de mettre à jour celui de l'UI. Si nécessaire, créer la liaison séparément avec la clé de ressource YAML, qui peut différer du nom affiché :

BASH
databricks bundle deployment bind \
  <resource_key> \
  <dev_job_id> \
  --target dev

Relire ensuite la clé de ressource, les chemins vers src/, le compute, les paramètres, schedules, notifications, permissions et références propres au workspace. Déplacer les différences dev/prod dans les variables et overrides de targets.

Alternative UI : près de Run now, choisir View as code > YAML, puis copier le bloc resources.jobs dans resources/*.yml. Ce YAML décrit seulement la ressource Job, pas le projet complet : databricks.yml, les sources et le binding restent nécessaires. Cette voie est utile lorsque bundle generate job ne prend pas le Job en charge ; la génération CLI est actuellement limitée aux Jobs comprenant des notebook tasks.

6.4.7 Distinguer configuration déployée et paramètres du run

Une variable de bundle ${var.schema_name} est résolue lors de la lecture, de la validation et du déploiement. Un job parameter est transmis à l'exécution. Supposons que ${var.schema_name} alimente base_parameters.schema_name :

BASH
databricks bundle deploy -t dev --var="schema_name=finance_schema"
databricks bundle run -t dev --var="schema_name=marketing_schema" sales_job

Le Job déployé conserve ici finance_schema : bundle run --var ne redéploie pas la définition distante avec marketing_schema. Pour changer la valeur métier du run, déclarer un job parameter et utiliser --params :

BASH
databricks bundle run -t dev \
  --params schema_name=marketing_schema \
  sales_job

Mémo : --var configure ce qui est déployé ; --params configure l'exécution.

6.4.8 Valider, déployer et promouvoir de dev à prod

La promotion reprend le même bundle et le même cycle de commandes dans chaque target. On le valide d'abord en développement, puis la CI applique la configuration approuvée en production.

Développement : validate → plan → deploy → run
BASH
databricks bundle validate --target dev
databricks bundle plan --target dev
databricks bundle deploy --target dev
databricks bundle run --target dev <resource_key>
Commande Rôle
validate Contrôle YAML, références, chemins, variables et substitutions
plan Construit le bundle et prévisualise créations, modifications et suppressions sans changer le workspace
deploy Construit/téléverse les artefacts et crée ou met à jour les ressources de la target
run Lance une ressource déjà déployée

deploy ne lance donc pas le Job ou le pipeline. plan complète validate sans le remplacer ; s'il est absent, mettre à jour la CLI et la contrainte bundle.databricks_cli_version.

Pour inspecter l'identité du bundle, sa target et ses ressources avec leurs liens workspace, sans déclencher de déploiement ni de run :

BASH
databricks bundle summary --target dev

Pour une étape de CI qui doit analyser la sortie, -o/--output json est un flag global de la CLI, par exemple databricks bundle plan --target prod --output json. Ne pas le confondre avec --log-format json, qui structure les logs de la CLI.

Après les tests unitaires de 6.1.2, compléter la validation dans Databricks. Pour un pipeline déclaratif :

  • exécuter un Dry run pour valider le graphe sans publier de données, puis vérifier tous les fichiers du projet ;
  • effectuer l'intégration dans un catalogue ou schéma de test avec de petits inputs contrôlés ;
  • vérifier lectures, écritures, droits, paramètres et dépendances ;
  • valider les expectations en contrôlant lignes conservées, rejetées et échecs attendus ;
  • tester AUTO CDC, expectations et autres fonctionnalités propres à Lakeflow dans Databricks, car elles ne sont pas toutes exécutables avec Spark local ;
  • terminer par un run du workflow complet dans cet environnement isolé.

Pour un Job importé, le plan doit annoncer une mise à jour, pas la création d'un doublon. Après le déploiement, vérifier dans l'UI que le Job conserve son ID et que le run réussit. Puis committer databricks.yml, resources/ et src/ : Git devient la source de vérité, et une modification durable effectuée seulement dans l'UI risque d'être écrasée au prochain déploiement.

Production : binding éventuel puis déploiement par la CI

Si aucun Job de production n'existe, le premier déploiement de la target prod le crée. S'il existe déjà, le lier auparavant à la même clé de ressource avec son propre ID :

BASH
databricks bundle deployment bind \
  <resource_key> \
  <prod_job_id> \
  --target prod

La séquence de CI reste explicite :

BASH
databricks bundle validate --target prod
databricks bundle plan --target prod
databricks bundle deploy --target prod \
  --fail-on-active-runs \
  --auto-approve
  • --auto-approve supprime les confirmations interactives, mais pas la validation ni les contrôles d'accès ; il n'a pas le rôle de --force ;
  • --fail-on-active-runs fait échouer le déploiement si une ressource du bundle est active ; il n'annule pas le run et n'attend pas sa fin ;
  • la CI doit idéalement s'authentifier avec un service principal plutôt qu'avec le PAT personnel d'un développeur.

Pour un pipeline déclaratif, compléter ce cycle générique avant la promotion :

  1. exécuter les tests, puis un Dry run portant sur tous les fichiers ;
  2. vérifier catalogue, schéma, sources, cibles et paramètres de production ;
  3. utiliser le channel CURRENT, ou une version explicitement testée ;
  4. configurer run_as, idéalement avec un service principal, puis vérifier compute, grants et secrets ;
  5. configurer dans Lakeflow Jobs les dépendances, retries, notifications et SLA ;
  6. après le déploiement par CI, contrôler le premier run et ses métriques de qualité.

Dans GitHub Actions, utiliser l'action officielle et épingler la version de CLI testée :

YAML
- name: Install Databricks CLI
  uses: databricks/setup-cli@main
  with:
    version: <tested-cli-version>

Pour supprimer un environnement de bundle devenu inutile :

BASH
databricks bundle destroy --target dev

destroy supprime définitivement les Jobs, pipelines, autres ressources et artefacts précédemment déployés sous l'identité de ce bundle — nom, target et workspace — puis nettoie son état de déploiement. Une confirmation est demandée par défaut. Toujours contrôler le workspace, le profil et la target ; réserver --auto-approve à une automatisation de nettoyage explicitement protégée et ne pas l'utiliser machinalement sur la production.


7. Dépannage, performance et maintenance

Fil conducteur : localiser la couche en cause → observer les métriques → corriger une hypothèse → mesurer de nouveau. Pour un échec de DAG ou de tâche Lakeflow, commencer par le monitoring des Jobs en 5.7 ; cette section traite ensuite du compute, de Spark et du layout Delta.

7.1 Démarche de diagnostic et Spark UI

Suivre cet ordre évite les changements au hasard :

  1. confirmer le symptôme et le premier run concerné ;
  2. comparer durée, volume d'entrée et configuration à un run sain ;
  3. localiser la première tâche ou requête lente/échouée ;
  4. lire le message d'erreur, les logs driver/executor et le plan ;
  5. inspecter le Spark UI si le problème se situe dans Spark ;
  6. formuler une hypothèse unique ;
  7. corriger puis re-mesurer avec un input comparable.

Une hausse de durée sans hausse de volume peut venir d'un changement de code, de plan, de distribution des clés, de compute ou de source externe.

Lire le Spark UI

Vue Ce qu'elle montre
Jobs Actions Spark et leur progression
SQL/DataFrame Plans logique et physique, métriques par opérateur, dont les spills signalés
Stages Frontières et volumes de shuffle, durée, agrégats de tasks et spill
Tasks Distribution des durées, shuffle read/write, memory/disk spill et erreurs
Executors CPU, mémoire, GC, tâches, executors perdus
Storage Données mises en cache
Streaming Micro-batches, input rate et processing rate

Un Job Lakeflow contient des tasks d'orchestration ; une task peut lancer plusieurs Spark jobs, eux-mêmes divisés en stages puis tasks Spark. Ne pas confondre ces niveaux.

7.2 Échecs avant l'exécution Spark

Si le compute ou le driver ne démarre pas, Spark n'a encore exécuté aucun stage : consulter les événements du compute avant le Spark UI.

Échec de démarrage du compute

  • quota ou capacité insuffisante chez le fournisseur cloud ;
  • type de machine indisponible ;
  • permissions, credentials ou instance profile incorrects ;
  • réseau, DNS, pare-feu ou accès au stockage bloqué ;
  • init script défaillant ;
  • policy compute non respectée.

Consulter les compute event logs avant de changer le code Spark. Si l'échec suit l'association d'une identité cloud — IAM role/instance profile, managed identity ou service account — vérifier la relation de confiance et l'autorisation permettant à Databricks d'attacher ou de transmettre cette identité, puis ses rôles sur les ressources. Un échec avant le démarrage du driver n'est généralement pas un problème de transformation DataFrame.

Conflit de bibliothèques

  • version incompatible avec le Databricks Runtime ou Python ;
  • dépendances transitives incompatibles ;
  • bibliothèque installée à plusieurs niveaux ;
  • état résiduel d'un cluster partagé.

Solutions : pinner les versions, utiliser un runtime LTS testé, déclarer les dépendances dans le bundle ou l'environnement du Job, tester sur compute éphémère et redémarrer après modification d'une bibliothèque de cluster.

7.3 Diagnostiquer une exécution et orienter la correction

Une fois Spark démarré, cette partie sert uniquement à identifier la couche en cause. Le diagnostic oriente ensuite vers l'optimisation de l'exécution Spark en 7.4 ou vers celle des lectures et du layout Delta en 7.5.

7.3.1 Identifier le symptôme et sa couche

Le diagnostic doit d'abord déterminer où agir :

  • métriques de shuffle, spill, skew, GC ou mémoire → optimiser l'exécution Spark en 7.4 ;
  • trop de fichiers lus, faible sélectivité, petites lectures fragmentées ou volume scanné élevé → optimiser les lectures et le layout Delta en 7.5 ;
  • les deux peuvent se cumuler : une lecture excessive augmente le volume qui sera ensuite shufflé.
Symptôme Signature à confirmer Orientation principale
Data skew Quelques tasks bien plus lentes ; maximum très supérieur au percentile 75 7.4 : clé, pré-agrégation, broadcast ou AQE
Shuffle excessif Gros shuffle read/write après join, groupBy ou distinct 7.4 : code, broadcast et partitions de shuffle
Shuffle fetch wait élevé Tasks bloquées à récupérer des blocs de shuffle distants ; faible temps de calcul utile 7.4, executors et réseau : volume, partitions, workers lents/perdus et connectivité
Spill memory spill ou disk spill dans un stage 7.4 : taille/nombre des partitions, réduction des données et mémoire executor
GC élevé GC Time représente une grande part de l'Executor Run Time 7.4 : pression mémoire, taille des partitions, cache/objets et mémoire executor
Une seule longue task Un seul cœur occupé 7.4 : retirer repartition(1), partitionner une window ou traiter un fichier non splittable
Driver OOM Driver redémarré, erreur après collect() ou toPandas() 7.4 : conserver le calcul distribué et limiter le résultat
Executor OOM Task/executor perdu, souvent sur une grosse partition 7.4 : corriger skew, partitionnement ou état avant le compute
ExecutorLostFailure Executor perdu avec gros shuffle, spill ou GC 7.4 si la cause est Spark ; sinon vérifier Spot, réseau et compute
Trop de petites tasks après un shuffle Nombreuses petites partitions dans un stage post-shuffle 7.4 : partitions initiales et coalescing AQE
Trop de petites tasks pendant un scan Beaucoup de petits fichiers d'entrée 7.5.3 : compaction avec OPTIMIZE
Scheduler Delay long, Executor Computing Time court Beaucoup de tasks minuscules, ou executors indisponibles 7.4 : réduire les tasks inutiles, corriger le partitionnement et vérifier la capacité
Locality Level ANY Aucune localisation plus proche retenue pour la task Indice de lecture distante ; vérifier I/O et réseau avant de conclure à un défaut
CPU faible, I/O élevée et volume scanné élevé Beaucoup de fichiers/octets lus pour peu de lignes retournées 7.5.1 à 7.5.4 : pruning, skipping et layout
Estimations ou stratégie de join incohérentes Taille estimée douteuse, join inadapté dans le plan initial 7.3.2 : statistiques et ANALYZE, puis 7.4 si nécessaire

Le spill apparaît lorsqu'une opération comme un tri, un join ou une agrégation ne tient plus dans la mémoire d'exécution : Spark déverse temporairement une partie de ses données intermédiaires sur le disque local. Cela évite parfois l'OOM mais ralentit le stage ; le spill est un symptôme à réduire, pas une optimisation. Le skew est une distribution déséquilibrée : quelques partitions contiennent bien plus de données que les autres. Ce sont deux problèmes distincts, même si le skew peut provoquer du spill.

Shuffle Read Fetch Wait Time mesure le temps pendant lequel une task attend des blocs de shuffle provenant de machines distantes. Un wait dominant avec peu d'Executor Computing Time indique que le transfert ou la disponibilité des blocs domine le calcul, mais ne prouve pas à lui seul une saturation réseau : vérifier aussi le volume de shuffle, le nombre et la taille des partitions, le skew, les remote bytes, les executors lents/perdus et une éventuelle préemption Spot.

Un Scheduler Delay dominant alors que le calcul executor est très court signifie souvent que Spark passe davantage de temps à distribuer et planifier des milliers de petites tasks qu'à les calculer. Vérifier aussi une éventuelle attente d'executors avant de fusionner aveuglément les partitions. Le niveau de localité ANY signifie que Spark a accepté n'importe quel executor, faute de localisation préférable ; avec du stockage objet cloud, l'absence de localité des blocs peut être normale. Ce niveau devient surtout utile lorsqu'il coïncide avec un débit de lecture faible ou beaucoup de trafic réseau.

7.3.2 Inspecter le plan et les statistiques

EXPLAIN FORMATTED affiche le plan sans exécuter la requête. Pour ne pas corriger avec des paramètres Spark un plan fondé sur des statistiques absentes ou obsolètes, suivre ce parcours : plan initial de référence → contrôle des statistiques → ANALYZE si nécessaire → nouveau plan → exécution et métriques runtime.

Version SQL :

SQL
-- 1. Capturer le plan initial de référence.
EXPLAIN FORMATTED
SELECT customer_id, sum(amount)
FROM dev.silver.orders
GROUP BY customer_id;

-- 2. Contrôler les métadonnées, dont les statistiques connues.
DESCRIBE TABLE EXTENDED dev.silver.orders;

-- 3. Seulement si les statistiques sont absentes ou obsolètes,
-- par exemple après un chargement ou une réécriture importante.
ANALYZE TABLE dev.silver.orders COMPUTE STATISTICS;
ANALYZE TABLE dev.silver.orders
COMPUTE STATISTICS FOR COLUMNS customer_id;

-- 4. Recalculer le plan initial et le comparer au premier.
EXPLAIN FORMATTED
SELECT customer_id, sum(amount)
FROM dev.silver.orders
GROUP BY customer_id;

Version PySpark :

PYTHON
query = """
SELECT customer_id, sum(amount)
FROM dev.silver.orders
GROUP BY customer_id
"""

# 1. Plan initial de référence.
spark.sql(query).explain(mode="formatted")

# 2. Contrôle des métadonnées et statistiques.
spark.sql("DESCRIBE TABLE EXTENDED dev.silver.orders").show(
    truncate=False
)

# 3. ANALYZE reste une commande SQL, même depuis un notebook Python.
# À exécuter uniquement si les statistiques doivent être rafraîchies.
spark.sql(
    "ANALYZE TABLE dev.silver.orders COMPUTE STATISTICS"
)
spark.sql("""
    ANALYZE TABLE dev.silver.orders
    COMPUTE STATISTICS FOR COLUMNS customer_id
""")

# 4. Nouveau DataFrame pour afficher le plan recalculé.
spark.sql(query).explain(mode="formatted")

Chercher notamment BroadcastHashJoin, SortMergeJoin, Exchange (shuffle), filtres poussés et volume scanné. Les statistiques de table et de colonnes aident notamment le plan initial à estimer les volumes, l'ordre des joins et l'intérêt d'un broadcast. Sur une table managée Unity Catalog avec predictive optimization actif, ANALYZE peut déjà être automatisé : vérifier avant de le relancer manuellement.

Enfin, exécuter réellement la requête et examiner Spark UI. Un EXPLAIN SQL ne montre pas les décisions finales d'AQE, car AQE utilise les statistiques observées pendant l'exécution. Ne modifier les paramètres du plan initial ou d'AQE que si le problème subsiste après ce contrôle : EXPLAIN avant → ANALYZE si nécessaire → EXPLAIN après → exécution → plan final AQE.

7.3.3 Approfondir les erreurs mémoire et ExecutorLostFailure

Pour un OOM, distinguer d'abord le driver de l'executor. Ajouter aveuglément de la mémoire peut masquer un skew, un broadcast trop gros ou un collect() incorrect.

Un GC Time élevé signifie que la JVM passe une part importante de son temps à récupérer de la mémoire au lieu de calculer. Une proportion extrême, par exemple 80 % du temps de task, indique une forte pression mémoire : vérifier grosses partitions, skew, cache inutile, état streaming, broadcast et création excessive d'objets avant d'augmenter la mémoire executor. Le GC est un symptôme ; augmenter le compute sans réduire la pression peut seulement déplacer le problème.

ExecutorLostFailure est un message générique. Lorsqu'une transformation wide produit de gros shuffle read/write, du spill et un GC élevé, un nombre insuffisant de partitions peut donner trop de données à chaque task, épuiser la mémoire puis faire disparaître l'executor. Dans ce scénario précis, les deux corrections attendues par le QCM sont :

  1. augmenter la mémoire disponible par executor, idéalement en choisissant des workers avec un meilleur ratio mémoire/cœur ;
  2. augmenter le nombre de partitions de shuffle pour réduire le volume traité par task.

Le réglage de spark.sql.shuffle.partitions est centralisé dans le bloc suivant. Ces deux actions ne corrigent pas un worker Spot retiré, une panne réseau ou un skew extrême : vérifier la cause dans Spark UI et les logs avant de modifier le compute.

7.4 Optimiser une exécution Spark

À partir du diagnostic de 7.3, suivre ce chemin : établir la configuration effective → examiner le plan initial → examiner le plan final AQE → optimiser le code → augmenter le compute seulement si nécessaire. Modifier une seule hypothèse à la fois, puis mesurer de nouveau. Les valeurs d'une session peuvent provenir du runtime, du compute, d'une policy ou du notebook ; toujours les consulter avant de supposer leur valeur.

7.4.1 Établir la configuration globale et celle du compute

Commencer par relever le parallélisme et la mémoire disponibles. Cette étape donne le contexte du diagnostic ; elle ne signifie pas qu'il faut immédiatement augmenter le compute.

Élément Signification Modification
spark.default.parallelism Parallélisme par défaut, surtout pour les RDD et certaines opérations non SQL Généralement dérivé du compute ; ne pas le confondre avec les partitions SQL de shuffle
spark.driver.memory Mémoire du driver Choisir/configurer le driver du compute, puis redémarrer
spark.executor.memory Mémoire d'un executor Choisir/configurer les workers du compute, puis redémarrer
Nombre et type de workers Capacité totale, cœurs et mémoire disponibles Configuration du compute ou du Job, éventuellement autoscaling

SQL — consulter :

SQL
-- Liste les propriétés SQL de la session avec valeur et description.
SET -v;

-- Paramètres ciblés issus de la configuration Spark/compute.
SET spark.default.parallelism;
SET spark.driver.memory;
SET spark.executor.memory;

PySpark — consulter :

PYTHON
compute_keys = [
    "spark.default.parallelism",
    "spark.driver.memory",
    "spark.executor.memory",
]

for key in compute_keys:
    try:
        value = spark.conf.get(key)
        modifiable = spark.conf.isModifiable(key)
    except Exception:
        value = "<non défini dans la session>"
        modifiable = False
    print(f"{key} = {value}; modifiable={modifiable}")

# Valeur effective dérivée du SparkContext et du compute.
print("defaultParallelism =", spark.sparkContext.defaultParallelism)

Modifier : SET ou spark.conf.set() ne redimensionne pas un driver ou un executor déjà démarré. Pour changer leur mémoire, modifier le type de nœud, le nombre de workers ou l'autoscaling dans la configuration du compute/Job, puis redémarrer. N'effectuer ce redimensionnement qu'après les optimisations de code, sauf si les métriques prouvent immédiatement un sous-dimensionnement.

Sur un compute classique AWS, Enable autoscaling local storage surveille l'espace disque libre de chaque worker et peut attacher des volumes EBS supplémentaires, jusqu'à la limite documentée du compute. Cette option aide un gros shuffle ou un spill temporaire à ne pas épuiser le disque local ; elle n'ajoute ni RAM, ni CPU, ni workers et ne corrige pas le skew ou un partitionnement inefficace. La distinguer de l'autoscaling du compute, qui fait varier le nombre de workers.

7.4.2 Examiner et ajuster le plan initial

Ces paramètres influencent le plan construit avant l'exécution, à partir des statistiques et estimations disponibles.

Paramètre Rôle Risque d'un mauvais réglage
spark.sql.shuffle.partitions Nombre initial de partitions après les joins et agrégations avec shuffle Trop peu : grosses partitions, spill/OOM ; trop : nombreuses petites tasks
spark.sql.autoBroadcastJoinThreshold Taille estimée maximale pour choisir immédiatement un broadcast join Trop haut : broadcast trop gros/OOM ; trop bas : shuffle évitable

Apache Spark utilise 200 par défaut, mais ce nombre n'est pas une recommandation universelle et Databricks accepte auto. Dimensionner sur le volume réellement shufflé et les métriques des tasks, pas directement sur la taille totale de la table ; AQE peut ensuite fusionner les petites partitions ou scinder celles affectées par le skew.

SQL — consulter puis modifier :

SQL
-- Consulter les valeurs effectives.
SET spark.sql.shuffle.partitions;
SET spark.sql.autoBroadcastJoinThreshold;

-- Choisir UNE stratégie de partitions après mesure :
SET spark.sql.shuffle.partitions = auto;  -- choix automatique Databricks
-- Alternative : SET spark.sql.shuffle.partitions = 400;

-- Exemple : seuil initial de 10 MiB, exprimés en octets.
SET spark.sql.autoBroadcastJoinThreshold = 10485760; -- 10 * 1024 * 1024

PySpark — consulter puis modifier :

PYTHON
print(
    "shuffle.partitions =",
    spark.conf.get("spark.sql.shuffle.partitions"),
)
print(
    "initial broadcast threshold =",
    spark.conf.get("spark.sql.autoBroadcastJoinThreshold"),
)

# Choisir UNE stratégie de partitions après mesure :
spark.conf.set("spark.sql.shuffle.partitions", "auto")
# Alternative : spark.conf.set("spark.sql.shuffle.partitions", "400")

# Exemple : seuil initial de 10 MiB.
spark.conf.set(
    "spark.sql.autoBroadcastJoinThreshold",
    str(10 * 1024 * 1024),
)

Le seuil initial utilise des tailles estimées. Si une petite dimension est stable et correctement décrite par les statistiques, un seuil placé légèrement au-dessus de sa taille peut choisir directement un BroadcastHashJoin et éviter un shuffle. Vérifier ou rafraîchir les statistiques avant d'augmenter ce seuil.

7.4.3 Examiner et ajuster AQE

AQE — Adaptive Query Execution réoptimise les stages encore modifiables à partir des statistiques réellement observées pendant l'exécution. Sur Databricks, spark.databricks.optimizer.adaptive.enabled est l'interrupteur principal ; les paramètres spark.sql.adaptive.* et spark.databricks.adaptive.* pilotent ses fonctionnalités.

Paramètre AQE Rôle
spark.databricks.optimizer.adaptive.enabled Active ou désactive AQE
spark.databricks.adaptive.autoBroadcastJoinThreshold Convertit éventuellement un join en broadcast selon la taille runtime
spark.sql.adaptive.coalescePartitions.enabled Fusionne les petites partitions post-shuffle
spark.sql.adaptive.advisoryPartitionSizeInBytes Taille cible des partitions après coalescing
spark.sql.adaptive.skewJoin.enabled Découpe certaines partitions skewées
spark.sql.adaptive.skewJoin.skewedPartitionFactor Facteur par rapport à la taille médiane pour détecter le skew
spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes Taille minimale participant à la détection du skew

Plan initial et plan AQE :

Paramètre Décision Statistiques
spark.sql.autoBroadcastJoinThreshold Avant l'exécution Taille estimée
spark.databricks.adaptive.autoBroadcastJoinThreshold Pendant l'exécution par AQE Taille réellement observée

Les deux seuils sont indépendants et ne s'additionnent pas. Une relation estimée trop grande peut commencer en SortMergeJoin, puis passer en BroadcastHashJoin si AQE mesure une taille inférieure à son propre seuil. Cette conversion peut survenir après l'amorce d'un shuffle ; un broadcast choisi dès le plan initial reste généralement plus rapide lorsque les statistiques sont fiables.

SQL — consulter puis modifier :

SQL
-- Consulter l'interrupteur et les réglages AQE.
SET spark.databricks.optimizer.adaptive.enabled;
SET spark.databricks.adaptive.autoBroadcastJoinThreshold;
SET spark.sql.adaptive.coalescePartitions.enabled;
SET spark.sql.adaptive.advisoryPartitionSizeInBytes;
SET spark.sql.adaptive.skewJoin.enabled;
SET spark.sql.adaptive.skewJoin.skewedPartitionFactor;
SET spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes;

-- Activer AQE et ses optimisations utiles.
SET spark.databricks.optimizer.adaptive.enabled = true;
SET spark.databricks.adaptive.autoBroadcastJoinThreshold = 30MB;
SET spark.sql.adaptive.coalescePartitions.enabled = true;
SET spark.sql.adaptive.advisoryPartitionSizeInBytes = 64MB;
SET spark.sql.adaptive.skewJoin.enabled = true;
SET spark.sql.adaptive.skewJoin.skewedPartitionFactor = 5;
SET spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes = 256MB;

PySpark — consulter puis modifier :

PYTHON
aqe_keys = [
    "spark.databricks.optimizer.adaptive.enabled",
    "spark.databricks.adaptive.autoBroadcastJoinThreshold",
    "spark.sql.adaptive.coalescePartitions.enabled",
    "spark.sql.adaptive.advisoryPartitionSizeInBytes",
    "spark.sql.adaptive.skewJoin.enabled",
    "spark.sql.adaptive.skewJoin.skewedPartitionFactor",
    "spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes",
]

for key in aqe_keys:
    print(f"{key} = {spark.conf.get(key)}")

spark.conf.set(
    "spark.databricks.optimizer.adaptive.enabled",
    "true",
)
spark.conf.set(
    "spark.databricks.adaptive.autoBroadcastJoinThreshold",
    "30MB",
)
spark.conf.set(
    "spark.sql.adaptive.coalescePartitions.enabled",
    "true",
)
spark.conf.set(
    "spark.sql.adaptive.advisoryPartitionSizeInBytes",
    "64MB",
)
spark.conf.set(
    "spark.sql.adaptive.skewJoin.enabled",
    "true",
)
spark.conf.set(
    "spark.sql.adaptive.skewJoin.skewedPartitionFactor",
    "5",
)
spark.conf.set(
    "spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes",
    "256MB",
)

Les valeurs ci-dessus sont des repères documentés, pas une recette universelle. Le broadcast AQE exige un type de join compatible et une distribution adaptée. Le skew AQE n'est reconnu que si les conditions de facteur et de taille minimale sont satisfaites.

Après chaque modification, comparer dans le Spark UI le plan initial et le plan final (AdaptiveSparkPlan), la durée des stages, le shuffle read/write, le spill, le GC et la distribution des tasks. Si les paramètres sont cohérents mais que le traitement reste coûteux, passer aux optimisations de code de 7.4.4 avant d'ajouter du compute.

7.4.4 Optimiser le code avant d'augmenter le compute

Avant d'ajouter des workers ou de choisir des machines plus grandes :

  1. filtrer tôt et ne sélectionner que les colonnes utiles ;
  2. utiliser les fonctions Spark natives ;
  3. éviter les shuffles et distinct inutiles ;
  4. broadcaster seulement une petite dimension ;
  5. choisir un partitionnement cohérent ;
  6. mettre en cache uniquement un DataFrame coûteux réellement réutilisé, puis unpersist().

Exemple de cache Spark explicite :

PYTHON
reused_df = (
    spark.table("dev.silver.orders")
         .filter(F.col("order_date") >= "2026-01-01")
         .cache()
)

reused_df.count()  # Première action : calcule et matérialise le cache.
reused_df.groupBy("country").sum("amount").show()
reused_df.groupBy("customer_id").count().show()

reused_df.unpersist()  # Libère les blocs du cache devenus inutiles.

cache() est lazy : il ne remplit le cache qu'à la première action. Il est rentable si le DataFrame coûteux est réutilisé ; pour une seule consommation, il ajoute souvent du travail et occupe inutilement la mémoire ou le disque des executors. unpersist() rend ces ressources réutilisables.

7.5 Optimiser les lectures, le layout et la maintenance Delta

Cette section distingue d'abord les mécanismes appliqués automatiquement pendant une lecture (7.5.1), puis les choix de layout et de maintenance qui les rendent efficaces (7.5.2 à 7.5.6).

7.5.1 Partition pruning et data skipping automatiques

Le partition pruning et le data skipping sont appliqués automatiquement par Databricks au moment de planifier ou d'exécuter une lecture. Il n'existe pas de commande à lancer pour les déclencher dans chaque requête. Leur efficacité dépend toutefois du filtre, du layout physique et des statistiques disponibles.

Mécanisme automatique Niveau ignoré Condition principale
Partition pruning Partitions/répertoires entiers La table est partitionnée et le prédicat est exploitable sur la colonne de partition
Data skipping Fichiers individuels Des statistiques de fichiers existent sur la colonne filtrée et leurs intervalles sont suffisamment sélectifs
Partition pruning

Si orders est partitionnée par order_date, Spark déduit automatiquement les partitions à lire :

SQL
SELECT *
FROM production.gold.orders
WHERE order_date = DATE '2026-08-07';
PYTHON
from pyspark.sql import functions as F

orders_df = spark.table("production.gold.orders").filter(
    F.col("order_date") == F.lit("2026-08-07")
)

Un filtre direct sur la colonne physique facilite le pruning. Une expression telle que year(order_date) = 2026 peut empêcher ou limiter ce mécanisme si la table est partitionnée par order_date plutôt que par une colonne year. Les colonnes de partition et le nombre de partitions relèvent en revanche d'un choix de conception manuel, présenté en 7.5.2.

Data skipping

Pour chaque fichier Delta, Databricks peut utiliser ses statistiques — notamment min, max, nombre de NULL et nombre de lignes — afin d'écarter automatiquement les fichiers qui ne peuvent pas contenir le résultat :

SQL
SELECT order_id, customer_id, amount
FROM production.gold.orders
WHERE customer_id = 123;
PYTHON
orders_df = (
    spark.table("production.gold.orders")
         .select("order_id", "customer_id", "amount")
         .filter(F.col("customer_id") == 123)
)

Les statistiques de data skipping sont collectées automatiquement lors des écritures, mais pas nécessairement pour toutes les colonnes d'une table externe. Si les valeurs de customer_id sont très dispersées dans chaque fichier, les intervalles min/max se chevauchent : le skipping est bien activé, mais il ignore peu de fichiers. Un meilleur layout obtenu avec ZORDER ou liquid clustering rend alors les statistiques plus sélectives.

Dans le plan et Spark UI, contrôler les filtres de partition, les filtres poussés, le nombre de fichiers lus et le volume scanné. À retenir : le pruning et le skipping sont automatiques à la lecture ; leur préparation et leur maintenance sont décrites de 7.5.2 à 7.5.6.

Les mécanismes suivants préparent et entretiennent physiquement la table pour rendre les lectures automatiques plus efficaces :

Mécanisme Configuration ou déclenchement Entretien ou exécution Résultat principal
Partitionnement classique Manuel à la création/écriture Les écritures placent les lignes dans les partitions Partition pruning automatique
ZORDER Manuel avec OPTIMIZE ... ZORDER BY Relancer explicitement sur les nouvelles données ; predictive optimization ne lance pas de Z-order Data skipping automatique amélioré
Liquid clustering avec clés Clés choisies manuellement avec CLUSTER BY (...) OPTIMIZE, manuellement ou via predictive optimization Data skipping automatique amélioré
Automatic Liquid Clustering Activation manuelle avec CLUSTER BY AUTO Predictive Optimization choisit/adapte les clés et organise les données de manière asynchrone Data skipping automatique amélioré
VACUUM manuel Commande explicite si nécessaire Supprime les fichiers non référencés après la rétention Réduit le stockage, mais peut limiter Time Travel et CDF
ANALYZE manuel Commande explicite si les statistiques doivent être rafraîchies À relancer après des changements importants si non automatisé Améliore les estimations du plan initial
Predictive optimization Activé explicitement, par héritage ou par défaut selon le compte Lance automatiquement OPTIMIZE, VACUUM et ANALYZE sur les tables managées éligibles Maintient layout, statistiques et stockage

7.5.2 Partitionnement classique

Le partitionnement convient surtout à une colonne peu ou modérément cardinale, souvent utilisée dans les filtres. Il faut éviter de créer une multitude de petites partitions.

SQL
CREATE TABLE production.gold.orders_by_date
USING DELTA
PARTITIONED BY (order_date)
AS SELECT * FROM production.silver.orders;
PYTHON
(
    spark.table("production.silver.orders")
         .write
         .format("delta")
         .partitionBy("order_date")
         .saveAsTable("production.gold.orders_by_date")
)

partitionBy("order_date") définit le layout physique de l'écriture, puis saveAsTable enregistre la table et ce partitionnement dans le metastore. Un filtre direct sur order_date bénéficie ensuite automatiquement du partition pruning. Cela ne rend pas tout partitionnement efficace : une colonne très cardinale produit trop de répertoires et de petits fichiers ; pour une nouvelle table managée, considérer d'abord liquid clustering.

7.5.3 Optimized writes, OPTIMIZE et ZORDER

Trois mécanismes répondent au problème des petits fichiers à des moments différents :

Mécanisme Moment Effet
Optimized writes Avant le commit Redistribue les lignes pour produire moins de fichiers, mieux dimensionnés
Auto compaction Après une écriture réussie Compacte automatiquement de petits fichiers récents
OPTIMIZE Maintenance d'une table existante Réécrit les petits fichiers déjà présents en fichiers plus gros

Les optimized writes peuvent ajouter un shuffle, mais évitent qu'un append streaming ou un MERGE fréquent ne crée un fichier par petite task/partition. Sur les runtimes modernes, ils sont activés automatiquement pour de nombreuses opérations Delta — notamment MERGE, UPDATE et DELETE — et sont autotunés sur les tables managées. Lorsqu'un réglage explicite est réellement nécessaire :

SQL
ALTER TABLE production.gold.orders SET TBLPROPERTIES (
  'delta.autoOptimize.optimizeWrite' = 'true',
  'delta.autoOptimize.autoCompact' = 'true'
);
PYTHON
# Réglages de session pour les écritures Delta concernées.
spark.conf.set("spark.databricks.delta.optimizeWrite.enabled", "true")
spark.conf.set("spark.databricks.delta.autoCompact.enabled", "true")

Optimized writes et auto compaction limitent les nouveaux petits fichiers, mais ne remplacent pas toujours un OPTIMIZE périodique sur tout l'historique. Pour réduire les petits fichiers sans besoin de placement particulier, utiliser OPTIMIZE sans ZORDER.

Ce principe vaut pour les sorties de COPY INTO, de Structured Streaming et de MERGE : optimized writes agit pendant les nouveaux commits, auto compaction juste après, puis OPTIMIZE traite l'existant. optimizeWrite n'est donc pas une option de suivi des fichiers de COPY INTO. Un premier OPTIMIZE sur une table de 10 To contenant des millions de petits fichiers peut être lent précisément parce qu'il doit lire, redistribuer et réécrire un grand volume ; prévenir leur accumulation est moins coûteux que la corriger tardivement. Selon le format de la table, le runtime et son partitionnement, cibler progressivement les données avec une clause WHERE prise en charge ou laisser Predictive Optimization entretenir la table managée.

OPTIMIZE est l'opération de réécriture qui compacte les petits fichiers. ZORDER BY est une stratégie optionnelle de placement ajoutée à cet OPTIMIZE pour rapprocher les valeurs sur lesquelles les requêtes filtrent souvent.

SQL
-- Compaction seule : améliore surtout la taille et le nombre de fichiers.
OPTIMIZE production.gold.orders;

-- Compaction et réorganisation pour améliorer le skipping sur customer_id.
OPTIMIZE production.gold.orders ZORDER BY (customer_id);
PYTHON
from delta.tables import DeltaTable

orders = DeltaTable.forName(spark, "production.gold.orders")
orders.optimize().executeCompaction()
orders.optimize().executeZOrderBy("customer_id")

Le Z-order est surtout utile sur quelques colonnes de filtre fréquentes, de cardinalité suffisante et disposant de statistiques. Trop de colonnes diluent son bénéfice. Configurer au besoin les colonnes suivies pour le data skipping, puis recalculer les statistiques Delta des fichiers existants :

Filtres dominants Layout Z-order plausible sur une table non liquid-clustered
WHERE event_ts BETWEEN ... ou recherche d'un timestamp précis ZORDER BY (event_ts)
Point lookup WHERE customer_id = ... ZORDER BY (customer_id)
Table partitionnée par country, puis filtres sur country et event_date Partition pruning sur country, puis ZORDER BY (event_date) dans les partitions
Plusieurs colonnes réellement sélectives ZORDER BY (col1, col2), en gardant peu de colonnes

Ce ne sont pas des règles universelles : si country n'est pas une colonne de partition et intervient dans les filtres sélectifs, il peut aussi entrer dans le layout. Pour une nouvelle table managée, préférer généralement liquid clustering, surtout lorsque les filtres évoluent.

Z-order implique une réécriture et une redistribution multidimensionnelle ; ajouter de nombreuses colonnes augmente le travail et, surtout, réduit la localité obtenue pour chacune. Choisir peu de colonnes sélectives d'après les filtres réels. Pour un point lookup, une clé de lookup peut être pertinente ; pour country + event_date, un partitionnement existant sur country permet d'abord le partition pruning, puis un Z-order sur event_date peut améliorer le skipping.

SQL
ALTER TABLE production.gold.orders SET TBLPROPERTIES (
  'delta.dataSkippingStatsColumns' = 'customer_id,order_date'
);

ANALYZE TABLE production.gold.orders COMPUTE DELTA STATISTICS;

COMPUTE DELTA STATISTICS alimente le data skipping par fichier ; il ne faut pas le confondre avec COMPUTE STATISTICS, utilisé en 7.3 pour les estimations de l'optimiseur et le plan initial.

7.5.4 Liquid clustering manuel ou automatique

Liquid clustering organise progressivement les données selon des clés modifiables. Databricks le recommande pour les nouvelles tables managées dans la plupart des cas, en particulier pour les colonnes de forte cardinalité, le skew, la croissance rapide ou des filtres évolutifs.

SQL
-- Clés choisies par l'équipe selon les filtres fréquents.
ALTER TABLE production.gold.user_events
CLUSTER BY (user_id, event_date);

-- Mode automatique : Predictive Optimization choisit et adapte les clés.
CREATE TABLE production.gold.orders
CLUSTER BY AUTO
AS SELECT * FROM production.silver.orders;

-- Applique incrémentalement le clustering lorsque l'entretien est manuel.
OPTIMIZE production.gold.user_events;
PYTHON
# La définition des clés reste une commande SQL depuis un notebook Python.
spark.sql("""
    ALTER TABLE production.gold.user_events
    CLUSTER BY (user_id, event_date)
""")
spark.sql("OPTIMIZE production.gold.user_events")

Avec des clés manuelles, choisir peu de colonnes discriminantes — jusqu'à quatre — principalement utilisées dans les filtres. Avec CLUSTER BY AUTO, Predictive Optimization analyse l'historique des requêtes et le coût attendu ; il peut adapter les clés ou n'en choisir aucune pour une table trop petite ou peu interrogée. Ce mode nécessite une table managée Unity Catalog et Predictive Optimization.

À retenir : liquid clustering n'est pas compatible sur une même table avec un partitionnement classique ou ZORDER. Changer les clés concerne d'abord les futures opérations ; une réorganisation complète des anciennes données peut nécessiter OPTIMIZE FULL selon le runtime.

Activer liquid clustering sur une table Delta utilise le protocole reader 3, writer 7 et les table features associées. Un ancien client Delta qui ne comprend pas ces protocoles peut donc ne plus lire ou écrire la table. Vérifier minReaderVersion, minWriterVersion et tableFeatures avec DESCRIBE DETAIL avant d'activer la fonctionnalité dans un écosystème hétérogène ; ne pas régler ces propriétés manuellement. Les versions de protocole ainsi relevées ne peuvent pas être rétrogradées.

7.5.5 Maintenance manuelle : VACUUM, ANALYZE, rétention, Time Travel et CDF

Les commandes manuelles restent utilisables sur les tables éligibles et sont nécessaires lorsque predictive optimization ne prend pas en charge la table ou lorsqu'une intervention contrôlée est souhaitée. La compaction et le placement avec OPTIMIZE/ZORDER sont présentés plus haut ; voici la gestion des anciens fichiers et des statistiques de l'optimiseur.

Version SQL :

SQL
-- Prévisualise les fichiers éligibles sans les supprimer.
VACUUM production.gold.orders DRY RUN;

-- Supprime les fichiers non référencés plus anciens que la rétention configurée.
-- La rétention par défaut est de 7 jours.
VACUUM production.gold.orders;

-- Même rétention exprimée explicitement ; prévisualisation sans suppression.
VACUUM production.gold.orders RETAIN 168 HOURS DRY RUN;

-- Statistiques de table pour l'optimiseur : taille et nombre de lignes.
ANALYZE TABLE production.gold.orders COMPUTE STATISTICS;

-- Ajoute les statistiques des colonnes importantes pour les filtres et joins.
ANALYZE TABLE production.gold.orders
COMPUTE STATISTICS FOR COLUMNS order_date, customer_id;

Version PySpark native pour VACUUM :

PYTHON
from delta.tables import DeltaTable

orders = DeltaTable.forName(spark, "production.gold.orders")
orders.vacuum(168)  # Rétention explicite de 168 heures, soit 7 jours.

ANALYZE TABLE reste une commande SQL sans équivalent direct dans l'API DataFrame. ANALYZE ne réécrit pas les données : il collecte des statistiques pour aider l'optimiseur à choisir un meilleur plan. Il ne faut pas le confondre avec OPTIMIZE, qui réécrit les fichiers, ni avec COMPUTE DELTA STATISTICS, qui recalcule les statistiques de data skipping par fichier.

Effets de la rétention sur Time Travel et CDF

VACUUM réduit le stockage en supprimant les fichiers non référencés — les anciens fichiers représentés comme tombstones dans le journal — plus anciens que delta.deletedFileRetentionDuration, dont la valeur par défaut est 7 jours. La propriété peut être ajustée avec prudence :

SQL
ALTER TABLE production.gold.orders SET TBLPROPERTIES (
  'delta.deletedFileRetentionDuration' = 'interval 14 days',
  'delta.logRetentionDuration' = 'interval 30 days'
);

deletedFileRetentionDuration règle l'éligibilité des fichiers supprimés à VACUUM; logRetentionDuration règle la conservation de l'historique du journal. Le time travel nécessite les deux : une version peut encore apparaître dans l'historique mais devenir illisible si VACUUM a supprimé un fichier qu'elle référence. Le même principe limite les versions CDF disponibles.

Ne pas réduire la rétention en dessous de la durée maximale pendant laquelle un lecteur, un stream ou un job peut rester actif ou arrêté : il pourrait reprendre en demandant un fichier déjà supprimé. DRY RUN permet de vérifier les candidats avant toute suppression.

Un DELETE fait immédiatement disparaître les lignes du snapshot courant, mais les anciens fichiers nécessaires au time travel restent physiquement présents jusqu'à leur purge après rétention. Pour une suppression réglementaire, traiter aussi les copies amont puis, si la table utilise des deletion vectors, matérialiser la suppression avant le VACUUM :

SQL
DELETE FROM production.silver.customers
WHERE customer_id = 12345;

-- Nécessaire pour réécrire les fichiers portant des deletion vectors.
REORG TABLE production.silver.customers APPLY (PURGE);

-- Après la période de rétention appropriée : contrôle, puis suppression physique.
VACUUM production.silver.customers DRY RUN;
VACUUM production.silver.customers;

VACUUM ... RETAIN 0 HOURS rend les anciennes données éligibles à une suppression irréversible et détruit le time travel correspondant. La vérification de sécurité refuse normalement une rétention inférieure à sept jours ; la désactiver puis utiliser RETAIN 0 HOURS n'est acceptable que dans une procédure exceptionnelle, contrôlée, sans lecteur ni écriture concurrents — jamais comme remède courant :

SQL
-- Exemple volontairement dangereux : ne pas exécuter dans un lab partagé.
SET spark.databricks.delta.retentionDurationCheck.enabled = false;
VACUUM production.silver.customers RETAIN 0 HOURS;

Après un VACUUM, un ancien fichier peut sembler encore lisible sur un cluster qui le possède dans son cache disque alors qu'un autre cluster échoue ; un redémarrage efface cette illusion. Le stockage constitue la vérité : ne jamais interpréter cette différence temporaire comme une rétention garantie.

Des MERGE très fréquents et peu sélectifs réécrivent de nombreux fichiers et ajoutent beaucoup d'actions AddFile/RemoveFile au Delta log ; ils peuvent donc produire à la fois petits fichiers, tombstones et métadonnées volumineuses. Le diagnostic consiste à batcher les petits commits, dédupliquer la source, améliorer le pruning/layout de la cible et activer les mécanismes de prévention des petits fichiers. Les checkpoints du Delta log accélèrent sa relecture. VACUUM supprime les anciens fichiers de données, pas directement le répertoire _delta_log, dont le nettoyage suit sa propre rétention. Sur les runtimes compatibles et après au moins un VACUUM complet éligible, VACUUM LITE peut éviter de lister tout le stockage en utilisant le journal.

7.5.6 Automatiser la maintenance avec predictive optimization

La maintenance Delta combine trois opérations distinctes : OPTIMIZE réécrit le layout des fichiers, VACUUM supprime les anciens fichiers non référencés et ANALYZE collecte des statistiques. Elles peuvent être lancées manuellement comme en 7.5.3 et 7.5.5 ou, pour les tables managées Unity Catalog éligibles, automatisées par predictive optimization.

Predictive optimization analyse l'usage et lance automatiquement des opérations de maintenance sur les tables managées Unity Catalog :

  • OPTIMIZE : compacte et améliore le layout ;
  • VACUUM : supprime les anciens fichiers devenus inutiles après la période de rétention ;
  • ANALYZE : maintient des statistiques utiles à l'optimiseur.

Avec CLUSTER BY AUTO, Predictive Optimization peut également sélectionner et adapter les clés de liquid clustering. Il ne faut pas programmer un OPTIMIZE redondant s'il s'en charge déjà. En revanche, il ne lance pas de ZORDER.

Activer, désactiver ou hériter

Le réglage suit la hiérarchie compte → catalog → schema → table. Un objet enfant hérite de son parent tant qu'il n'a pas d'override explicite :

SQL
-- Override au niveau catalog : active pour les enfants qui héritent.
ALTER CATALOG production ENABLE PREDICTIVE OPTIMIZATION;

-- Override au niveau schema : bloque l'activation héritée du catalog.
ALTER SCHEMA production.raw DISABLE PREDICTIVE OPTIMIZATION;

-- Supprime l'override du schema : reprend la valeur du catalog.
ALTER SCHEMA production.raw INHERIT PREDICTIVE OPTIMIZATION;

-- Override ou héritage au niveau d'une table managée.
ALTER TABLE production.gold.orders ENABLE PREDICTIVE OPTIMIZATION;
ALTER TABLE production.gold.orders DISABLE PREDICTIVE OPTIMIZATION;
ALTER TABLE production.gold.orders INHERIT PREDICTIVE OPTIMIZATION;

ENABLE et DISABLE définissent une valeur explicite ; INHERIT ne signifie pas « activer », mais supprimer l'override et reprendre le réglage effectif du parent. DATABASE est accepté comme alias de SCHEMA.

Vérifier le statut effectif et son origine avec EXTENDED :

SQL
DESCRIBE CATALOG EXTENDED production;
DESCRIBE SCHEMA EXTENDED production.gold;
DESCRIBE TABLE EXTENDED production.gold.orders;

Chercher le champ Predictive Optimization, qui indique si le réglage est activé et s'il est hérité. Sur les runtimes récents, DESCRIBE TABLE EXTENDED production.gold.orders AS JSON peut aussi exposer les dernières évaluations et raisons pour lesquelles une opération automatique a été appliquée ou ignorée. Il faut être owner de l'objet ou disposer de MANAGE pour modifier ce réglage.

Suivre les opérations automatiques

La table système system.storage.predictive_optimization_operations_history permet d'auditer les tables traitées, le type et le statut des opérations, leurs métriques et leur consommation estimée en DBU :

SQL
SELECT
  date_trunc('DAY', start_time) AS day,
  concat_ws('.', catalog_name, schema_name, table_name) AS table_name,
  operation_type,
  operation_status,
  sum(usage_quantity) AS estimated_dbus
FROM system.storage.predictive_optimization_operations_history
WHERE start_time >= current_timestamp() - INTERVAL 30 DAYS
GROUP BY ALL
ORDER BY day DESC, estimated_dbus DESC;

Dans cette table, la compaction automatique apparaît sous operation_type='COMPACTION', pas nécessairement sous le nom de commande OPTIMIZE; on trouve aussi notamment VACUUM, ANALYZE et CLUSTERING. usage_quantity est exprimé en ESTIMATED_DBU et ne constitue pas directement un coût monétaire. La table est régionale, actuellement en Public Preview, et ses données peuvent arriver avec retard.

Pièges :

  • predictive optimization concerne les tables managées, pas les tables externes ;
  • OPTIMIZE ne trie pas globalement tout le dataset à chaque appel ;
  • VACUUM n'est pas une optimisation de requête immédiate et peut limiter le time travel ;
  • optimiser le layout ne corrige pas un join erroné ou une lecture inutile de toutes les colonnes.

8. Gouvernance et sécurité avec Unity Catalog

La section 2.3 décrit quels objets sont gouvernés et où résident leurs données. Cette section répond à trois autres questions : qui agit, avec quels privilèges et quelles lignes ou colonnes restent visibles.

8.1 Principals, ownership et responsabilités

Un principal est :

  • un utilisateur ;
  • un groupe ;
  • un service principal pour les workloads automatisés.

Attribuer les droits aux groupes plutôt qu'à chaque utilisateur simplifie les arrivées, départs et audits.

Rôle/capacité Responsabilité principale
Account admin Identités, groupes, workspaces et configuration au niveau compte
Metastore admin Gouvernance globale du metastore Unity Catalog
Workspace admin Objets et réglages du workspace, selon les limites Unity Catalog
Owner d'un objet Tous les privilèges sur cet objet et capacité à les déléguer
Principal avec MANAGE Gérer les privilèges, ownership et opérations de gestion sans être owner
Service principal Identité non humaine pour Jobs et CI/CD

Le principe du moindre privilège s'applique : seulement les droits nécessaires, à la portée la plus appropriée, avec une identité de run non humaine en production.

Changer le propriétaire d'une table

Dans l'interface, ouvrir Catalog Explorer, sélectionner la table, puis modifier le champ Owner sur la page de la table. C'est l'emplacement attendu dans le QCM.

L'opération possède aussi un équivalent SQL pour l'automatisation :

SQL
ALTER TABLE production.sales.orders
OWNER TO `data_platform_owners`;

Le nouveau propriétaire doit être un principal Unity Catalog, idéalement un groupe ou un service principal plutôt qu'une personne pour un actif de production. Il faut disposer des droits permettant le transfert d'ownership ; changer le propriétaire n'est pas équivalent à accorder seulement SELECT ou MODIFY.

8.2 Hiérarchie des privilèges

Pour lire une table, un principal a généralement besoin de :

TEXT
USE CATALOG sur le catalogue
    + USE SCHEMA sur le schéma
        + SELECT sur la table/vue

Dans l'interface actuelle, les grants se gèrent dans Catalog Explorer : cliquer sur Catalog dans la barre latérale, sélectionner le catalogue, schéma, table, vue ou fonction, puis ouvrir l'onglet Permissions. Certains anciens supports l'appellent Data Explorer. L'owner, le propriétaire d'un catalogue/schéma parent, un principal avec MANAGE ou un administrateur autorisé peut accorder ou révoquer les privilèges selon sa portée.

SQL
GRANT USE CATALOG ON CATALOG production TO `sales_analysts`;
GRANT USE SCHEMA ON SCHEMA production.sales TO `sales_analysts`;
GRANT SELECT ON TABLE production.sales.orders TO `sales_analysts`;

-- Grant au niveau schéma : hérité par les objets actuels et futurs concernés.
GRANT SELECT ON SCHEMA production.sales TO `sales_analysts`;

GRANT MODIFY ON TABLE production.sales.orders TO `sales_engineers`;

-- Autorise l'appel d'une fonction Unity Catalog.
GRANT EXECUTE ON FUNCTION production.utils.normalize_email
TO `sales_engineers`;

-- Accorde tous les privilèges applicables à cette table, sauf les exclusions décrites plus bas.
GRANT ALL PRIVILEGES ON TABLE production.sales.orders
TO `sales_admins`;

SHOW GRANTS ON TABLE production.sales.orders;

REVOKE MODIFY ON TABLE production.sales.orders FROM `sales_engineers`;
  • SELECT lit une table, vue ou vue matérialisée.
  • MODIFY permet insertion, mise à jour et suppression de données.
  • EXECUTE permet d'appeler une fonction, un modèle enregistré ou un service compatible ; pour une fonction, il faut aussi pouvoir traverser ses catalogue et schéma avec USE CATALOG et USE SCHEMA.
  • CREATE TABLE sur un schéma permet d'y créer une table.
  • USE CATALOG et USE SCHEMA donnent le droit de traverser le conteneur, pas de lire ses données.
  • ALL PRIVILEGES représente dynamiquement tous les privilèges applicables à l'objet et à ses enfants, mais n'inclut jamais MANAGE, EXTERNAL USE SCHEMA ni EXTERNAL USE LOCATION.
  • Les grants sur catalogue ou schéma sont hérités par les objets enfants selon le modèle de privilèges Unity Catalog.

Privilèges courants par objet à reconnaître pour l'examen :

Objet Unity Catalog Privilèges principaux
Catalog USE CATALOG, BROWSE, CREATE SCHEMA, APPLY TAG, MANAGE et grants héritables vers les enfants
Schema USE SCHEMA, CREATE TABLE, CREATE FUNCTION, CREATE MATERIALIZED VIEW, CREATE MODEL, CREATE VOLUME, APPLY TAG, MANAGE et grants héritables vers les enfants
Table SELECT, MODIFY, APPLY TAG, MANAGE
View SELECT, APPLY TAG, MANAGE
Materialized view SELECT, REFRESH, APPLY TAG, MANAGE
Function ou modèle enregistré EXECUTE, MANAGE ; les modèles ajoutent notamment CREATE MODEL VERSION et APPLY TAG
Volume READ VOLUME, WRITE VOLUME, APPLY TAG, MANAGE
External location BROWSE, READ FILES, WRITE FILES, CREATE EXTERNAL TABLE, CREATE EXTERNAL VOLUME, CREATE MANAGED STORAGE, EXTERNAL USE LOCATION, MANAGE
Storage credential CREATE EXTERNAL LOCATION, CREATE EXTERNAL TABLE, READ FILES, WRITE FILES, MANAGE

ALL PRIVILEGES est utilisable sur ces objets lorsque le type le prend en charge. Sur une table, il implique actuellement SELECT, MODIFY et APPLY TAG, mais pas MANAGE. OWNERSHIP est une capacité liée au propriétaire de l'objet, pas un privilège inclus par GRANT ALL PRIVILEGES.

Pour charger des fichiers avec COPY INTO ou Auto Loader, le privilège de lecture dépend de la manière dont la source est exposée :

Source d'ingestion Privilège de lecture principal Privilèges usuels sur la cible
URI cloud couverte par une external location READ FILES sur l'external location USE CATALOG, USE SCHEMA, puis MODIFY sur la table cible
Chemin /Volumes/... READ VOLUME sur le volume USE CATALOG, USE SCHEMA, puis MODIFY sur la table cible

La réponse courte « READ FILES + MODIFY » ne vaut donc que pour une source lue directement à travers une external location. Elle ne remplace ni READ VOLUME pour un chemin de volume, ni les privilèges de parcours USE CATALOG et USE SCHEMA. Si la table cible doit aussi être créée, il faut en outre CREATE TABLE sur son schéma.

Piège important sur DENY : la commande SQL DENY existe pour le hive_metastore historique et un deny y prime sur un grant, mais elle n'est pas supportée par Unity Catalog. Dans Unity Catalog, retirer un droit avec REVOKE et utiliser groupes, ownership, filtres, masques ou ABAC pour les règles fines. Le fait que le référentiel cite DENY peut précisément servir à tester cette distinction.

8.3 Contrôles d'accès fins : row filters, column masks, dynamic views et ABAC

Les grants de la section 8.2 autorisent l'accès à l'objet. Les contrôles fins adaptent ensuite les lignes ou valeurs visibles sans multiplier les copies physiques des données.

8.3.1 Row filters et column masks

La row-level security (RLS) se met en œuvre dans Unity Catalog notamment avec des row filters, des dynamic views ou des politiques ABAC. Un row filter est une fonction booléenne évaluée pour chaque ligne ; une column mask remplace la valeur visible selon l'identité du lecteur.

SQL
CREATE OR REPLACE FUNCTION production.security.region_filter(region STRING)
RETURN IF(
  is_account_group_member('global_sales'),
  true,
  region = 'FR'
);

ALTER TABLE production.sales.orders
SET ROW FILTER production.security.region_filter ON (region);

-- Variante : attacher le filtre dès la création de la table.
CREATE TABLE production.sales.orders_secured (
  order_id BIGINT,
  region STRING,
  amount DECIMAL(12, 2)
)
USING DELTA
WITH ROW FILTER production.security.region_filter ON (region);
SQL
CREATE OR REPLACE FUNCTION production.security.mask_email(email STRING)
RETURN CASE
  WHEN is_account_group_member('pii_readers') THEN email
  ELSE regexp_replace(email, '(^.).*(@.*$)', '$1***$2')
END;

ALTER TABLE production.sales.customers
ALTER COLUMN email
SET MASK production.security.mask_email;
  • Le row filter retire du résultat les lignes pour lesquelles la fonction retourne false.
  • Le mask conserve la colonne, mais transforme la valeur visible ; son type de retour doit être compatible.
  • Ces politiques table-level conviennent à une logique propre à une table.

Une fonction de masque peut aussi dépendre d'autres colonnes de la même ligne. La colonne masquée est implicitement le premier argument ; USING COLUMNS fournit les arguments supplémentaires dans l'ordre :

SQL
CREATE OR REPLACE FUNCTION production.security.mask_national_id(
  national_id STRING,
  country STRING
)
RETURN CASE
  WHEN is_account_group_member('pii_readers') THEN national_id
  WHEN country = 'FR' THEN concat('***', right(national_id, 4))
  ELSE 'REDACTED'
END;

ALTER TABLE production.sales.customers
ALTER COLUMN national_id
SET MASK production.security.mask_national_id
USING COLUMNS (country);

Le propriétaire ou gestionnaire qui attache la fonction doit pouvoir l'utiliser, notamment avec USE CATALOG, USE SCHEMA et EXECUTE sur la fonction. Il n'existe pas d'équivalent DataFrame PySpark à cette propriété Unity Catalog.

Un row filter peut recevoir plusieurs colonnes. L'ordre et les types des colonnes indiquées dans ON (...) doivent correspondre aux paramètres de la fonction :

SQL
CREATE OR REPLACE FUNCTION production.security.contract_region_filter(
  contract_type STRING,
  region STRING
)
RETURN
  is_account_group_member('contract_admins')
  OR (contract_type = 'STANDARD' AND region = 'FR');

ALTER TABLE production.sales.contracts
SET ROW FILTER production.security.contract_region_filter
ON (contract_type, region);

Une table ne peut avoir qu'un row filter attaché à la fois ; regrouper les conditions nécessaires dans la même fonction. Il n'existe pas d'équivalent DataFrame PySpark : la règle est une propriété gouvernée de la table.

8.3.2 Dynamic views : sécuriser une vue

Une dynamic view est une vue Unity Catalog dont le SQL utilise l'identité ou les groupes de l'utilisateur courant pour filtrer des lignes ou masquer des valeurs. Elle peut aussi projeter, transformer ou joindre plusieurs tables. Elle ne stocke pas les données : sa requête est évaluée à la lecture et peut donc produire un résultat différent selon l'utilisateur.

SQL
CREATE OR REPLACE VIEW production.sales.customers_secure AS
SELECT
  customer_id,
  CASE
    WHEN is_account_group_member('pii_readers') THEN email
    ELSE regexp_replace(email, '(^.).*(@.*$)', '$1***$2')
  END AS email,
  country
FROM production.sales.customers;

GRANT USE CATALOG ON CATALOG production TO `sales_analysts`;
GRANT USE SCHEMA ON SCHEMA production.sales TO `sales_analysts`;
GRANT SELECT ON VIEW production.sales.customers_secure TO `sales_analysts`;

Les membres de pii_readers voient l'adresse complète ; les autres voient une valeur masquée. Pour que la vue constitue une frontière de sécurité :

  • son owner doit pouvoir lire les tables et vues référencées ;
  • les consommateurs reçoivent USE CATALOG, USE SCHEMA et SELECT sur la vue ;
  • ne pas leur accorder directement SELECT sur les tables sources, y compris par héritage depuis un catalogue ou un schéma, sinon ils peuvent contourner la vue.

Contrairement à un column mask ou un row filter attaché à une table, la règle d'une dynamic view ne s'applique que lorsque les utilisateurs interrogent cette vue. Le mot dynamic désigne ici l'adaptation du résultat à l'identité du lecteur, pas une materialized view ni un mécanisme de rafraîchissement.

Dans un environnement legacy sans Unity Catalog, is_member('workspace_group') peut tester l'appartenance directe ou indirecte à un groupe local au workspace dans la logique d'une vue. Ces groupes sont limités à ce workspace et incompatibles avec les privilèges Unity Catalog. Pour une règle gouvernée par Unity Catalog, utiliser des account groups et préférer is_account_group_member(...); is_member peut aussi reconnaître un account group affecté au workspace, mais la fonction account-level rend l'intention explicite.

8.3.3 ABAC Unity Catalog

ABAC signifie Attribute-Based Access Control. Une politique centrale correspond aux objets par leurs governed tags, puis applique automatiquement un row filter ou un column mask.

TEXT
Governed tag account-level : pii = credit_card
                 ↓ affecté directement à une colonne
bank.safebox.cards.card_number
                 ↓ MATCH COLUMNS de la policy
fonction de masque appliquée à la lecture

ABAC est recommandé pour les règles homogènes à grande échelle :

  • politique attachée à un catalogue, schéma ou table ;
  • objets ciblés par tags gouvernés ;
  • application dynamique et centralisée ;
  • séparation entre auteurs de politiques et propriétaires de données ;
  • couverture automatique des nouveaux objets correctement tagués.
Créer le governed tag et tagguer une colonne

Un governed tag est défini au niveau du compte avec des valeurs autorisées. Sa création en SQL nécessite Databricks Runtime 18.1 ou supérieur ; elle est aussi disponible dans Catalog > Govern > Governed Tags :

SQL
CREATE GOVERNED TAG pii
DESCRIPTION 'Catégorie de donnée personnelle'
VALUES ('credit_card', 'ssn', 'email');

-- Le tag doit être affecté directement à chaque colonne concernée.
ALTER TABLE bank.safebox.cards
ALTER COLUMN card_number
SET TAGS ('pii' = 'credit_card');

-- Vérifier l'affectation dans le catalogue.
SELECT
  table_schema,
  table_name,
  column_name,
  tag_name,
  tag_value
FROM bank.information_schema.column_tags
WHERE table_schema = 'safebox'
  AND table_name = 'cards';

Avec Databricks Runtime 16.1 ou supérieur, la forme moderne équivalente est SET TAG ON COLUMN bank.safebox.cards.card_number pii = credit_card. Les clés et valeurs sont sensibles à la casse. Une colonne n'hérite pas des tags de sa table, de son schéma ou de son catalogue : pour qu'un MATCH COLUMNS la trouve, elle doit porter directement le tag. Une commande ne taggue qu'une colonne à la fois.

Pour affecter un governed tag, il faut être owner ou disposer de USE CATALOG, USE SCHEMA et APPLY TAG sur l'objet, ainsi que de ASSIGN sur le governed tag. La création du governed tag exige le privilège account-level CREATE. Restreindre soigneusement APPLY TAG et ASSIGN : retirer un tag peut empêcher une policy ABAC de protéger la donnée. Les métadonnées de tags étant stockées en clair, ne jamais mettre une valeur sensible réelle dans le nom ou la valeur du tag.

Le tag seul ne masque ni ne filtre rien. La création de la policy nécessite ensuite l'ownership ou MANAGE sur sa portée et, si une UDF réalise le filtrage ou le masquage, EXECUTE sur cette fonction.

Exemple : masquer toutes les colonnes du schéma bank.safebox portant le governed tag pii = credit_card pour les analystes financiers, sauf les administrateurs :

SQL
CREATE POLICY banking_policy
ON SCHEMA bank.safebox
COLUMN MASK bank.safebox.mask_credit_card
TO financial_analysts
EXCEPT admins
FOR TABLES
MATCH COLUMNS has_tag_value('pii', 'credit_card') AS cc
ON COLUMN cc;

Lecture de la policy :

  • ON SCHEMA définit la portée, y compris les tables futures du schéma ;
  • TO désigne les principals soumis au masque et EXCEPT les principals exemptés ;
  • MATCH COLUMNS ... AS cc sélectionne par tag les colonnes et leur donne un alias ;
  • ON COLUMN cc passe la valeur de chaque colonne correspondante à la fonction de masque.

La fonction bank.safebox.mask_credit_card doit exister dans Unity Catalog ; son premier paramètre et son type de retour doivent être compatibles avec la colonne masquée. Les tags doivent être governed. Les formes historiques hasTag() et hasTagValue() restent acceptées pour compatibilité, mais préférer has_tag() et has_tag_value() dans le nouveau code. Une policy masque ou filtre des données déjà accessibles : elle ne remplace pas les privilèges USE CATALOG, USE SCHEMA et SELECT. Il n'existe pas d'équivalent PySpark direct au DDL CREATE POLICY ; un programme Python peut au besoin exécuter ce SQL avec spark.sql(...).

Choisir selon la portée recherchée :

Besoin Mécanisme
Règle propre à une table ou à une colonne Row filter/column mask table-level
Exposer une projection, un join ou une logique SQL complète Dynamic view
Appliquer la même règle à de nombreuses tables, y compris futures ABAC avec governed tags

8.4 Partager des données avec Delta Sharing

Delta Sharing est le protocole ouvert de partage sécurisé de tables, vues et autres actifs sans copier les données chez le fournisseur. Le fournisseur crée un share et le recipient obtient un accès en lecture aux actifs ajoutés, avec les contrôles Unity Catalog du fournisseur.

SQL
CREATE SHARE IF NOT EXISTS sales_share;

ALTER SHARE sales_share
ADD TABLE production.sales.orders;

CREATE RECIPIENT IF NOT EXISTS partner_bi;
GRANT SELECT ON SHARE sales_share TO RECIPIENT partner_bi;

Il n'existe pas d'équivalent DataFrame PySpark à ces DDL de gouvernance ; les exécuter au besoin avec spark.sql(...).

Pour « partager entre workspaces », distinguer la frontière :

Situation Mécanisme
Workspaces attachés au même metastore Unity Catalog Accès direct au même objet avec grants et, si configurés, workspace catalog bindings ; Delta Sharing n'est généralement pas nécessaire
Workspaces/recipients sur des metastores, comptes, régions ou clouds différents Delta Sharing Databricks-to-Databricks, aussi présenté comme Open Sharing/OpenSharing dans la documentation récente
Consommateur sans Databricks Delta Sharing ouvert avec un client compatible et un credential de recipient

Delta Sharing partage une vue gouvernée des données, pas leur ownership ni des droits d'écriture sur la table source. Il convient donc au partage inter-organisation ou inter-metastore ; pour une simple collaboration dans le même metastore, préférer les privilèges Unity Catalog.

8.5 Métadonnées, audit, lineage et secrets

Même si ces thèmes sont moins explicites dans le nouveau blueprint, ils complètent le modèle de gouvernance :

  • Unity Catalog capture automatiquement le lineage entre tables, colonnes, notebooks, Jobs et dashboards pris en charge ;
  • les system tables et audit logs servent à analyser accès et opérations ;
  • les secrets doivent être récupérés via un secret scope ou une identité managée/service principal ;
  • les credentials cloud ne doivent pas être embarquées dans le code ; Unity Catalog utilise storage credentials et external locations.

Utiliser Databricks Secrets

Dans les DDL qui le prennent en charge, SQL peut référencer une valeur sans l'écrire en clair, comme dans user secret ('federation', 'postgres_user') et password secret (...) dans l'exemple de CREATE CONNECTION en 3.5. Dans un notebook Python :

PYTHON
jdbc_password = dbutils.secrets.get(
    scope="jdbc-production",
    key="password",
)

Databricks tente de remplacer par [REDACTED] la valeur secrète littérale lorsqu'elle est affichée dans les sorties prises en charge. Ce mécanisme n'est pas une frontière de sécurité : un utilisateur autorisé à exécuter du code peut transformer ou transmettre la valeur, et une valeur dérivée peut ne plus être reconnue. Protéger le secret scope par ACL, appliquer le moindre privilège et éviter tout print, log, paramètre visible ou stockage du secret dans Git.

Métadonnées SQL avec information_schema

Unity Catalog expose ses métadonnées au moyen de vues SQL standardisées : catalogs, schemas, tables, colonnes, propriétaires, créateurs, timestamps, privilèges, routines, row filters ou column masks.

Namespace Portée
<catalog>.information_schema Objets du catalog indiqué uniquement
system.information_schema Objets de tous les catalogs du metastore Unity Catalog, hors hive_metastore

Dans les deux cas, les résultats sont automatiquement filtrés selon les privilèges Unity Catalog de l'utilisateur : « tous les catalogs » ne signifie donc pas « tous les objets sans autorisation ».

Version SQL :

SQL
-- Vue transverse au metastore, limitée aux objets visibles par l'utilisateur.
SELECT
  table_catalog,
  table_schema,
  table_name,
  table_type,
  table_owner,
  created,
  created_by,
  last_altered
FROM system.information_schema.tables
WHERE table_catalog = 'production'
  AND table_schema = 'gold'
ORDER BY created DESC;

-- Même type de métadonnées, limité au catalog production.
SELECT table_schema, table_name, table_owner
FROM production.information_schema.tables;

Version PySpark :

PYTHON
metadata_df = (
    spark.table("system.information_schema.tables")
         .select(
             "table_catalog",
             "table_schema",
             "table_name",
             "table_type",
             "table_owner",
             "created",
             "created_by",
         )
         .where("table_catalog = 'production' AND table_schema = 'gold'")
)

Le DataFrame lit ici la même vue système ; SQL reste généralement plus direct pour explorer les métadonnées. Pour une requête transverse, appliquer des filtres sélectifs sur les identifiants en minuscules plutôt qu'un LOWER(...) : LIMIT réduit l'affichage mais pas le travail de recherche sous-jacent.


9. Tables de décision à connaître par cœur

Cette section sert de révision rapide : partir de la contrainte discriminante du scénario, puis choisir l'objet ou le mécanisme correspondant.

9.1 Choisir le mode d'ingestion

Besoin dominant Choix État incrémental À retenir
Création ponctuelle depuis une requête CTAS Aucun Le schéma vient du SELECT
Charger périodiquement de nouveaux fichiers en SQL COPY INTO Historique des fichiers chargés Simple et idempotent, sauf force=true
Très nombreux fichiers ou arrivée continue Auto Loader Checkpoint Structured Streaming cloudFiles, évolution de schéma et rescued data
Événements Kafka à forte cadence/faible latence Structured Streaming Kafka Offsets dans le checkpoint startingOffsets ne vaut qu'au premier démarrage
Réplication d'une source prise en charge Lakeflow Connect managé Snapshot initial, puis incrémental lorsque possible Préférer le connecteur au polling JDBC artisanal
Interroger une base distante sans copier ses données Lakehouse Federation Pas d'état d'ingestion CONNECTION + foreign catalog ; performances dépendantes de la source
Base JDBC sans connecteur incrémental Runs batch avec requête sur une colonne curseur Watermark métier persisté après succès Curseur monotone/composite et MERGE idempotent

9.2 Choisir le mécanisme d'évolution de schéma

Mécanisme Portée Effet principal
Auto Loader addNewColumns Schéma découvert à la lecture Ajoute les nouvelles colonnes puis demande un redémarrage ; anciennes lignes à NULL
Auto Loader addNewColumnsWithTypeWidening Colonnes et élargissements de types pris en charge Même arrêt/reprise, avec élargissement tel que INT vers LONG ; changements incompatibles dans rescued data
Auto Loader failOnNewColumns Schéma découvert à la lecture Échoue sans modifier le schéma enregistré
Auto Loader rescue Données inattendues à la lecture Place les champs non conformes dans _rescued_data
Auto Loader none Schéma figé à la lecture Ignore les nouvelles colonnes sans échec ; les sauve seulement si rescued data est configuré
Auto Loader schemaHints Inférence à la lecture Guide le type attendu de quelques colonnes sans déclarer tout le schéma
mergeSchema=true Écriture DataFrame vers Delta Ajoute des colonnes compatibles à la cible
spark.databricks.delta.schema.autoMerge.enabled=true Toutes les écritures Delta compatibles de la SparkSession Évolution implicite globale ; mécanisme legacy, préférer une option locale
overwriteSchema=true Overwrite d'une table Delta Remplace le schéma avec les données écrasées
MERGE WITH SCHEMA EVOLUTION Upsert Delta Fait évoluer la cible pendant le MERGE

Sans schéma explicite, addNewColumns est le mode Auto Loader par défaut ; avec un schéma fourni, le défaut devient none. Un schema hint reste compatible avec addNewColumns. Dans un stream PySpark autonome vers Delta, configurer séparément la lecture Auto Loader et l'évolution de la cible ; Lakeflow Pipelines gère cette articulation pour ses streaming tables.

9.3 Choisir l'objet de sortie

Objet Résultat stocké ? Mise à jour Cas d'usage
Temporary view Non Pendant la session ou l'update du pipeline Étape interne non publiée
Vue Unity Catalog Non Recalcul à la lecture Définition SQL gouvernée et réutilisable
Materialized view Oui Rafraîchie par la plateforme Résultat analytique ; objet autonome planifiable par SCHEDULE, ou refresh du pipeline
Streaming table Oui Progression incrémentale depuis un flux Ingestion et transformations streaming
Table Delta Oui DML ou réécriture explicite État persistant contrôlé par le traitement

9.4 Choisir le type ou la copie d'une table

Besoin Choix Conséquence essentielle
Unity Catalog gère emplacement et cycle de vie Table managée DROP TABLE supprime à terme métadonnées et données
Chemin cloud imposé ou partagé hors Databricks Table externe DROP TABLE conserve les fichiers
Données restant dans un système fédéré Table foreign La source externe garde le cycle de vie
Convertir l'objet externe existant en conservant son identité SET MANAGED Gère les écritures concurrentes et minimise la bascule
Créer une copie indépendante Deep clone Copie les données et possède son propre historique
Créer rapidement un snapshot dépendant de la source Shallow clone Référence les fichiers source

9.5 Rejouer ou reprendre en sécurité

Mécanisme Ce qui est repris/rejoué Condition de sûreté
Checkpoint streaming Offsets, commits et état de la requête Réutiliser le même checkpoint uniquement pour la même requête
Checkpoint supprimé/nouveau Nouvelle requête et données encore disponibles potentiellement relues Préparer une cible idempotente ; ne pas supprimer pour « réparer » un stream
schemaLocation recréé Schéma réinféré, mais progression des fichiers inchangée si le checkpoint est conservé Revalider les types ; ce n'est pas un mécanisme de déduplication
COPY INTO Nouveaux fichiers non suivis Ne pas forcer le replay sans stratégie de déduplication
MERGE Changements de la source Clé stable, source dédupliquée et logique déterministe
foreachBatch Un micro-batch pouvant être retenté Rendre le callback idempotent avec MERGE ou batch_id
Retry/repair run Une task ou les tasks non réussies L'orchestrateur ne rend pas les écritures idempotentes
Append aveugle Toutes les lignes de l'input Risque de doublons lors d'un replay

9.6 Choisir l'optimisation adaptée

Besoin Mécanisme Effet
Réduire le nombre de partitions sans redistribution complète coalesce Moins coûteux, mais peut déséquilibrer
Rééquilibrer ou partitionner par clé repartition Shuffle complet
Ignorer des partitions de table Partition pruning Exploite un filtre direct sur la colonne de partition
Ignorer des fichiers Delta Data skipping Exploite les statistiques de fichiers
Limiter les petits fichiers pendant une écriture Optimized writes/auto compaction Agit avant ou juste après le commit, souvent automatiquement
Compacter/réécrire les fichiers OPTIMIZE Améliore le layout physique
Améliorer le skipping sur quelques colonnes de filtre ZORDER BY Option de placement d'OPTIMIZE
Layout adaptable recommandé pour les nouvelles tables managées Liquid clustering Clés modifiables, manuelles ou automatiques

9.7 Choisir le contrôle d'accès

Besoin Mécanisme
Autoriser l'accès à un objet GRANT avec USE CATALOG, USE SCHEMA, SELECT/MODIFY
Retirer certaines lignes Row filter
Transformer une valeur sensible Column mask
Exposer une projection, un join ou une logique contextuelle complète Dynamic view
Appliquer une politique homogène à de nombreux objets tagués ABAC avec governed tags
Partager en lecture entre metastores/organisations Delta Sharing

10. Pièges de QCM classés par thème

Pour chaque proposition, vérifier d'abord la portée du mécanisme : lecture ou écriture, batch ou streaming, table ou pipeline, configuration ou exécution.

10.1 Plateforme, Delta Lake et Unity Catalog

  • Après le DROP d'une table managée UC, UNDROP TABLE permet par défaut une récupération pendant 7 jours ; après expiration, les fichiers sont supprimés de façon asynchrone, généralement sous 48 heures. Une table externe conserve ses fichiers.
  • USE CATALOG et USE SCHEMA ne suffisent pas à lire une table : il faut aussi SELECT.
  • DENY n'est pas pris en charge par Unity Catalog ; il appartient au modèle legacy Hive metastore.
  • Un row filter retire des lignes ; un column mask transforme les valeurs visibles.
  • Une dynamic view applique une logique SQL contextuelle, mais peut être contournée si l'utilisateur lit directement ses tables sources.
  • ABAC applique centralement des règles aux objets ou colonnes sélectionnés par governed tags ; une colonne doit être tagguée directement et le tag seul n'applique aucun masque. TO et EXCEPT déterminent les principals concernés.
  • Une violation de contrainte CHECK fait échouer toute la transaction, pas seulement la ligne invalide.
  • Un shallow clone référence les fichiers source ; un deep clone copie les données et devient indépendant.
  • Une storage credential représente l'identité cloud ; une external location lui associe un chemin gouverné.
  • Une table gouverne des données tabulaires ; un volume gouverne des fichiers non tabulaires. Pour un bucket géré par le client, un external volume gouverne les accès via /Volumes, pas les accès directs accordés par l'IAM cloud.
  • Pour Lakehouse Federation, une CONNECTION décrit la source distante et ses credentials, puis un foreign catalog expose ses objets sans les copier. Lakeflow Connect, lui, ingère les données dans Delta.
  • ALTER COLUMN ... SET NOT NULL vérifie d'abord toutes les lignes existantes et échoue si l'une contient NULL.
  • Le source d'un notebook est un objet workspace chiffré du control plane ; le stockage de ses résultats suit d'autres règles.
  • Le propriétaire d'une table se modifie dans le champ Owner de Catalog Explorer ou avec ALTER TABLE ... OWNER TO.
  • Delta Lake ne remplace pas le stockage : ses fichiers et son journal résident généralement dans le cloud object storage.
  • Chaque commit Delta publie un nouveau snapshot cohérent : un lecteur ne voit pas un mélange partiel de deux versions concurrentes.
  • Un overwrite Delta remplace l'état courant mais crée une nouvelle version ; l'historique reste lisible jusqu'aux limites de rétention et de VACUUM.
  • delta.appendOnly=true interdit les modifications/suppressions de lignes existantes, mais autorise un MERGE insert-only et ne déduplique pas les nouveaux inserts.
  • Les contraintes Delta sont enregistrées dans les métadonnées du transaction log et appliquées à chaque commit.
  • Entre workspaces d'un même metastore, utiliser d'abord les grants UC ; Delta Sharing vise surtout un autre metastore, compte ou consommateur externe.
  • <catalog>.information_schema décrit un seul catalog ; system.information_schema traverse les catalogs du metastore, hors hive_metastore. Les deux ne retournent que les objets visibles selon les privilèges Unity Catalog.
  • Le schema enforcement Delta bloque une écriture non conforme, sauf conversion sûre ou évolution explicitement autorisée.
  • spark.databricks.delta.schema.autoMerge.enabled=true autorise globalement l'évolution compatible dans la SparkSession ; ce n'est ni une propriété de table ni une option Auto Loader. Préférer mergeSchema ou WITH SCHEMA EVOLUTION sur l'opération concernée.
  • Pour convertir une table externe, SET MANAGED gère les écritures concurrentes et minimise le downtime ; un deep clone crée une seconde table à basculer séparément.
  • Databricks SQL fournit le data warehouse ; un SQL warehouse est le compute SQL qui l'alimente.
  • L'application web du workspace est dans le control plane ; les traitements s'exécutent dans un compute plane.
  • « Toutes les données sont dans le compte cloud client » n'est pas universel : distinguer architecture classic, serverless, control plane et stockage des tables.
  • La redaction d'un secret récupéré avec dbutils.secrets.get() protège certaines sorties accidentelles ; elle ne remplace ni les ACL du scope ni le moindre privilège.

10.2 Ingestion et évolution de schéma

  • COPY INTO ignore un fichier déjà suivi même si son contenu a été modifié ; force=true le recharge. CTAS ne mémorise pas les fichiers traités.
  • COPY INTO ... VALIDATE ALL ou VALIDATE n ROWS contrôle le chargement sans écrire. Depuis une external location UC, retenir READ FILES sur la source et MODIFY sur la cible, plus les privilèges de parcours ; depuis un volume, utiliser READ VOLUME.
  • Auto Loader utilise format("cloudFiles"), puis cloudFiles.format pour le format réel.
  • schema_of_json aide à découvrir un schéma ; il ne remplace pas un contrat de schéma de production relu et versionné.
  • addNewColumns fait d'abord échouer le stream après avoir élargi le schéma ; les anciennes lignes valent NULL pour la nouvelle colonne.
  • failOnNewColumns échoue sans actualiser le schéma : un retry seul ne résout pas le problème.
  • addNewColumnsWithTypeWidening élargit aussi certains types ; none ignore les nouvelles colonnes, tandis que rescue les conserve dans rescued data.
  • COPY_OPTIONS ('force' = 'true') désactive l'idempotence de COPY INTO et peut créer des doublons.
  • Pour JSON, CSV et XML, Auto Loader infère par défaut des STRING ; cloudFiles.inferColumnTypes=true active l'inférence des types sur échantillon.
  • USING CSV ... LOCATION enregistre une table externe CSV ; il ne convertit pas les fichiers en Delta, et LOCATION s'écrit sans =.
  • cloudFiles.includeExistingFiles=true inclut le backlog initial et n'est évalué qu'à la création du stream/checkpoint.
  • Avec AvailableNow, cloudFiles.maxFilesPerTrigger borne strictement le nombre de fichiers par micro-batch et cloudFiles.maxBytesPerTrigger borne souplement les octets ; le backlog initial est drainé en plusieurs batches, puis le stream s'arrête.
  • Effacer schemaLocation force une réinférence ; effacer le checkpoint recrée la requête et peut retraiter les fichiers. Inverser les deux conduit à un mauvais diagnostic des doublons.
  • Les schema hints peuvent guider des champs imbriqués ; ils ne remplacent ni le checkpoint ni un contrat de données validé. Un champ attendu mais absent d'un document vaut NULL.
  • mergeSchema fait évoluer la cible Delta ; il ne corrige pas un type JSON incompatible lu par Auto Loader. Utiliser schéma explicite, schemaHints, rescued data ou transformation Silver.
  • Auto Loader et Delta possèdent deux schémas indépendants : schemaEvolutionMode peut accepter une colonne à la lecture alors que l'écriture dans une cible existante échoue sans mergeSchema, ALTER TABLE ou autre évolution Delta. mergeSchema n'est donc nécessaire que si la cible doit réellement évoluer, pas pour toute écriture.
  • Directory listing reliste le stockage ; file events/notifications vise l'échelle et la faible latence. Sur S3, le flux classique utilise SNS puis une file SQS.
  • Une notification SQS peut être retentée ; le checkpoint Auto Loader déduplique normalement l'identité du fichier. Vérifier préfixes S3, SNS/SQS, IAM et checkpoint avant d'accuser le retry.
  • _rescued_data capture surtout des champs/type inattendus ; _corrupt_record est préférable à badRecordsPath pour inspecter les documents réellement mal formés.
  • Auto Loader vers ADLS nécessite des droits cloud adaptés ; Storage Blob Data Contributor est un rôle de stockage courant, pas une option cloudFiles.
  • Dans COPY INTO, modifiedAfter filtre la date de modification des fichiers via FORMAT_OPTIONS, pas une colonne des enregistrements.
  • Le JDBC Spark standard est batch ; une streaming table ne le rend pas continu. Le connecteur PostgreSQL managé peut toutefois ingérer le CDC.
  • Un connecteur Lakeflow managé effectue généralement un snapshot initial, puis une capture incrémentale lorsque la source le permet ; CDC, curseur, pagination, rate limits et évolution de schéma restent spécifiques au connecteur.
  • Sur un JSON STRING, payload:field et get_json_object extraient du texte en reparsant la chaîne à la lecture ; persister un VARIANT évite ce parsing répété pour un document flexible réutilisé.
  • binaryFile produit une ligne par fichier ; pathGlobFilter limite les noms retenus, par exemple aux fichiers *.jpg.
  • Auto Loader est une source Spark Structured Streaming nommée cloudFiles, pas un mécanisme batch indépendant.
  • Un CTAS dérive son schéma du SELECT et ne permet pas de lui adjoindre une spécification manuelle de colonnes.

10.3 Transformations, jointures et qualité

  • EXPECT seul conserve la ligne ; DROP ROW la retire ; FAIL UPDATE fait échouer l'update.
  • expect_all_or_drop supprime toute ligne violant au moins une règle du dictionnaire ; expect_all_or_fail arrête atomiquement l'update.
  • Un format ISO et une liste autorisée se contrôlent avec des prédicats SQL ; une quarantaine est un branchement vers une table d'invalides.
  • dropDuplicates ne garantit pas de garder la ligne la plus récente ; utiliser une window ordonnée.
  • distinct() retire seulement les doublons exacts sur toutes les colonnes ; pour choisir la dernière version d'une clé, il faut toujours une window ordonnée.
  • df.union() conserve les doublons et associe les colonnes par position.
  • UNION SQL déduplique ; UNION ALL conserve les doublons.
  • WITH SCHEMA EVOLUTION fait évoluer la cible d'un MERGE, pas le schéma lu par Auto Loader.
  • mergeSchema ajoute des colonnes compatibles ; overwriteSchema remplace le schéma avec un overwrite.
  • lower(trim(email)) retire les espaces aux extrémités puis met l'adresse en minuscules ; les NULL restent NULL.
  • selectExpr exprime une projection avec des chaînes SQL, par exemple CAST(reading_value AS DOUBLE) AS sensor_value ; utiliser TRY_CAST si les valeurs mal formées doivent devenir NULL plutôt que faire échouer la requête.
  • Un MERGE exige une clé métier stable et une source dédupliquée ; le Z-order peut réduire les fichiers cible lus, mais ne corrige pas une condition ON non unique.
  • COUNT(*) compte les lignes ; COUNT(col) ignore les NULL de col.
  • collect_list conserve les doublons ; collect_set les retire ; aucun ne garantit l'ordre après shuffle.
  • explode augmente le nombre de lignes ; flatten retire un niveau d'imbrication d'un tableau.
  • TRY_CAST retourne NULL pour une valeur mal formée ; CAST lève normalement une erreur.
  • <=> considère deux NULL égaux ; = retourne NULL dans ce cas.
  • Un cross join produit le produit cartésien et peut faire exploser le nombre de lignes.
  • CAST('100$' AS INT) échoue ; TRY_CAST('100$' AS INT) retourne NULL.
  • Une expectation SQL qui doit échouer s'écrit ON VIOLATION FAIL UPDATE, pas seulement FAIL.
  • Un instant doit être typé TIMESTAMP et normalisé en UTC, pas conservé comme chaîne locale ambiguë.
  • expect_all reçoit un dictionnaire nom-vers-prédicat ; lire une table streaming s'écrit spark.readStream.table(...).
  • FILTER(array, x -> prédicat) filtre les éléments du tableau sans filtrer la ligne ni l'exploser.
  • Pour un CDC en pipeline, AUTO CDC INTO est le nom actuel de l'ancien APPLY CHANGES INTO. KEYS identifie les lignes, SEQUENCE BY ordonne les versions et le SCD type 1 conserve l'état courant ; les deletes exigent une condition explicite.
  • Une colonne GENERATED ALWAYS AS (expression) est calculée par Delta. Si une écriture fournit elle-même la colonne, la valeur doit respecter l'expression.
  • count_if(condition) compte uniquement les lignes pour lesquelles le prédicat vaut TRUE ; les résultats FALSE et NULL ne sont pas comptés.
  • DataFrameWriter.mode("ignore") ignore silencieusement l'écriture entière si la destination existe ; il ne déduplique pas les lignes et ne répare pas une cible partielle.
  • summary("count", "mean", "90%") sélectionne des statistiques descriptives, dont des percentiles approximatifs ; ce n'est pas une agrégation métier à schéma stable.

10.4 Structured Streaming et pipelines déclaratifs

  • Un checkpoint ne doit pas être partagé entre plusieurs streams indépendants.
  • Un checkpoint persistant permet la reprise, mais l'exactly-once de bout en bout exige aussi une source rejouable et un sink transactionnel/idempotent.
  • Trigger.Once est déprécié et traite le backlog en un micro-batch ; AvailableNow est préféré et peut utiliser plusieurs micro-batches avant de s'arrêter.
  • Auto Loader ne garantit pas l'ordre d'arrivée des fichiers.
  • Une streaming table ne garantit pas un ordre global ; Kafka n'ordonne qu'au sein d'une partition. Utiliser temps d'événement ou séquence métier.
  • Un changement de la table statique d'un stream-static join ne recalcule pas les anciennes sorties sans full refresh.
  • Une materialized view tente un traitement incrémental ; joindre deux streaming tables via une MV ne signifie pas full refresh systématique.
  • Une materialized view autonome peut porter SCHEDULE [REFRESH] EVERY ... ou SCHEDULE [REFRESH] CRON ... dans son DDL ; le cron Quartz possède six champs. Une MV appartenant à un pipeline explicite suit plutôt la planification de ce pipeline.
  • Un stream-stream join est stateful et doit borner son état avec watermarks et conditions temporelles.
  • CREATE TEMPORARY VIEW dans un pipeline reste privé au pipeline ; CREATE VIEW publie la définition dans Unity Catalog.
  • schemaLocation conserve le schéma Auto Loader ; checkpointLocation conserve la progression, les commits et l'état de la requête.
  • Un watermark borne l'état selon le temps d'événement ; ce n'est pas le trigger du stream.
  • foreachBatch est at-least-once : le callback doit produire une écriture idempotente.
  • Le CDF expose les changements Delta ; le checkpoint du lecteur streaming suit les versions déjà consommées.
  • RESTORE crée un nouveau commit visible par le CDF ; il ne rembobine pas le curseur et ne recrée pas un historique CDF expiré.
  • Avec Kafka, startingOffsets=latest s'applique au premier démarrage ; le checkpoint prime ensuite.
  • Après SET MANAGED, redémarrer le stream avec son checkpoint ; la conversion permet aussi redirection compatible par chemin et rollback temporaire.
  • Une update ordinaire de streaming table reprend au checkpoint ; son full refresh tronque la cible et rejoue toute la source encore disponible. Une materialized view est, elle, recalculée depuis ses sources.
  • Renommer une colonne amont impose de modifier les définitions downstream ; un full refresh peut être requis si leur état ou leurs anciennes lignes doivent être reconstruits.
  • Un pipeline triggered se planifie avec une Pipeline task Lakeflow Jobs. Un full refresh est une opération explicite sur une streaming table ou une materialized view ; il n'a pas lieu à chaque run.
  • Pour un paramètre dynamique consommé en SQL par une Pipeline task, utiliser Parameters et :clé ; un job parameter de même nom est transmis automatiquement. Utiliser IDENTIFIER(:target_schema) dans une position d'identifiant. Configuration et spark.conf.get restent nécessaires pour le code Python du pipeline.

10.5 Lakeflow Jobs, Git et Bundles

  • Repair run ne garantit pas l'idempotence d'une tâche partiellement écrite.
  • Une tâche All done est adaptée au nettoyage après succès ou échec.
  • Un data-driven trigger évite les runs à vide ; un schedule répond à une contrainte horaire.
  • Un trigger Table update détecte aussi les commits de DELETE, mais son démarrage dépend de la détection et des délais configurés. Les versions actuelles permettent plusieurs triggers par Job : « toujours créer deux Jobs » est une ancienne contrainte, pas une règle générale.
  • Une target de bundle surcharge les valeurs globales pour un environnement.
  • Les bundles exigent la CLI moderne : 0.205+ est le socle d'installation actuel et bundle schema demande au moins 0.218.0; les fonctions récentes peuvent imposer davantage. Utiliser bundle.databricks_cli_version au lieu de mémoriser « 0.200+ » comme seuil universel.
  • mode: development préfixe notamment les ressources et met les schedules/triggers en pause ; ce mode ne remplace pas l'isolation des données.
  • Dans artifacts, le type d'un wheel est whl, pas wheel ; l'artefact est construit et téléversé pendant bundle deploy.
  • validate vérifie le bundle ; deploy crée/met à jour ; run exécute.
  • Une task value se référence par {{tasks.<task>.values.<clé>}}, avec values au pluriel.
  • Les doubles accolades d'une référence dynamique ne calculent pas une expression ; l'opérateur d'un If/else est configuré séparément.
  • Une alerte Job signale l'issue globale ; une alerte task peut se déclencher à chaque échec, sauf si elle est masquée jusqu'à la dernière retry.
  • Pour GitHub hébergé, la GitHub App/OAuth est recommandée ; un PAT est une solution de repli à limiter et faire expirer.
  • Deux tasks sans dépendance s'exécutent en parallèle lorsque leurs dépendances éventuelles sont satisfaites et que les ressources le permettent.
  • Une variable --var est résolue dans la configuration du bundle ; un paramètre de run se transmet avec --params.
  • --auto-approve retire les confirmations interactives, pas la validation du bundle.
  • --fail-on-active-runs fait échouer le déploiement ; il n'arrête pas les runs en cours.
  • Les permissions DAB contrôlent l'accès au Job ; run_as définit son identité d'exécution et les grants UC contrôlent ses données.
  • Un run_as.service_principal_name placé dans la target prod fixe l'identité runtime, qui peut être différente de l'identité de déploiement.
  • Pull met à jour un Git folder depuis sa branche distante ; push envoie les changements locaux vers le dépôt.
  • Le sparse checkout synchronise seulement certains sous-répertoires d'un grand dépôt ; ses motifs cone n'acceptent pas les exclusions ! et l'option ne se désactive pas sur le Git folder existant.
  • bundle generate job --existing-job-id ... --bind génère la configuration et lie le bundle au Job existant ; sans binding, deploy risque de créer un nouveau Job.
  • Pour Unified Authentication avec un PAT, les variables attendues sont DATABRICKS_HOST et DATABRICKS_TOKEN ; OAuth utilise d'autres credentials.
  • --profile sélectionne host/credentials dans .databrickscfg ; --target sélectionne la configuration du bundle. Ce ne sont pas deux noms pour le même réglage.
  • databricks pipelines start-update <id> --full-refresh reconstruit le pipeline ; ce n'est pas le mode normal d'une update incrémentale.
  • databricks bundle schema produit le JSON Schema utilisable par l'IDE ; bundle validate reste nécessaire pour valider le projet et sa target.
  • %pip crée un environnement limité au notebook et non persistant entre les sessions ; les autres notebooks du compute ne voient pas ses packages. Une mise à jour peut nécessiter dbutils.library.restartPython().
  • Limite de concurrence atteinte avec queueing : le run attend puis démarre en retard ; sans queueing, il est skipped.
  • Pour analyser l'évolution d'un Job, ouvrir Jobs & Pipelines > Job > Runs. L'UI conserve 60 jours d'historique ; ouvrir le run et ses tâches pour confirmer la cause d'une dérive.

10.6 Performance, compute et maintenance

  • Un broadcast join diffuse la petite table, pas la grosse.
  • Photon peut consommer des DBU à un taux différent sur compute classic, mais il est inclus sur serverless/SQL. Comparer coût horaire et durée totale : un taux supérieur peut être compensé par une exécution nettement plus courte.
  • Un workload exclusivement en R requiert un compute Dedicated ; Standard ne prend pas en charge R. Dedicated convient également aux accès système privilégiés, RDD et GPU, mais Standard reste préférable pour les workloads compatibles et partagés.
  • collect() charge les résultats sur le driver.
  • Un gros max de durée par rapport au percentile 75 signale souvent du skew.
  • Le spill est un transfert mémoire vers disque, souvent pendant un shuffle.
  • Scheduler Delay long et Executor Computing Time court indique souvent trop de petites tasks ou une attente d'executors. La locality ANY signale l'absence de placement plus local, mais peut être normale avec du stockage objet.
  • Predictive optimization automatise la maintenance des tables managées Unity Catalog, pas externes.
  • Liquid clustering remplace partitionnement/ZORDER sur la même table ; ne pas les combiner.
  • Une erreur de démarrage de cluster se cherche d'abord dans les compute event logs, pas dans le Spark UI.
  • cache() est lazy et n'est utile que si le DataFrame est réutilisé ; appeler ensuite unpersist().
  • VACUUM supprime d'anciens fichiers physiques et peut réduire le time travel ; il n'optimise pas directement un plan de requête.
  • ANALYZE collecte des statistiques pour l'optimiseur ; OPTIMIZE réorganise les fichiers.
  • coalesce(n) réduit les partitions sans shuffle complet ; repartition(n, cols...) redistribue par shuffle.
  • OPTIMIZE réécrit/compacte ; ZORDER BY est une option de placement pour améliorer le data skipping.
  • Optimized writes réduit les petits fichiers pendant l'écriture ; auto compaction agit après le commit ; OPTIMIZE compacte les fichiers déjà présents.
  • Pour compacter seulement, exécuter OPTIMIZE sans Z-order. Pour un point lookup ou un filtre temporel sélectif, Z-order la colonne réellement filtrée si la table n'utilise pas liquid clustering.
  • Z-order sur beaucoup de colonnes dilue la localité et augmente le coût de la réécriture ; sélectionner peu de colonnes de filtre réellement utiles.
  • delta.deletedFileRetentionDuration contrôle l'éligibilité des fichiers supprimés à VACUUM; l'historique du journal suit delta.logRetentionDuration, et le time travel dépend des deux.
  • Un DELETE masque immédiatement les lignes du snapshot courant ; REORG ... APPLY (PURGE) puis VACUUM sont nécessaires pour purger physiquement certaines données avec deletion vectors. RETAIN 0 HOURS est destructif et normalement bloqué par la sécurité.
  • Des différences de time travel entre clusters après VACUUM peuvent venir du cache disque ; elles ne garantissent pas que les fichiers existent encore dans le stockage.
  • Des MERGE fréquents et peu sélectifs gonflent les actions du Delta log et les tombstones. VACUUM nettoie les fichiers de données, pas directement _delta_log.
  • Auto Stop arrête un SQL warehouse inactif ; l'autoscaling change le nombre de clusters pendant son fonctionnement.
  • AQE peut convertir automatiquement un sort-merge join en broadcast join, mais seulement si le type et la taille du join le permettent.
  • CLUSTER BY AUTO utilise Predictive Optimization et l'historique des requêtes ; des clés manuelles se choisissent selon les filtres fréquents.
  • Pour un ExecutorLostFailure lié à de grosses partitions de shuffle, augmenter mémoire et partitions peut aider, mais il faut confirmer la cause dans Spark UI.
  • L'autoscaling du stockage local attache du disque aux workers menacés par un gros shuffle/spill ; il n'ajoute ni executors, ni RAM, ni CPU et ne corrige pas le skew.
  • ENABLE/DISABLE PREDICTIVE OPTIMIZATION créent un override ; INHERIT reprend la valeur du parent. Vérifier avec DESCRIBE ... EXTENDED.
  • Les instances Spot conviennent surtout aux batchs non critiques, idempotents et relançables ; elles peuvent être reprises par le cloud provider.
  • Un instance pool conserve des instances classic prêtes et réduit le démarrage/autoscaling ; les instances idle ne consomment pas de DBU mais restent facturées par le fournisseur cloud.
  • Dans l'UI actuelle, Catalog Explorer > objet > Permissions gère les grants ; Data Explorer est l'ancien nom rencontré dans certains supports.
  • EXECUTE autorise l'appel d'une fonction/modèle/service. ALL PRIVILEGES reste dépendant du type d'objet et exclut MANAGE, EXTERNAL USE SCHEMA et EXTERNAL USE LOCATION.

11. Checklist finale, corrigé et labs

Noter chaque point : 0 = non maîtrisé, 1 = réponse comprise mais syntaxe hésitante, 2 = réponse et syntaxe restituées sans aide. Revoir en priorité les points à 0, puis pratiquer ceux à 1.

11.1 Checklist par domaine

Être capable de répondre ou de produire un exemple court sans copier-coller :

Delta Lake et ingestion — points 1 à 8

  • 1. créer une table Delta managée et une table externe ;
  • 2. expliquer l'effet de DROP TABLE sur chacune et la fenêtre UNDROP d'une table managée ;
  • 3. lire l'historique, faire un time travel et restaurer une table ;
  • 4. charger avec CTAS, COPY INTO et Auto Loader, puis valider un COPY INTO sans écrire et identifier ses privilèges UC ;
  • 5. configurer schema location, checkpoint et rescued data ;
  • 6. expliquer sink et checkpoint, ainsi que leur rôle distinct ;
  • 7. distinguer le schéma lu par Auto Loader du schéma écrit dans Delta, puis choisir mergeSchema, overwriteSchema ou le réglage de session ;
  • 8. inférer un exemple avec schema_of_json, puis utiliser un schéma explicite avec from_json ;

Transformations et Delta DML — points 9 à 18

  • 9. nettoyer et typer avec SQL/PySpark, notamment lower/trim, CAST/TRY_CAST et selectExpr ;
  • 10. choisir coalesce ou repartition, et expliquer le shuffle associé ;
  • 11. distinguer explode/flatten, collect_list/collect_set et COUNT(*)/COUNT(col) ;
  • 12. traduire une transformation, un join, une agrégation et un MERGE entre SQL et PySpark, puis justifier le langage choisi ;
  • 13. choisir et écrire inner, left, broadcast, multi-key et cross join ;
  • 14. expliquer union, unionByName, UNION et UNION ALL ;
  • 15. distinguer distinct() des doublons par clé et dédupliquer en gardant la version la plus récente ;
  • 16. agréger avec count, count_if, count_distinct, approx_count_distinct, sum et mean, puis sélectionner des statistiques avec summary() ;
  • 17. écrire un MERGE avec insert, update et delete ;
  • 18. expliquer l'effet de MERGE WITH SCHEMA EVOLUTION ;

Objets Gold, pipelines et streaming — points 19 à 26

  • 19. choisir table, vue, vue matérialisée ou streaming table pour une Gold et planifier une MV autonome ;
  • 20. distinguer CREATE TEMPORARY VIEW et CREATE VIEW dans un pipeline ;
  • 21. choisir entre stream-static, materialized view et stream-stream join ;
  • 22. expliquer watermarks, état, output modes et contenu d'un checkpoint streaming ;
  • 23. écrire un foreachBatch idempotent et lire un CDF en Structured Streaming ;
  • 24. expliquer les trois politiques d'expectation ;
  • 25. organiser et valider un pipeline multi-fichiers ;
  • 26. transmettre target_schema d'un Job à une Pipeline task et distinguer Parameters SQL de Configuration Python ;

Jobs, Git et déploiement — points 27 à 37

  • 27. dessiner un DAG avec dépendances, if/else, for-each et cleanup ;
  • 28. construire un If/else avec {{tasks.<task>.values.<clé>}} ;
  • 29. choisir scheduled, file-arrival ou table-update trigger, expliquer la détection d'un DELETE et les triggers multiples ;
  • 30. expliquer retry, rerun, repair et idempotence ;
  • 31. analyser les 60 jours de runs dans l'UI, puis choisir des notifications Job/task et les combiner avec les retries ;
  • 32. faire branch/commit/push/PR avec un Git folder et utiliser le sparse checkout pour un grand dépôt ;
  • 33. expliquer GitHub App/OAuth, PAT Git et service principal ;
  • 34. gérer une bibliothèque limitée au notebook avec %pip, puis exécuter des tests PySpark avec pytest ;
  • 35. lire un databricks.yml avec variables, targets, contrainte de version CLI, mode: development et artefact whl ;
  • 36. installer la CLI moderne, générer bundle schema, distinguer --profile de --target, déployer et lancer un full refresh ;
  • 37. choisir entre REST API, CLI, SDK et bundle pour automatiser un déploiement ;

Performance, maintenance et gouvernance — points 38 à 48

  • 38. localiser skew, shuffle, spill, scheduler delay et locality level dans le Spark UI ;
  • 39. expliquer le risque de collect() et utiliser cache() puis unpersist() à bon escient ;
  • 40. distinguer driver OOM, executor OOM et cluster startup failure, puis expliquer l'autoscaling du stockage local ;
  • 41. distinguer DDL/DML, CDC/SCD et SCD type 1/type 2, puis écrire un AUTO CDC INTO avec KEYS et SEQUENCE BY ;
  • 42. expliquer liquid clustering, predictive optimization, OPTIMIZE, VACUUM et ANALYZE ;
  • 43. distinguer partition pruning, data skipping, OPTIMIZE et ZORDER ;
  • 44. expliquer rétention VACUUM et time travel, puis distinguer CTAS, deep clone et shallow clone ;
  • 45. définir une contrainte CHECK et une colonne générée, puis prévoir l'échec d'une valeur non conforme ;
  • 46. relier identité cloud, storage credential, external location, volume et table externe, puis distinguer Connect de Federation/CONNECTION ;
  • 47. gérer les grants dans Catalog Explorer et accorder USE CATALOG, USE SCHEMA, SELECT, MODIFY, EXECUTE ou ALL PRIVILEGES selon l'objet ;
  • 48. choisir entre row filter multi-colonnes, column mask, dynamic view et ABAC.

SQL, ingestion, compute et Bundles — points 49 à 56

  • 49. configurer Auto Stop sur un SQL warehouse et le distinguer de l'autoscaling ;
  • 50. comparer les modes Auto Loader addNewColumns, addNewColumnsWithTypeWidening, failOnNewColumns, rescue et none ;
  • 51. utiliser TRY_CAST pour isoler une valeur non convertible ;
  • 52. écrire une égalité null-safe avec <=> et un produit cartésien avec crossJoin ;
  • 53. créer et appeler une fonction SQL UDF simple ;
  • 54. expliquer quand AQE peut sélectionner automatiquement un broadcast join ;
  • 55. utiliser bundle init, distinguer --var/--params et déployer avec --auto-approve ;
  • 56. expliquer le risque de COPY_OPTIONS ('force' = 'true').

Schémas, déploiement et objets workspace — points 57 à 64

  • 57. expliquer l'effet de cloudFiles.inferColumnTypes=true et le comportement par défaut ;
  • 58. ajouter une contrainte NOT NULL à une colonne existante ;
  • 59. déployer avec --fail-on-active-runs et expliquer son effet exact ;
  • 60. prévoir le résultat de CAST('100$' AS INT) et de TRY_CAST ;
  • 61. enregistrer des CSV comme table externe avec la syntaxe USING CSV ... LOCATION ;
  • 62. situer le source d'un notebook entre control plane et compute plane ;
  • 63. définir les permissions d'un Job dans un bundle et les distinguer d'un run_as service principal par target ;
  • 64. changer le propriétaire d'une table dans Catalog Explorer ou en SQL.

Git, Delta et gouvernance avancée — points 65 à 70

  • 65. mettre à jour un Git folder depuis son dépôt distant avec Pull ;
  • 66. expliquer pourquoi Delta dépend du cloud object storage et comment il impose le schéma à l'écriture ;
  • 67. importer et binder un Job existant dans un bundle, puis expliquer l'effet de bundle deploy ;
  • 68. écrire une expectation qui fait échouer atomiquement l'update ;
  • 69. créer un governed tag, tagguer et vérifier une colonne, puis écrire la policy ABAC de masquage correspondante ;
  • 70. justifier SET MANAGED plutôt qu'un deep clone pour convertir une table externe.

Plateforme, sources et traitements avancés — points 71 à 83

  • 71. identifier Databricks SQL comme service de data warehousing et situer l'application web dans le control plane ;
  • 72. typer les instants en TIMESTAMP, les normaliser en UTC et distinguer TIMESTAMP_NTZ ;
  • 73. réutiliser un dictionnaire de règles avec @dp.expect_all ;
  • 74. configurer cloudFiles.includeExistingFiles et ingérer des images avec binaryFile/pathGlobFilter ;
  • 75. filtrer les fichiers de COPY INTO avec modifiedAfter ;
  • 76. distinguer JDBC batch, snapshot puis incrémental d'un connecteur managé, et requête fédérée sans ingestion ;
  • 77. filtrer les éléments d'un tableau avec une lambda SQL/PySpark ;
  • 78. lire Kafka avec Structured Streaming et expliquer startingOffsets face au checkpoint ;
  • 79. choisir entre :/get_json_object sur un JSON STRING, : sur un VARIANT et from_json vers un STRUCT ;
  • 80. choisir des clés manuelles de liquid clustering ou utiliser CLUSTER BY AUTO ;
  • 81. diagnostiquer un ExecutorLostFailure pendant un shuffle et dimensionner mémoire/partitions ;
  • 82. redémarrer correctement un stream après SET MANAGED et expliquer redirection/rollback ;
  • 83. expliquer DESCRIBE DATABASE/DESCRIBE SCHEMA et distinguer <catalog>.information_schema de system.information_schema.

Architecture et configuration de production — points 84 à 90

  • 84. activer, désactiver ou hériter de Predictive Optimization à chaque niveau, puis vérifier le statut avec DESCRIBE ... EXTENDED ;
  • 85. configurer Unified Authentication par PAT, sélectionner un profil CLI et récupérer un secret sans l'exposer ;
  • 86. expliquer que cloudFiles est une source Structured Streaming et choisir Structured Streaming pour un Kafka à fort débit/faible latence ;
  • 87. distinguer un run queued d'un run skipped lorsque max_concurrent_runs est atteint ;
  • 88. situer stockage, source du notebook et exécution du Job dans les architectures classic et serverless ;
  • 89. choisir Standard ou Dedicated, expliquer le coût Photon, réserver Spot aux workloads tolérant les interruptions et expliquer un instance pool ;
  • 90. expliquer pourquoi un CTAS dérive son schéma du SELECT au lieu d'accepter une déclaration manuelle de colonnes.

Fiabilité, refresh et partage — points 91 à 108

  • 91. expliquer snapshot isolation/MVCC et l'effet d'un overwrite sur l'historique Delta ;
  • 92. prévoir séparément les effets de la suppression de schemaLocation et du checkpoint ;
  • 93. combiner évolution Auto Loader, schema hints, Parquet et capture des enregistrements corrompus ;
  • 94. choisir directory listing ou file events et relier S3/SNS/SQS ou les droits ADLS au mode notification ;
  • 95. distinguer Trigger.Once et AvailableNow, régler la taille des micro-batches Auto Loader et énoncer les conditions d'un exactly-once ;
  • 96. dédupliquer un flux Kafka avec une clé métier et un MERGE dans foreachBatch ;
  • 97. distinguer update/full refresh d'une streaming table et d'une materialized view, puis gérer un renommage amont et la planification ;
  • 98. distinguer optimized writes, auto compaction, OPTIMIZE et ZORDER, puis choisir une colonne de layout selon les filtres ;
  • 99. expliquer appendOnly, deletedFileRetentionDuration, logRetentionDuration et leur effet sur VACUUM/Time Travel ;
  • 100. choisir entre grants inter-workspace dans un même metastore et Delta Sharing vers un autre recipient ;
  • 101. concevoir une ingestion JDBC incrémentale avec curseur composite et écriture idempotente ;
  • 102. expliquer le suivi COPY INTO, les retries SQS et le diagnostic de fichiers S3 manquants ;
  • 103. traiter des types JSON imbriqués avec schemaHints, les champs absents et la portée exacte de mergeSchema ;
  • 104. fiabiliser et optimiser un MERGE : clé unique, source dédupliquée, append-only, pruning et petits fichiers ;
  • 105. valider un format ISO-8601 et une liste autorisée, puis construire une quarantaine par branchement ;
  • 106. prévoir l'effet de RESTORE sur le CDF et choisir Time Travel pour une reproductibilité ML ;
  • 107. gérer l'ordre métier d'un stream et prévoir un full snapshot JDBC lorsqu'aucun curseur fiable n'existe ;
  • 108. diagnostiquer petits fichiers, tombstones et Delta log, puis expliquer une purge réglementaire et le danger de VACUUM RETAIN 0 HOURS.

11.2 Réponses synthétiques

Les numéros ci-dessous correspondent exactement aux 108 items précédents. L'objectif est de pouvoir restituer chaque réponse en quelques phrases, puis d'écrire la syntaxe essentielle sans aide.

Delta Lake et ingestion — réponses 1 à 8

  1. Table managée ou externe — Une table managée ne possède pas de LOCATION explicite : Unity Catalog choisit et gère son stockage. Une table externe pointe vers un chemin couvert par une external location.

    SQL
    CREATE TABLE dev.sales.orders (id BIGINT) USING DELTA;
    
    CREATE TABLE dev.sales.orders_ext (id BIGINT) USING DELTA
    LOCATION 's3://company-data/sales/orders';
    
  2. Effet de DROP TABLE — Pour une table managée UC, la récupération avec UNDROP TABLE est possible pendant 7 jours par défaut ; après expiration, les fichiers sont supprimés de façon asynchrone, généralement sous 48 heures. Pour une table externe, DROP supprime seulement l'enregistrement de table et conserve les fichiers cloud.

  3. Historique, time travel et restauration — DESCRIBE HISTORY liste les commits ; VERSION AS OF/TIMESTAMP AS OF lit un ancien snapshot ; RESTORE crée une nouvelle version reproduisant l'ancien état.

    SQL
    DESCRIBE HISTORY dev.sales.orders;
    SELECT * FROM dev.sales.orders VERSION AS OF 12;
    RESTORE TABLE dev.sales.orders TO VERSION AS OF 12;
    
  4. CTAS, COPY INTO ou Auto Loader — CTAS convient à une création ponctuelle depuis un SELECT. COPY INTO charge les nouveaux fichiers et mémorise ceux déjà traités ; VALIDATE ALL/VALIDATE n ROWS vérifie sans écrire. Avec une external location UC, il faut notamment READ FILES sur la source et MODIFY sur la cible ; un volume emploie READ VOLUME. Auto Loader est le choix scalable pour une arrivée continue ou massive avec checkpoint et évolution de schéma.

  5. Schéma, checkpoint et rescued data — cloudFiles.schemaLocation conserve le schéma inféré/évolué ; checkpointLocation conserve offsets, commits, identité et état de la requête ; _rescued_data conserve les champs inattendus ou incompatibles au lieu de les perdre.

  6. Sink ou checkpoint — Le sink est la destination métier, par exemple une table Delta. Le checkpoint est l'état technique de reprise du stream ; ce n'est ni la donnée de sortie ni une sauvegarde du sink.

  7. Évolution de lecture et d'écriture — cloudFiles.schemaEvolutionMode fait évoluer le schéma lu et stocké dans schemaLocation; il ne modifie pas à lui seul une table Delta existante. Pour l'écriture, mergeSchema=true ajoute localement des colonnes compatibles, overwriteSchema=true remplace schéma et données avec un overwrite, et spark.databricks.delta.schema.autoMerge.enabled=true élargit toutes les écritures compatibles de la session. Un flux Auto Loader autonome qui accepte de nouvelles colonnes doit donc faire évoluer aussi sa cible ; mergeSchema n'est pas requis si les schémas sont déjà compatibles ou si le pipeline gère la streaming table.

  8. schema_of_json et from_json — schema_of_json aide à découvrir le DDL d'un exemple ; en production, relire et versionner ce schéma, puis le fournir explicitement à from_json.

    SQL
    SELECT schema_of_json('{"id":1,"country":"FR"}');
    SELECT from_json(payload, 'id BIGINT, country STRING') AS event
    FROM dev.bronze.events;
    
    PYTHON
    schema = "id BIGINT, country STRING"
    parsed_df = events_df.withColumn("event", F.from_json("payload", schema))
    

Transformations et Delta DML — réponses 9 à 18

  1. Nettoyage et typage — lower(trim(email)) standardise un e-mail sans modifier les NULL. En PySpark, écrire F.lower(F.trim(F.col("email"))). selectExpr("CAST(reading_value AS DOUBLE) AS sensor_value") permet une projection avec syntaxe SQL ; préférer TRY_CAST si une valeur invalide doit devenir NULL. Ces transformations sont lazy et retournent un nouveau DataFrame.

  2. coalesce ou repartition — coalesce(n) réduit les partitions sans shuffle complet, rapidement mais parfois avec déséquilibre. repartition(n, cols...) déclenche un shuffle pour augmenter, réduire ou rééquilibrer les partitions.

  3. Fonctions proches — explode crée plusieurs lignes, flatten retire un niveau d'imbrication d'un tableau ; collect_list conserve les doublons, collect_set les retire ; COUNT(*) compte les lignes, COUNT(col) seulement les valeurs non NULL.

  4. SQL ou PySpark — Les deux produisent des plans Spark. Choisir SQL pour une transformation déclarative, une agrégation, une vue ou un MERGE lisible ; choisir PySpark pour des fonctions réutilisables, des tests, une logique programmatique ou une source déjà représentée par un DataFrame.

  5. Choix du join — inner conserve les correspondances ; left conserve toute la gauche ; un broadcast copie la petite table sur les executors ; un multi-key join combine plusieurs égalités ; cross produit le produit cartésien et doit être volontaire.

SQL
SELECT /*+ BROADCAST(c) */ o.*, c.segment
FROM orders o LEFT JOIN customers c
  ON o.customer_id = c.customer_id AND o.country = c.country;
PYTHON
result = orders.join(
    F.broadcast(customers),
    (orders.customer_id == customers.customer_id)
    & (orders.country == customers.country),
    "left",
)
  1. Unions — df.union(df2) associe par position et conserve les doublons, comme UNION ALL. unionByName associe par nom et peut accepter des colonnes manquantes. UNION SQL déduplique et coûte donc généralement un shuffle supplémentaire.

  2. Doublons exacts ou dernière version — SELECT DISTINCT */df.distinct() retire les lignes identiques sur toutes les colonnes. Pour une clé métier, utiliser row_number() dans une fenêtre partitionnée par la clé et ordonnée par timestamp décroissant, puis garder rn = 1 ; dropDuplicates ne choisit pas déterministement la plus récente.

  3. Agrégations et résumé — count compte, count_if(prédicat) compte les résultats TRUE, count_distinct donne un cardinal exact, approx_count_distinct un cardinal approximatif moins coûteux, sum additionne et mean/avg calcule la moyenne. summary("count", "mean", "90%") calcule des statistiques exploratoires choisies, avec percentiles approximatifs ; ce n'est pas un groupBy métier.

  4. MERGE complet — La condition ON porte la clé métier ; les clauses WHEN MATCHED exécutent update/delete et WHEN NOT MATCHED insert. Dédupliquer la source si plusieurs lignes peuvent correspondre à la même cible.

SQL
MERGE INTO dev.silver.customers t
USING dev.bronze.customer_changes s
ON t.customer_id = s.customer_id
WHEN MATCHED AND s.operation = 'DELETE' THEN DELETE
WHEN MATCHED THEN UPDATE SET
  t.email = s.email,
  t.country = s.country
WHEN NOT MATCHED AND s.operation <> 'DELETE' THEN
  INSERT (customer_id, email, country)
  VALUES (s.customer_id, s.email, s.country);
  1. MERGE WITH SCHEMA EVOLUTION — Il autorise le MERGE à ajouter à la cible des colonnes source compatibles, notamment avec UPDATE SET * et INSERT *. Il ne valide pas le sens métier des nouvelles colonnes et ne remplace pas l'évolution de schéma Auto Loader.

Objets Gold, pipelines et streaming — réponses 19 à 26

  1. Objet Gold et refresh — Table pour une sortie persistée pilotée explicitement ; vue pour stocker seulement une requête ; materialized view pour persister et rafraîchir un résultat analytique ; streaming table pour faire progresser une cible depuis un flux. Une MV autonome peut déclarer SCHEDULE REFRESH EVERY 1 DAY ou un cron Quartz à six champs, par exemple SCHEDULE CRON '0 0 0 * * ?' AT TIME ZONE 'UTC'; une MV d'un pipeline suit le schedule du pipeline.

  2. Vue temporaire ou persistante — CREATE TEMPORARY VIEW reste limitée à la session ou au pipeline et sert d'étape interne. CREATE VIEW catalog.schema.name enregistre une définition gouvernée et réutilisable dans Unity Catalog.

  3. Trois joins streaming — Stream-static enrichit chaque nouveau micro-batch avec le snapshot courant d'une dimension sans recalculer l'ancien résultat. Une materialized view joint les états matérialisés avec une sémantique batch. Stream-stream joint deux flux incrémentalement, conserve de l'état et nécessite des bornes temporelles/watermarks.

  4. Watermark, état, output mode et checkpoint — Le watermark borne l'attente des données tardives et permet de nettoyer l'état. append émet les résultats finalisés, update les résultats modifiés, complete tout le résultat. Le checkpoint stocke offsets, commits, métadonnées de requête et état des opérateurs stateful.

  5. foreachBatch et CDF — foreachBatch traite chaque micro-batch avec (batch_df, batch_id) et fournit une garantie at-least-once : utiliser un MERGE déterministe ou batch_id pour l'idempotence. Le CDF se lit avec readChangeFeed=true et expose _change_type, _commit_version et _commit_timestamp.

PYTHON
changes = (spark.readStream
                .option("readChangeFeed", "true")
                .table("dev.silver.customers"))

(changes.writeStream
        .foreachBatch(upsert_changes)
        .option("checkpointLocation", "/Volumes/dev/checkpoints/customer_cdf")
        .start())
  1. Expectations — EXPECT enregistre la violation mais conserve la ligne ; EXPECT ... ON VIOLATION DROP ROW écarte la ligne ; EXPECT ... ON VIOLATION FAIL UPDATE rend l'update atomiquement en échec.

  2. Pipeline multi-fichiers — Un fichier contient un seul langage, mais SQL et Python peuvent coexister dans le pipeline. Les références entre datasets construisent le DAG indépendamment de l'ordre des fichiers. Utiliser le graphe et Dry run, puis valider l'ensemble du pipeline avant la production.

  3. Parameters ou Configuration — Avec la fonctionnalité Beta actuelle, définir target_schema comme job parameter ou dans Pipeline task > Parameters, puis utiliser IDENTIFIER(:target_schema) en SQL ; les job parameters sont automatiquement transmis. Pour du Python de pipeline, utiliser Configuration et spark.conf.get("target_schema"). Un ancien QCM peut attendre Configuration comme mécanisme général : repérer s'il emploie ${clé}/spark.conf.get ou le nouveau paramètre SQL :clé.

Jobs, Git et déploiement — réponses 27 à 37

  1. DAG Lakeflow Jobs — Les dépendances imposent l'ordre ; les branches indépendantes s'exécutent en parallèle si les ressources le permettent. If/else choisit une branche, For each répète une tâche et une tâche de cleanup utilise souvent Run if: All done.

  2. If/else avec une task value — Le notebook amont appelle dbutils.jobs.taskValues.set(key="record_count", value=n). La condition aval référence exactement {{tasks.ingest.values.record_count}}, puis configure séparément l'opérateur et la valeur de comparaison.

  3. Choix du trigger — Scheduled répond à une exigence horaire ; file-arrival démarre après l'arrivée de fichiers ; table-update réagit aux nouveaux commits, y compris DELETE. Le déclenchement suit la détection et les délais configurés, il n'est pas instantané au sens strict. Les Jobs actuels acceptent plusieurs triggers ; deux Jobs ne sont qu'un fallback pour une combinaison ou un ancien environnement non pris en charge.

  4. Retry, rerun, repair et idempotence — Retry relance automatiquement une task selon sa politique. Rerun relance le Job. Repair relance les tâches échouées/ignorées et leurs dépendantes sans rejouer les succès inutiles. Le code reste responsable de l'idempotence : un replay ne doit ni dupliquer ni corrompre les données.

  5. Historique et notifications — Dans Jobs & Pipelines > Job > Runs, comparer les runs actifs et terminés conservés 60 jours, puis ouvrir les tâches et métriques du run pour confirmer la cause d'une dérive. Une notification Job décrit l'état global ; une notification task localise une étape critique. Pour éviter une alerte à chaque tentative, n'alerter qu'après la dernière retry lorsque l'option le permet.

  6. Workflow Git folder — Créer/sélectionner une branche, modifier, consulter le diff, commit, push, puis ouvrir une pull request. Pour un monorepo, le sparse checkout limite le folder à des sous-répertoires avec des motifs cone ; il ne prend pas les exclusions ! et ne se désactive pas sur le folder existant.

  7. OAuth, PAT et service principal — GitHub App/OAuth est recommandé pour l'accès Git interactif d'un utilisateur. Un PAT Git est un secret de repli à portée et durée minimales. Un service principal est l'identité non humaine adaptée à la CI/CD et au run as de production.

  8. Dépendances notebook et tests — %pip install "package==version" crée un environnement Python limité au notebook, non visible par les autres notebooks et non persistant entre les sessions ; un changement peut nécessiter dbutils.library.restartPython(). Pour tester, construire de petits DataFrames, appeler une transformation sans dépendance au notebook, comparer avec assertDataFrameEqual, puis lancer pytest -q.

  9. Lecture de databricks.yml — resources déclare les Jobs/pipelines, variables factorise les valeurs et targets définit dev/test/prod. bundle.databricks_cli_version impose une version compatible. mode: development préfixe notamment les noms et met les schedules/triggers en pause. artifacts décrit les builds ; pour un wheel Python, le type exact est whl.

  10. CLI, bundle et full refresh — Utiliser la CLI moderne : socle actuel 0.205+, et au moins 0.218 pour bundle schema; installer avec le script officiel ou databricks/setup-cli en GitHub Actions, idéalement avec une version épinglée. bundle schema alimente l'IDE, bundle validate vérifie réellement le projet. --profile choisit l'authentification, --target la configuration. pipelines start-update <id> --full-refresh reconstruit le pipeline.

  11. Interface d'automatisation — REST API pour les primitives HTTP et intégrations génériques ; CLI pour le terminal et les scripts ; SDK pour une application typée Python/Java/Go/R ; bundle pour déployer déclarativement un ensemble versionné de ressources et d'environnements.

Performance, maintenance et gouvernance — réponses 38 à 48

  1. Lire Spark UI — Un skew apparaît par quelques tasks beaucoup plus longues/grosses ; le shuffle par Exchange et de forts shuffle read/write ; le spill par memory/disk spill. Scheduler Delay long avec calcul court suggère trop de petites tasks ou une attente d'executors. La locality ANY signifie qu'aucun placement plus local n'a été retenu, ce qui peut être normal avec du stockage objet.

  2. collect, cache et libération — collect() ramène toutes les lignes sur le driver et peut provoquer un OOM. cache() est lazy : une première action matérialise le DataFrame ; le cache n'est rentable qu'en cas de réutilisation. unpersist() libère ensuite les blocs.

  3. Échecs et disque local — Driver OOM : données/métadonnées centralisées, souvent après collect. Executor OOM : task, état ou broadcast trop gros. Cluster startup failure : compute jamais démarré ; consulter ses event logs. L'autoscaling du stockage local peut attacher des volumes lorsqu'un worker manque de disque pendant un shuffle/spill, mais n'ajoute ni RAM, ni CPU, ni executors.

  4. DDL/DML et CDC/SCD — DDL définit les objets (CREATE, ALTER, DROP) ; DML agit sur les lignes (INSERT, UPDATE, DELETE, MERGE). CDC transporte les changements. SCD définit leur stockage : type 1 remplace l'état, type 2 crée des versions historisées. Dans un pipeline, AUTO CDC INTO — anciennement APPLY CHANGES INTO — applique ces changements : KEYS identifie l'entité et SEQUENCE BY retient l'ordre logique, y compris pour les événements arrivés en retard.

  5. Maintenance moderne — Liquid clustering organise progressivement les données avec des clés adaptables. Predictive optimization automatise OPTIMIZE, VACUUM et ANALYZE sur les tables managées UC. OPTIMIZE réécrit le layout, VACUUM supprime les anciens fichiers et ANALYZE collecte les statistiques de l'optimiseur.

  6. Pruning, skipping, optimize et Z-order — Partition pruning ignore des partitions grâce à un filtre sur la colonne de partition. Data skipping ignore des fichiers grâce aux statistiques min/max. OPTIMIZE compacte/réécrit les fichiers ; ZORDER BY est une option de cet OPTIMIZE qui colocalise les valeurs et améliore le skipping.

  7. Rétention, time travel et copies — VACUUM utilise par défaut une rétention de sept jours et supprime les fichiers non référencés plus anciens ; les versions qui en dépendent deviennent illisibles. CTAS écrit le résultat transformable d'une requête sans reprendre la configuration de la table source. Un deep clone copie les données et les métadonnées de table et devient indépendant ; un shallow clone ne copie que les métadonnées et référence les fichiers source. Aucun clone ne reprend l'historique Delta de la source.

  8. Contrainte et colonne générée — Une contrainte CHECK s'applique à toutes les écritures et une seule ligne invalide fait échouer la transaction. order_date DATE GENERATED ALWAYS AS (CAST(order_time AS DATE)) demande à Delta de calculer la date ; une valeur explicitement fournie doit correspondre à cette expression.

SQL
ALTER TABLE dev.silver.orders
ADD CONSTRAINT valid_amount CHECK (amount >= 0);
  1. Stockage, volumes et fédération — L'identité cloud porte les permissions physiques ; la storage credential l'encapsule ; l'external location l'associe à un chemin. Une table externe gouverne des données tabulaires, un external volume des fichiers non tabulaires. Lakeflow Connect copie vers Delta ; Lakehouse Federation utilise une CONNECTION puis un foreign catalog pour interroger la source sans ingestion.

  2. Privilèges Unity Catalog — Gérer les grants dans Catalog Explorer > objet > Permissions ou en SQL. Pour lire : USE CATALOG + USE SCHEMA + SELECT; pour modifier, ajouter MODIFY; pour appeler une fonction, EXECUTE. ALL PRIVILEGES accorde les droits applicables au type d'objet, mais pas MANAGE, EXTERNAL USE SCHEMA ni EXTERNAL USE LOCATION.

SQL
GRANT USE CATALOG ON CATALOG production TO `data_engineers`;
GRANT USE SCHEMA ON SCHEMA production.sales TO `data_engineers`;
GRANT SELECT, MODIFY ON TABLE production.sales.orders TO `data_engineers`;
  1. Filtrage et masquage — Un row filter décide quelles lignes sont visibles et peut recevoir plusieurs colonnes dans l'ordre/type de ses paramètres ; un seul est attaché par table. Un column mask transforme une valeur, une dynamic view code une logique contextuelle complète et ABAC applique des politiques par governed tags à grande échelle.

SQL, ingestion, compute et Bundles — réponses 49 à 56

  1. Auto Stop — Il arrête le SQL warehouse après la durée d'inactivité configurée et réduit les coûts. Une nouvelle requête peut le redémarrer. L'autoscaling est différent : il adapte le nombre de clusters pour absorber la concurrence pendant que le warehouse fonctionne.

  2. Modes Auto Loader — addNewColumns met à jour schemaLocation, arrête le stream, puis le redémarrage reprend ; les anciennes lignes valent NULL pour la nouvelle colonne. addNewColumnsWithTypeWidening ajoute aussi les élargissements compatibles. failOnNewColumns échoue sans mise à jour, rescue capture les nouveautés dans rescued data sans échec et none les ignore sauf rescued data explicitement configuré.

  3. Conversion tolérante — TRY_CAST('100$' AS INT) retourne NULL, ce qui permet d'identifier ou mettre en quarantaine la ligne. CAST est adapté aux données garanties conformes et échoue sinon.

  4. Jointures particulières — a.order_id <=> b.order_id matche aussi deux valeurs NULL, contrairement à =. colors_df.crossJoin(sizes_df) produit toutes les combinaisons, soit n × m lignes.

  5. Fonction SQL UDF — La syntaxe minimale est CREATE FUNCTION ... RETURNS ... RETURN expression. Préférer les fonctions intégrées pour une expression locale simple ; utiliser une UDF Unity Catalog pour une logique réellement réutilisable et gouvernée.

SQL
CREATE OR REPLACE FUNCTION dev.utils.plus_one(value INTEGER)
RETURNS INTEGER
RETURN value + 1;
  1. AQE et broadcast — AQE observe les tailles runtime et peut convertir un sort-merge join en broadcast-hash join si un côté est sous le seuil et que le type de join l'autorise. Ce choix est automatique mais non garanti ; un hint reste pertinent quand la petite dimension est connue avec certitude.

  2. Commandes bundle — bundle init initialise un projet. --var fournit une variable de configuration résolue au déploiement ; --params transmet un job parameter au run. --auto-approve rend le déploiement non interactif sans remplacer validate.

BASH
databricks bundle init
databricks bundle deploy -t dev --var="schema_name=finance_schema"
databricks bundle run -t dev --params schema_name=marketing_schema sales_job
databricks bundle deploy -t prod --auto-approve
  1. Replay forcé de COPY INTO — COPY_OPTIONS ('force' = 'true') recharge les fichiers même s'ils ont déjà été suivis. Cela désactive l'idempotence ; préparer la cible ou une déduplication/upsert avant de l'utiliser.

Schémas, déploiement et objets workspace — réponses 57 à 64

  1. Inférence Auto Loader — JSON, CSV et XML sont inférés en STRING par défaut. cloudFiles.inferColumnTypes=true demande une inférence de types à partir de l'échantillon initial ; un schéma explicite ou des hints restent plus sûrs en production.

  2. Ajouter NOT NULL — ALTER TABLE transactions ALTER COLUMN transaction_id SET NOT NULL est valide pour Delta. Databricks vérifie l'historique présent : toute valeur NULL fait échouer l'ALTER, puis la contrainte protège les futures écritures.

  3. Protéger un déploiement — databricks bundle deploy -t prod --fail-on-active-runs s'arrête en erreur si un Job ou pipeline du déploiement est actif. Il ne l'annule pas et n'attend pas qu'il se termine.

  4. CAST strict — CAST('100$' AS INT) lève CAST_INVALID_INPUT, car le symbole $ rend la chaîne invalide. TRY_CAST retourne NULL; nettoyer la valeur avant conversion seulement si la règle métier l'autorise.

  5. Table externe CSV — La syntaxe correcte utilise LOCATION 'chemin' sans =. Elle référence les fichiers au format CSV et ne fournit ni conversion Delta ni time travel.

SQL
CREATE TABLE dev.raw.my_table (col1 STRING, col2 STRING)
USING CSV
OPTIONS (header = 'true', delimiter = ';')
LOCATION 's3://company-data/input/my_table';
  1. Emplacement du notebook — Son source et ses métadonnées sont des objets workspace stockés et chiffrés dans le control plane. Le compute plane exécute le code ; les résultats peuvent être stockés dans le control plane ou le stockage du workspace selon leur taille, le mode d'exécution et la configuration.

  2. Permissions DAB et run_as — permissions attribue des ACL comme CAN_VIEW, CAN_MANAGE_RUN, CAN_MANAGE ou IS_OWNER. run_as.service_principal_name dans la target prod fixe l'identité runtime pour ses ressources ; elle peut différer du déployeur et doit posséder les privilèges UC nécessaires.

  3. Ownership d'une table — Dans l'UI : Catalog Explorer > table > Owner. En code : ALTER TABLE catalog.schema.table OWNER TO principal. Le transfert d'ownership est différent d'un simple GRANT.

Git, Delta et gouvernance avancée — réponses 65 à 70

  1. Actualiser un Git folder — Utiliser Pull pour récupérer la branche distante suivie et mettre à jour le contenu du Git folder. Push effectue le trajet inverse, Commit enregistre les changements locaux et Clone crée initialement le folder.

  2. Delta et stockage — Delta Lake est une couche de table transactionnelle, pas un stockage physique autonome : les fichiers Parquet et _delta_log résident généralement dans le cloud object storage. Delta impose le schéma à l'écriture et refuse les colonnes ou types incompatibles, sauf cast sûr ou évolution explicitement activée.

  3. Importer un Job existant — databricks bundle generate job --existing-job-id <job_id> --bind génère le YAML et les fichiers pris en charge, puis lie la ressource au Job existant. Le binding permet à databricks bundle deploy de mettre à jour ce Job ; sans binding, le déploiement crée normalement une nouvelle ressource. deploy ne lance pas le Job : utiliser bundle run pour cela.

  4. Expectation bloquante — La syntaxe complète est CONSTRAINT valid_id EXPECT (id IS NOT NULL) ON VIOLATION FAIL UPDATE. Toute ligne invalide fait échouer et annuler atomiquement l'update du flow. En Python, utiliser @dp.expect_or_fail("valid_id", "id IS NOT NULL").

  5. Tag et policy ABAC — Créer le governed tag account-level avec ses valeurs autorisées, puis affecter directement pii = credit_card à la colonne avec ALTER TABLE ... ALTER COLUMN ... SET TAGS. Vérifier dans <catalog>.information_schema.column_tags. L'affectation exige notamment APPLY TAG sur l'objet et ASSIGN sur le tag. Dans la policy, MATCH COLUMNS has_tag_value(...) AS cc cible la colonne, ON COLUMN cc applique le masque, et TO/EXCEPT définissent les principals. Le tag seul ne protège rien et ABAC ne remplace pas SELECT.

  6. SET MANAGED ou deep clone — Pour convertir une table externe UC existante, SET MANAGED conserve son identité et son historique, absorbe les commits concurrents pendant la copie et réduit la coupure à une courte bascule finale. Un deep clone crée une nouvelle table autonome : il convient à une copie, mais oblige à gérer la synchronisation et le cutover des consommateurs.

Plateforme, sources et traitements avancés — réponses 71 à 83

  1. Data warehousing et plans — Databricks SQL fournit les services de data warehousing et utilise des SQL warehouses comme compute. L'application web est hébergée dans le control plane ; le compute plane traite les données.

  2. Timestamps en UTC — Stocker un instant en TIMESTAMP après interprétation du fuseau source et normalisation UTC. Fixer la session à UTC pour un affichage reproductible. Réserver TIMESTAMP_NTZ à une heure civile qui ne représente volontairement pas un instant universel.

  3. Expectations réutilisables — @dp.expect_all(valid_products) applique toutes les entrées d'un dictionnaire {nom: prédicat_sql} avec le mode warn : les lignes sont conservées et mesurées. La fonction doit retourner un DataFrame, par exemple spark.readStream.table("bronze_sales").

  4. Backfill et fichiers binaires Auto Loader — cloudFiles.includeExistingFiles=true, valeur par défaut, inclut les fichiers présents au premier démarrage seulement. cloudFiles.format=binaryFile retourne une ligne par fichier et pathGlobFilter=*.jpg limite la sélection aux JPEG.

  5. Filtre temporel COPY INTO — Placer 'modifiedAfter' = '<timestamp>' dans FORMAT_OPTIONS. Le filtre porte sur la date de modification des fichiers ; le suivi idempotent de COPY INTO continue à ignorer les fichiers déjà chargés.

  6. JDBC, Connect ou Federation — spark.read.format("jdbc") est batch. Un connecteur managé effectue généralement un snapshot initial puis un chargement incrémental lorsque la source le permet, par CDC ou curseur selon le connecteur. Federation interroge à distance via CONNECTION + foreign catalog sans copier dans Delta.

  7. Filtrer un array — FILTER(students, s -> s.total_courses < 3) et F.filter("students", lambda s: s.total_courses < 3) retournent un sous-tableau dans la même ligne. WHERE filtre des lignes et explode transforme les éléments en lignes.

  8. Kafka streaming — Utiliser spark.readStream.format("kafka"), les bootstrap servers et exactement une option de souscription. startingOffsets=latest ne pilote que la nouvelle requête ; une reprise utilise les offsets du checkpoint. key et value sont binaires et doivent être décodés.

  9. Extraire du JSON — Sur un JSON STRING, payload_json:device_id ou get_json_object(payload_json, '$.device_id') extrait du texte mais reparsera la chaîne lors d'une autre requête. Pour un document flexible réutilisé, persister try_parse_json(payload_json) en VARIANT, puis caster payload:device_id. Pour un contrat stable, préférer from_json vers un STRUCT ; matérialiser en colonnes typées les champs souvent filtrés ou agrégés.

  10. Liquid clustering — Pour des filtres fréquents sur user_id et event_date, utiliser CLUSTER BY (user_id, event_date). CLUSTER BY AUTO laisse Predictive Optimization analyser l'historique des requêtes et adapter les clés de façon asynchrone sur une table managée Unity Catalog.

  11. Executor perdu pendant un shuffle — Si de grosses partitions provoquent un OOM executor, augmenter la mémoire par executor et le nombre de partitions réduit la charge par task. Confirmer avec shuffle, spill, GC et tailles de tasks dans Spark UI ; le message peut aussi signaler réseau, Spot ou skew.

  12. Stream après SET MANAGED — Redémarrer la requête avec la même configuration et le même checkpoint pour reprendre après le dernier offset validé. La conversion peut rediriger temporairement les accès par chemin compatibles et permet UNSET MANAGED pendant la fenêtre de rollback.

  13. Décrire ou interroger les métadonnées — DESCRIBE DATABASE db_hr est un alias de DESCRIBE SCHEMA db_hr; EXTENDED ajoute des détails. Pour des requêtes standardisées, <catalog>.information_schema décrit un catalog, tandis que system.information_schema traverse tout le metastore Unity Catalog hors hive_metastore, en filtrant automatiquement les objets non accessibles.

Architecture et configuration de production — réponses 84 à 90

  1. Predictive Optimization — ENABLE ou DISABLE pose un override sur le catalog, schema ou la table ; INHERIT le retire et reprend la valeur effective du parent. Contrôler le champ Predictive Optimization avec DESCRIBE CATALOG|SCHEMA|TABLE EXTENDED <nom>.

  2. Authentification, profils et secrets — Avec un PAT, définir DATABRICKS_HOST et DATABRICKS_TOKEN; préférer OAuth M2M en CI. --profile sélectionne un profil nommé de .databrickscfg. Les secrets applicatifs se lisent avec dbutils.secrets.get; la redaction des sorties ne remplace pas les ACL ni l'interdiction de logger la valeur.

  3. Ingestion streaming — Auto Loader expose cloudFiles, une source Spark Structured Streaming pour les fichiers. Pour Kafka à fort débit et latence sous la minute, utiliser readStream.format("kafka"), un trigger adéquat, un checkpoint stable et un sink Delta ; ajuster partitions et compute au débit mesuré.

  4. Queueing — Si une limite de concurrence est atteinte, un run avec queueing attend qu'une capacité se libère et commence donc après son horaire. Sans queueing, le run excédentaire est skipped. Avant d'augmenter max_concurrent_runs, vérifier que les runs qui se chevauchent peuvent écrire sans conflit.

  5. Emplacement des données et du calcul — En classic, compute et workspace storage résident dans le compte cloud client ; le serverless compute réside dans un compute plane géré par Databricks. Le source d'un notebook est un objet workspace, mais ses commandes s'exécutent dans le compute plane du Job. Les tables peuvent résider dans le stockage cloud gouverné connecté.

  6. Compute et coût — Standard convient aux workloads partagés ; Dedicated est requis notamment pour R, RDD, GPU ou accès système privilégié. Sur classic, Photon peut avoir un taux de DBU différent/supérieur, mais sa vitesse peut réduire le coût total ; il est déjà inclus sur serverless/SQL. Spot convient aux batchs idempotents et relançables. Un instance pool conserve des machines classic prêtes pour réduire le startup time ; elles ne consomment pas de DBU idle, mais restent facturées par le cloud.

  7. Schéma d'un CTAS — CREATE TABLE cible AS SELECT ... dérive noms et types du résultat de la requête ; une table_specification manuelle ne peut pas être combinée avec AS query. Utiliser casts et alias dans le SELECT, ou créer la table explicitement puis y insérer les données.

Fiabilité, refresh et partage — réponses 91 à 108

  1. Snapshot Delta et overwrite — Chaque commit publie atomiquement une version. Un lecteur utilise un snapshot cohérent, principe souvent décrit comme MVCC/snapshot isolation. Un overwrite crée une nouvelle version courante sans effacer immédiatement les anciennes ; VERSION AS OF et RESTORE restent possibles tant que les fichiers et le journal nécessaires sont retenus.

  2. Réinitialiser le bon état — schemaLocation conserve les schémas Auto Loader : le supprimer force une réinférence, mais n'efface pas le suivi des fichiers. Le checkpoint conserve progression, commits et état : le supprimer crée une nouvelle requête et peut retraiter toutes les données encore disponibles. Pour des doublons, contrôler d'abord checkpoint, allowOverwrites et idempotence du sink.

  3. Évolution, hints et lignes corrompues — Auto Loader convient aux JSON massifs évolutifs ; JSON/CSV/XML restent STRING sans inférence de types, tandis que Parquet porte ses types. schemaHints guide le type attendu de quelques colonnes sans figer tout le schéma ni effectuer un cast métier. _rescued_data récupère les champs/types inattendus ; _corrupt_record capture un document mal formé et est préférable au badRecordsPath legacy.

  4. Découverte des fichiers et cloud — Directory listing reliste le chemin ; file events/notifications réduit le coût et la latence à grande échelle. Sur S3 classique, un événement passe par SNS puis une file SQS. Sur ADLS, Storage Blob Data Contributor est un rôle courant de lecture/écriture pour l'identité, auquel s'ajoutent les permissions de notification adaptées ; ce n'est pas une option Auto Loader.

  5. Triggers, admission et exactly-once — Trigger.Once traite le backlog en un micro-batch puis s'arrête et est déprécié. AvailableNow fige le backlog au démarrage, le traite en un ou plusieurs micro-batches, puis s'arrête. Pour Auto Loader, cloudFiles.maxFilesPerTrigger est une limite dure et cloudFiles.maxBytesPerTrigger une limite souple ; le premier seuil atteint borne le batch. Le checkpoint permet la reprise, mais l'exactly-once exige aussi source rejouable et sink transactionnel/idempotent.

  6. Déduplication Kafka — Décoder un event_id stable, résoudre les doublons à l'intérieur du micro-batch, puis appeler dans foreachBatch un MERGE cible sur cet identifiant. Le callback est at-least-once : l'offset ne remplace pas une clé métier si le producteur peut republier le même événement.

  7. Update ou full refresh — Une update de streaming table reprend au checkpoint ; son full refresh tronque la cible, efface cette progression et rejoue la source encore disponible. Une materialized view maintient/recalcule le résultat de sa requête. Après un renommage amont, modifier les références downstream et full-refresh seulement si l'état ou l'historique doit être reconstruit. Planifier un pipeline triggered avec une Pipeline task Lakeflow Jobs.

  8. Petits fichiers et layout — Optimized writes agit avant le commit, auto compaction après, et OPTIMIZE sur les fichiers déjà écrits. Sans besoin de placement, utiliser OPTIMIZE seul. ZORDER BY (event_ts) aide un filtre temporel et ZORDER BY (id) un point lookup si les statistiques/layout s'y prêtent ; sur une nouvelle table managée, considérer d'abord liquid clustering.

  9. Append-only et rétention — delta.appendOnly=true autorise les inserts mais interdit de modifier/supprimer les lignes existantes. delta.deletedFileRetentionDuration détermine quand VACUUM peut supprimer les anciens fichiers ; delta.logRetentionDuration conserve le journal. Time Travel/CDF exigent encore le journal et les fichiers référencés.

  10. Partage inter-workspace — Deux workspaces du même metastore accèdent généralement aux mêmes objets par grants Unity Catalog et catalog bindings. Pour un autre metastore, compte, région ou consommateur externe, utiliser Delta Sharing : le recipient lit les actifs du share sans obtenir ownership ni droit d'écriture sur la source.

  11. JDBC incrémental — Le JDBC Spark standard reste batch. Conserver un curseur monotone, idéalement (updated_at, id), interroger seulement les lignes postérieures, écrire avec un MERGE idempotent, puis avancer le curseur après le commit. Un petit overlap protège des ex æquo/retards ; un connecteur managé CDC/cursor reste préférable s'il existe.

  12. Suivi de fichiers et notifications — COPY INTO ignore un fichier déjà suivi même s'il est modifié ; force=true le recharge et peut dupliquer les lignes. Les retries SQS n'impliquent pas à eux seuls des doublons Auto Loader, car le checkpoint suit l'identité des fichiers. Pour un fichier S3 absent, vérifier préfixes d'événements, SNS/SQS, IAM/région et checkpoint ; à très grande échelle, préférer les managed file events aux listings répétés.

  13. JSON imbriqué et schémas — cloudFiles.schemaHints accepte des chemins imbriqués comme customer.address.zip et items.element.price. Un champ attendu mais absent vaut NULL. Un type incompatible se traite au lecteur par schéma/hints/rescued data ou en Silver ; mergeSchema ne corrige pas le JSON, il élargit la table Delta cible à l'écriture.

  14. MERGE fiable et performant — Dédupliquer la source sur une clé métier stable avant le MERGE; plusieurs nouvelles lignes de même clé peuvent sinon être insérées et plusieurs matches cible sont ambigus. appendOnly autorise un merge insert-only, mais interdit ses updates/deletes. Un prédicat de date/partition valide et un bon layout — Z-order pour une table non liquid-clustered ou liquid clustering — peuvent réduire les fichiers cible lus ; optimized writes/auto compaction préviennent les petits fichiers et OPTIMIZE traite l'existant.

  15. Validation et quarantaine — ISO-8601 se valide avec un prédicat RLIKE complété par TRY_CAST; une liste autorisée avec IN (...). Appliquer ces expressions via expect, expect_or_drop, expect_or_fail ou leurs variantes expect_all*. Une quarantaine classe ensuite les lignes et branche les enregistrements valides et invalides vers deux tables.

  16. RESTORE, CDF et reproductibilité — RESTORE ajoute un nouveau commit qui peut apparaître au CDF et provoquer un retraitement ; il ne rembobine pas le curseur et ne recrée pas les versions CDF expirées. Pour reproduire un entraînement, enregistrer la version Delta et relire avec VERSION AS OF/versionAsOf, avec le code et les paramètres. Utiliser CDF pour suivre les changements ou archiver preimage/postimage.

  17. Ordre streaming et JDBC sans curseur — Une streaming table ne garantit aucun ordre global : Auto Loader est non ordonné et Kafka seulement ordonné par partition. Utiliser temps d'événement, watermark ou séquence métier, notamment AUTO CDC ... SEQUENCE BY. Sans colonne JDBC incrémentale fiable, charger un snapshot complet en staging, dédupliquer puis MERGE; ne synchroniser les suppressions que si le snapshot est complet.

  18. Maintenance et purge — Des commits/MERGE fréquents peuvent créer petits fichiers, nombreuses actions AddFile/RemoveFile et tombstones. Prévenir avec batching, pruning/layout et optimized writes ; compacter avec OPTIMIZE. Un DELETE retire la ligne du snapshot courant ; avec deletion vectors, REORG ... APPLY (PURGE) puis VACUUM après rétention assurent la suppression physique. RETAIN 0 HOURS est destructif, réduit le Time Travel et exige de contourner une sécurité normalement fixée à sept jours. VACUUM ne nettoie pas directement _delta_log.

11.3 Deux labs de synthèse conseillés

Lab 1 — Pipeline médaillon

  1. Déposer des JSON contenant quelques champs invalides dans un volume.
  2. Les ingérer avec Auto Loader en availableNow.
  3. Conserver métadonnées et _rescued_data en Bronze.
  4. Nettoyer, typer et dédupliquer en Silver.
  5. Créer une vue matérialisée Gold avec revenu journalier.
  6. Ajouter trois expectations, une de chaque type.
  7. Orchestrer avec un trigger file arrival et rendre l'écriture rejouable.

Lab 2 — Production, performance et sécurité

  1. Versionner le projet dans un Git folder.
  2. Créer un bundle avec targets dev et prod.
  3. Déployer en dev et provoquer volontairement une task en échec.
  4. Lire le DAG, corriger puis faire un repair run.
  5. Créer un join skewé et l'observer dans le Spark UI.
  6. Tester un broadcast join et comparer les métriques.
  7. Accorder un accès lecture minimal à un groupe.
  8. Ajouter un mask sur l'e-mail et un row filter par région.

12. Stratégie le jour J

  1. Lire d'abord la dernière phrase : elle contient souvent la décision demandée.
  2. Repérer les contraintes discriminantes : one-time/incremental, batch/streaming, petite/grande table, managée/externe, horaire/data-driven.
  3. Éliminer les réponses syntaxiquement impossibles.
  4. Préférer la solution Databricks la plus managée qui satisfait toutes les contraintes.
  5. Ne pas inventer une exigence absente de l'énoncé.
  6. Marquer les questions longues et revenir après un premier passage.
  7. Avec 90 minutes pour 45 questions, viser environ 60 minutes au premier passage et garder le reste pour la revue.

13. Sources officielles

Référentiel prioritaire :

Documentation de révision :

Les pages AWS, Azure et GCP décrivent les mêmes concepts Databricks de base, mais les noms des stockages, types de machines, chemins et réglages réseau varient selon le cloud.