Introductie
Delta Lake is in eerdere hoofdstukken telkens genoemd als "de tabellaag die ACID-garanties toevoegt aan een lakehouse" — in Data Warehousing Fundamentals, Databricks, Microsoft Fabric — zonder ooit uit te leggen hóe dat precies werkt. Dat is het onderwerp van dit hoofdstuk: het mechanisme dat transacties, schema-garanties en versiehistorie mogelijk maakt bovenop iets dat in de kern nog steeds een verzameling gewone, onveranderlijke Parquet-bestanden is.
De Transaction Log: het Hele Geheim in Eén Concept
Parquet-bestanden zelf zijn onveranderlijk en weten niets van elkaar — een map vol Parquet-bestanden is puur dat: bestanden. Delta Lake's volledige functionaliteit komt voort uit één aanvullend mechanisme: de transaction log, een map genaamd _delta_log naast de data, gevuld met genummerde JSON-bestanden (000000.json, 000001.json, ...) die exact vastleggen welke Parquet-bestanden op elk moment "geldig" zijn.
orders_delta_table/
├── part-00000-xyz.parquet ← ruwe databestanden, zelf onveranderlijk
├── part-00001-abc.parquet
└── _delta_log/
├── 00000000000000000000.json ← commit 0: tabel aangemaakt, 2 bestanden toegevoegd
├── 00000000000000000001.json ← commit 1: 1 bestand toegevoegd, 1 "verwijderd" (logisch)
└── 00000000000000000002.json
Elk JSON-bestand beschrijft een commit: welke bestanden zijn toegevoegd (add), welke zijn logisch verwijderd (remove — het fysieke bestand blijft nog even bestaan, zie VACUUM verderop), en metadata zoals het schema op dat moment. Een "update" van een rij in Delta Lake schrijft dus nooit een bestaand Parquet-bestand aan — het schrijft een nieuw Parquet-bestand met de bijgewerkte data, en voegt een commit toe die het oude bestand als verwijderd markeert en het nieuwe als toegevoegd. Dit is de sleutel tot bijna alles wat volgt in dit hoofdstuk: ACID, time travel, en concurrency zijn allemaal directe gevolgen van dit ene ontwerp.
Optimistic Concurrency Control
Omdat meerdere schrijfprocessen tegelijk tegen dezelfde Delta-tabel kunnen draaien, moet er een mechanisme zijn om conflicten te voorkomen zonder de tabel voor iedereen te vergrendelen (wat traditionele databases vaak wel doen, ten koste van doorvoersnelheid). Delta Lake gebruikt optimistic concurrency control: elk schrijfproces leest de huidige staat van de log, bereidt zijn wijziging voor, en probeert dan de volgende genummerde commit toe te voegen. Als een ander proces intussen al die volgende commit heeft geclaimd, faalt de poging, en Delta Lake probeert automatisch opnieuw — het herevalueert of de wijziging nog steeds veilig is gegeven de nieuwe staat, en committed dan als een latere versie.
Dit werkt goed zolang conflicten zeldzaam zijn (het gebruikelijke geval bij pipelines die verschillende delen van een tabel raken), maar twee schrijfprocessen die dezelfde rijen proberen te wijzigen, resulteren in een retry-poging die alsnog kan mislukken — relevant om te weten bij het ontwerpen van sterk parallelle laadpatronen, waar je bewust conflicterende writes wilt vermijden in plaats van op automatische retries te vertrouwen.
Time Travel: de Log als Geschiedenisboek
Omdat elke commit een volledig, geordend overzicht is van welke bestanden op dat moment geldig waren, kan Delta Lake een query tegen elke eerdere versie beantwoorden door simpelweg de log terug te lezen tot dat punt — functioneel hetzelfde resultaat als Snowflake's Time Travel uit Snowflake, maar hier expliciet gebaseerd op de commit-geschiedenis in plaats van een intern, verborgen mechanisme:
-- Bevragen op versienummer
SELECT * FROM orders_delta TIMESTAMP AS OF '2026-08-10';
SELECT * FROM orders_delta VERSION AS OF 42;
df_old = spark.read.format("delta").option("versionAsOf", 42).load("/path/orders_delta")
Schema Enforcement versus Schema Evolution
Standaard handhaaft Delta Lake het schema strikt: een write die kolommen bevat die niet in het bestaande schema passen (verkeerd type, onverwachte kolom), wordt geweigerd — precies het schema-contract-probleem uit Modern Data Platform Architecture, hier afgedwongen op storage-niveau in plaats van alleen via een extern data contract-document.
# Faalt standaard als het binnenkomende schema niet overeenkomt met het tabelschema
df.write.format("delta").mode("append").save("/path/orders_delta")
# Expliciete, bewuste schema-evolutie toestaan (nieuwe kolom toevoegen)
df.write.format("delta").mode("append").option("mergeSchema", "true").save("/path/orders_delta")
Het verschil met de schema-on-read-flexibiliteit van een pure data lake (zie Data Warehousing Fundamentals) is precies het punt: Delta Lake kiest bewust voor warehouse-achtige striktheid als standaardgedrag, met mergeSchema als expliciete, bewuste uitzondering — niet andersom. Dit voorkomt dat een stille schemawijziging in een bronsysteem een pipeline corrumpeert in plaats van hem direct, zichtbaar te laten falen.
Laat de Databricks Notebook Generator een compleet notebook met Delta Lake-merges voor je opzetten.
MERGE INTO: Upserts op wat Ooit Alleen-Lezen Was
Plain Parquet-bestanden ondersteunen geen UPDATE of MERGE — je kunt alleen hele bestanden lezen of nieuwe wegschrijven. Delta Lake's transaction log maakt MERGE INTO mogelijk door onder de motorkap precies te bepalen welke Parquet-bestanden rijen bevatten die moeten wijzigen, alleen díe bestanden te herschrijven, en de log dienovereenkomstig bij te werken — functioneel hetzelfde resultaat als de MERGE-patronen uit SQL for Data Engineers, nu mogelijk op bestandsopslag die dat van nature niet ondersteunt:
MERGE INTO silver.orders AS target
USING staging.orders_updates AS source
ON target.order_id = source.order_id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *
Dit is precies het mechanisme waarmee de SCD Type 2-patronen uit Star Schema & Dimensional Modeling en de idempotente laadpatronen uit Introduction to Data Engineering op een lakehouse worden geïmplementeerd, in plaats van alleen op een traditioneel warehouse.
OPTIMIZE en Z-Ordering: het Kleine-Bestanden-Probleem
Frequente, kleine writes (elke paar minuten een nieuwe batch) resulteren in veel kleine Parquet-bestanden — inefficiënt om te lezen, omdat elk bestand een eigen open/close-overhead heeft, ongeacht hoe weinig data het bevat. OPTIMIZE compacteert kleine bestanden tot grotere, efficiëntere bestanden:
OPTIMIZE silver.orders;
OPTIMIZE silver.orders ZORDER BY (customer_id);
Z-ordering gaat een stap verder dan simpele compactie: het herordent data binnen de nieuwe, grotere bestanden zodat rijen met gerelateerde waarden voor de opgegeven kolom(men) fysiek dicht bij elkaar liggen — vergelijkbaar in doel met een clustering key in Snowflake uit Snowflake, maar met een andere onderliggende techniek (een Z-order-curve die meerdere kolommen tegelijk kan optimaliseren, in plaats van Snowflake's enkelvoudige clustering-sleutel-aanpak). Het resultaat is effectievere file skipping: een query die filtert op customer_id kan hele bestanden overslaan omdat de min/max-metadata van elk bestand aangeeft dat de gezochte waarde daar niet in voorkomt.
VACUUM: de Spanning tussen Time Travel en Opslagkosten
Elke MERGE, UPDATE of OPTIMIZE laat de oude, nu logisch verwijderde Parquet-bestanden fysiek staan — nodig om time travel naar eerdere versies mogelijk te maken, maar het betekent dat opslag onbeperkt blijft groeien zonder opruiming. VACUUM verwijdert fysiek bestanden die ouder zijn dan de retentiedrempel (standaard 7 dagen) én niet meer worden gerefereerd door een recente commit:
VACUUM silver.orders RETAIN 168 HOURS; -- 7 dagen, de standaard-ondergrens
De afweging is expliciet: een langere retentie behoudt meer time-travel-geschiedenis maar kost meer opslag; een kortere retentie bespaart opslag maar beperkt hoe ver je terug kunt kijken. Verlaag de retentie nooit lichtvaardig onder de standaard zonder te beseffen dat dit ook lopende, langlopende leesqueries kan breken als het bestand waar ze middenin lezen, wordt weggehaald.
Change Data Feed: Rijwijzigingen Zelf Consumeren
Standaard toont een Delta-tabel alleen de huidige staat; Change Data Feed (CDF), expliciet ingeschakeld per tabel, legt daarnaast vast wélke rijen zijn toegevoegd, gewijzigd of verwijderd per commit, inclusief een _change_type-kolom (insert, update_preimage, update_postimage, delete):
ALTER TABLE silver.orders SET TBLPROPERTIES (delta.enableChangeDataFeed = true);
SELECT * FROM table_changes('silver.orders', 10, 15); -- wijzigingen tussen versie 10 en 15
Dit is direct bruikbaar om downstream-consumenten (een andere pipeline, een reverse ETL-proces zoals besproken in ETL vs ELT) alleen te laten reageren op wat daadwerkelijk is veranderd, in plaats van steeds de hele tabel opnieuw te verwerken — een incrementeel patroon dat een stuk preciezer is dan alleen op een updated_at-watermark vertrouwen.
Deletion Vectors: Snel Verwijderen zonder Herschrijven
Een DELETE of UPDATE op een traditionele Delta-tabel herschrijft historisch het volledige Parquet-bestand dat de betreffende rijen bevat, ook als er maar één rij van de duizenden in dat bestand verandert — kostbaar bij grote bestanden en frequente, kleine wijzigingen. Deletion vectors, inmiddels standaard ingeschakeld op moderne Delta-versies, lossen dit op door verwijderde rijen te markeren in een apart, klein bestand naast het originele Parquet-bestand, zonder dat bestand zelf te herschrijven:
-- Met deletion vectors: dit markeert rijen als verwijderd i.p.v. bestanden te herschrijven
DELETE FROM silver.orders WHERE order_id = '12345';
Een latere OPTIMIZE compacteert deze markeringen alsnog fysiek weg door de betrokken bestanden op dat moment pas echt te herschrijven — deletion vectors verschuiven dus de kostbare herschrijfoperatie van "onmiddellijk bij elke DELETE" naar "periodiek, gebundeld, op een moment dat jij kiest", vergelijkbaar met hoe write-ahead logs in traditionele databases directe schrijfkosten uitstellen naar een latere, efficiëntere checkpoint-operatie.
Liquid Clustering: Herclusteren zonder Herontwerp
Z-ordening hierboven vereist dat je vooraf een vaste set clusteringkolommen kiest, en het wijzigen daarvan betekent een volledige herschrijving van de tabel. Liquid clustering is Delta Lake's nieuwere antwoord hierop: clusteringkolommen kunnen worden aangepast zonder de bestaande data te herschrijven, en het systeem past incrementeel toe op nieuw geschreven data in plaats van alles in één keer opnieuw te ordenen:
CREATE TABLE silver.orders CLUSTER BY (customer_id, order_date);
-- Later, zonder de tabel te herbouwen, de clusteringkolommen aanpassen
ALTER TABLE silver.orders CLUSTER BY (order_date, region);
Voor tabellen waar querypatronen na verloop van tijd veranderen — precies het soort evolutie dat in de Flowmetric-case study uit Modern Data Platform Architecture van fase 1 naar fase 3 plaatsvindt — is dit een aanzienlijke operationele verbetering ten opzichte van Z-ordering's vaste-kolommen-aanpak: de clusteringstrategie kan meegroeien met de organisatie zonder een dure, disruptieve herbouw.
Best Practices
- Laat schema enforcement standaard aan staan, en gebruik
mergeSchemaalleen bewust, per write, niet als permanente instelling die stille schemadrift toestaat. - Draai
OPTIMIZEperiodiek op tabellen met frequente, kleine writes — het kleine-bestanden-probleem lost zichzelf niet op. - Kies Z-order-kolommen op basis van je meest voorkomende filterpatronen, net als bij clustering keys — niet blindelings op elke kolom.
- Stel een bewust retentiebeleid in voor
VACUUM, afgestemd op hoe ver terug je daadwerkelijk time travel nodig hebt, niet op de standaardwaarde zonder nadenken. - Gebruik Change Data Feed voor downstream-consumenten die specifiek op wijzigingen moeten reageren, in plaats van een volledige tabel-rescan te forceren bij elke run.
- Overweeg liquid clustering boven traditionele Z-ordering voor tabellen met evoluerende querypatronen, zodat clusteringkolommen kunnen meegroeien zonder een dure, disruptieve herbouw.
Veelgemaakte Fouten
mergeSchemastandaard aanzetten op elke write, waardoor stille, ongewenste schemawijzigingen ongemerkt een tabel binnensluipen.OPTIMIZEnooit draaien op een tabel met frequente kleine writes, waardoor leesprestaties geleidelijk verslechteren zonder duidelijke oorzaak.VACUUMmet een te korte retentie draaien terwijl er nog langlopende queries of time-travel-afhankelijkheden actief zijn, wat die queries kan laten falen.- Denken dat plain Parquet en Delta Lake uitwisselbaar zijn — een Delta-tabel is een map met Parquet-bestanden plus een
_delta_log; die log direct negeren en de Parquet-bestanden los behandelen, breekt alle ACID-garanties. - Change Data Feed inschakelen op elke tabel "voor de zekerheid", wat onnodige opslag- en verwerkingskosten toevoegt voor tabellen waar niemand ooit de wijzigingsstroom consumeert.
Performance Tips
- Combineer
OPTIMIZE ZORDER BYmet je partitioneringskolom-strategie — Z-ordering werkt binnen partities, dus een goed gekozen partitiekolom (vaak datum) blijft ook op een Delta-tabel relevant. - Plan
OPTIMIZEbuiten piekuren — het is zelf een zware operatie die tijdelijk extra compute vraagt, net als reclustering bij Snowflake uit Snowflake. - Gebruik
VACUUMregelmatig, niet incidenteel, zodra retentie is verstreken — opgehoopte, nooit opgeruimde oude bestanden vertragen ook metadata-operaties zoals het lezen van de transaction log zelf. - Beperk het aantal kolommen in een Z-order-clausule tot twee of drie — te veel kolommen tegelijk verdunt het skipping-voordeel per kolom, in plaats van het te versterken.
Interviewvragen
"Hoe voegt Delta Lake ACID-garanties toe aan iets dat in de kern gewone, onveranderlijke Parquet-bestanden zijn?"
Let op: de _delta_log-transaction-log, die per commit vastlegt welke bestanden geldig zijn — updates schrijven nooit een bestaand bestand aan, ze voegen nieuwe bestanden toe en markeren oude als verwijderd.
"Wat is optimistic concurrency control, en hoe voorkomt het conflicten tussen gelijktijdige schrijfprocessen?" Let op: elk proces probeert de volgende commit toe te voegen; bij een conflict (een ander proces was sneller) wordt automatisch herprobeerd, in plaats van de tabel voor iedereen te vergrendelen.
"Wat is het verschil tussen schema enforcement en schema evolution in Delta Lake?"
Let op: enforcement is het standaardgedrag (afwijkende schema's worden geweigerd); evolution (mergeSchema) is een expliciete, bewuste uitzondering — nooit andersom.
"Waarom is VACUUM nodig, en welke afweging maak je bij het instellen van de retentieperiode?"
Let op: oude, logisch verwijderde bestanden blijven fysiek bestaan voor time travel; een langere retentie behoudt meer geschiedenis maar kost meer opslag, een kortere kan lopende queries breken.
"Wat is Z-ordering, en hoe verhoudt het zich tot een clustering key in Snowflake?" Let op: beide optimaliseren fysieke data-layout voor filterprestaties via file/partition skipping, met Z-ordering's Z-curve-techniek als het onderliggende, iets andere mechanisme dan Snowflake's clustering keys.
"Wat is Change Data Feed, en wanneer zou je het gebruiken?" Let op: een expliciet ingeschakeld mechanisme dat rijwijzigingen (insert/update/delete) per commit blootlegt, nuttig voor downstream-consumenten die incrementeel willen reageren op wijzigingen in plaats van steeds de hele tabel te herverwerken.
"Wat zijn deletion vectors, en welk probleem lossen ze op?"
Let op: ze markeren verwijderde rijen in een klein, apart bestand in plaats van direct het volledige onderliggende Parquet-bestand te herschrijven — de kostbare herschrijving wordt uitgesteld naar een latere, gebundelde OPTIMIZE-operatie.
"Wat is het verschil tussen Z-ordering en liquid clustering?" Let op: Z-ordering vereist vooraf vastgelegde kolommen en een volledige herschrijving om te wijzigen; liquid clustering laat clusteringkolommen aanpassen zonder bestaande data te herschrijven, wat beter meegroeit met veranderende querypatronen.
Relevante Documentatie
- Delta Lake: Transaction Log Protocol — de technische specificatie van
_delta_log. - Databricks: Delta Lake — Optimize and Z-Order — compactie en Z-ordering in detail.
- Delta Lake: Table Utility Commands (VACUUM) — retentie en opruiming.
- Delta Lake: Change Data Feed — configuratie en het gebruik van
table_changes().
Samenvatting
Delta Lake's volledige functionaliteit — ACID-transacties, time travel, schema-garanties — komt voort uit één mechanisme: de _delta_log-transaction-log die bijhoudt welke onveranderlijke Parquet-bestanden op elk moment geldig zijn, terwijl schrijfoperaties nooit bestaande bestanden aanpassen maar altijd nieuwe toevoegen. Optimistic concurrency control laat meerdere schrijvers veilig samenwerken zonder vergrendeling; MERGE INTO maakt upserts mogelijk op opslag die dat van nature niet ondersteunt. OPTIMIZE/Z-ordering en VACUUM zijn de operationele tegenhangers van respectievelijk het kleine-bestanden-probleem en de opslagkosten van behouden geschiedenis, en Change Data Feed ontsluit rijwijzigingen voor incrementele downstream-consumptie. Met deze mechaniek begrepen — van SQL, via medallion architecture, platformen, Python en Spark, tot de tabellaag zelf — verschuift het volgende hoofdstuk de aandacht naar hoe je dit alles betrouwbaar naar productie krijgt: CI/CD for Data Engineering.
Hulp nodig bij het bouwen van een modern dataplatform?
DataPartner365 helpt organisaties met Microsoft Fabric, Snowflake, Databricks, dbt, Azure, data-architectuur en CI/CD.
Neem contact op met DataPartner365