Waarom “gewoon bestanden opslaan” je pipeline kan breken

Je krijgt een incidentmelding: de nachtelijke batch is ineens 3× trager, een restore duurt uren langer dan verwacht, of een dataset die “gesorteerd” zou zijn levert toch rare performance op bij lookups. De reflex is vaak: “dan zetten we het in een database” of “meer compute.” Maar heel vaak zit de oorzaak eerder: in hoe bestanden op storage staan, hoe het OS ze aanbiedt, en welke garanties je niet krijgt als je een bestandsysteem gebruikt als datalaag.

Bestanden zijn aantrekkelijk omdat ze simpel voelen: je schrijft bytes, je leest bytes. In data engineering vertaalt dat zich naar CSV/JSON/Parquet in object storage, logs op disk, exports, landing zones en tijdelijke outputs. Het probleem is dat “simpel” hier ook betekent: minimale coördinatie, weinig ingebouwde integriteitsregels, en performance die sterk afhangt van access patterns (sequentieel vs random) en fysieke layout (blokken, caching, fragmentatie).

In deze les behandel je bestandsystemen als een technisch fundament: wat ze wél goed doen, welke beperkingen structureel zijn, en waarom die beperkingen historisch rechtstreeks aansluiten op ideeën als records, blokken/sectoren, latency vs throughput, sort/merge en rebuild-patronen. Dat geeft je taal en intuïtie om te besluiten: “Dit kan prima als files” versus “Hier heb ik database-achtige garanties nodig.”

Het vocabulaire: van bytes naar records (en terug)

Een bestandsysteem beheert opslag als bestanden (naam + bytes) in directories, met metadata zoals grootte en timestamps. Het levert twee basisoperaties die voor data engineering cruciaal zijn: streaming lezen/schrijven (sequentieel) en positioneel lezen/schrijven (random). Die twee vormen lijken op het verschil tussen tape en schijf: sequentieel kan extreem efficiënt zijn in throughput, terwijl random gedrag snel latency-dominant wordt.

Belangrijke begrippen die je steeds terugziet:

  • Bestand: een byte stream met een begin en eind; het bestandsysteem kent meestal geen “records” of schema.

  • Record: een logische eenheid (rij/event). In files is dit jouw verantwoordelijkheid (delimiters, fixed-width, framing).

  • Blok/sector: kleinste fysieke I/O-eenheid op storage. Je leest niet “één record”, maar een blok waar meerdere records in kunnen zitten.

  • Latency vs throughput: latency bepaalt hoe duur veel kleine I/O’s zijn; throughput bepaalt hoe snel een grote scan gaat zodra je streamt.

  • Sequentieel vs random access: sequentieel maximaliseert doorvoer; random veroorzaakt overhead (positioneren, metadata, cache misses).

Een nuttige analogie: een bestandsysteem is een magazijn met dozen (bestanden). Het systeem zorgt dat dozen vindbaar zijn en dat je ze kunt openen en vullen. Maar wat er in de doos zit (schema, consistentie, “één transactie”), en of twee medewerkers tegelijk dezelfde doos mogen aanpassen zonder rommel, is grotendeels niet geregeld.

Wat bestandsystemen goed doen (en waar de grens ligt)

Bestanden zijn geweldig voor streaming en bulk output

Bestandsystemen blinken uit in grote sequentiële lees- en schrijfstromen. Als je een dataset achter elkaar wegschrijft en later in één of enkele passes verwerkt, sluit dat aan bij het klassieke tape-idee: streaming is goedkoop. Veel data lake-processen voelen daarom als “lees → transformeer → schrijf nieuw”, wat conceptueel sterk lijkt op het historische rebuild-patroon: in-place updates vermijden en in plaats daarvan een nieuwe, volledige output produceren.

Dit werkt vooral goed als je data organisatie discipline hebt. Denk aan: consistente recordframing (bijvoorbeeld per regel of per pagina), vaste of duidelijk parsebare velden, en een sleutel die je eventueel kunt sorteren voor join/merge-achtige bewerkingen. Zodra je dat doet, winnen files op eenvoud, schaalbaarheid en herstelbaarheid: je kunt outputs als snapshots behandelen en een mislukte run herhalen zonder ingewikkelde transactieherstelmechanismen.

De typische misvatting is: “Als het een bestand is, is het automatisch simpel en betrouwbaar.” In werkelijkheid is het simpel in primitieve operaties, niet in garanties. Jij moet bepalen wat een “complete dataset” is, hoe je omgaat met partiële writes, hoe je checkpoints implementeert, en hoe je voorkomt dat downstream systemen halffabrikaten oppikken.

Bestanden zijn zwak in gelijktijdigheid, punt-updates en integriteitsgaranties

Zodra je verwachtingen hebt zoals “update één record”, “twee processen schrijven veilig tegelijk”, of “lees altijd een consistente snapshot terwijl er geschreven wordt”, schuif je richting database-vereisten. Een bestandsysteem heeft meestal geen begrip van records of keys; het kent bytes en offsets. Daardoor zijn punt-updates vaak logischerwijs duur of riskant: je moet zelf precies weten waar een record begint, hoe lang het is, en wat er gebeurt als de lengte verandert.

Gelijktijdigheid (concurrency) is een tweede harde grens. Bestandsystemen bieden wel locks en permissies, maar dat is grofmaziger dan databasetransacties. Twee writers op hetzelfde bestand leidt snel tot corruptie, interleaving of “last write wins”-achtige problemen. En zelfs met locks krijg je vaak óf te veel blokkering (slechte throughput) óf te weinig bescherming (inconsistenties). Dit is de moderne variant van de oude les: je kunt niet doen alsof je random updates op een sequentieel-georiënteerd systeem hebt zonder prijs te betalen.

Een veelgemaakte denkfout: “We zetten gewoon één groot CSV-bestand neer en updaten rijen.” Dat is conceptueel alsof je een tape of een stapel ponskaarten probeert te behandelen als een database: je krijgt óf dure rewrites, óf rommelige append-delta’s die later weer samengevoegd moeten worden.

Performance is niet “file vs database”, maar access pattern vs fysieke realiteit

Bestands-I/O gaat in blokken. Dat betekent dat een workload met veel kleine reads (bijvoorbeeld duizenden lookups verspreid over één groot bestand) vaak precies de pijn van schijf-achtige random I/O triggert: hoge latency per operatie, cache die maar deels helpt, en throughput die instort. Daartegenover staat dat een scan van hetzelfde bestand ineens wél snel kan zijn: één doorlopende stroom, goede prefetch, weinig overhead.

Daar komt een tweede effect bij: fysieke layout is niet gelijk aan logische layout. Een bestand dat “logisch gesorteerd” is, kan fysiek verspreid raken door hoe het wegschrijven en herschrijven gebeurt (fragmentatie-achtig gedrag, zeker bij edits of groei). Het gevolg is dat twee identieke queries totaal verschillend kunnen presteren afhankelijk van hoe de data fysiek in blokken ligt en hoe goed het OS/cache het patroon herkent.

Best practice hier is verrassend historisch: ontwerp data zodat je óf sequentieel kunt verwerken (sort/merge, batches), óf dat je een structuur gebruikt die random access economisch maakt (indexen, clustering). Als je niets expliciet ontwerpt, krijg je meestal het slechtste van beide: veel kleine reads zonder index, én veel write-amplification door herschrijven.

Beperkingen die je altijd moet meenemen

Hieronder staan de belangrijkste beperkingen van bestandsystemen in data-engineering context, naast wat ze wél bieden. Zie dit als een kompas: wanneer “files” prima zijn, en wanneer je bij voorbaat rekening houdt met extra lagen (formaten, manifesten, compaction, of uiteindelijk databases).

Dimensie Wat bestandsystemen goed doen Waar het vaak misgaat
Toegangsmodel Sequentiële I/O is efficiënt: grote scans en grote writes halen hoge throughput. Random I/O (veel kleine reads/writes) wordt latency-dominant en onvoorspelbaar.
Data-organisatie Je kunt zelf append-only datasets maken en outputs als snapshots behandelen. Het systeem kent geen records/keys; recordgrenzen, schema en integriteit zijn jouw probleem.
Updates Nieuwe versies schrijven (rebuild) is robuust en herhaalbaar. In-place updates zijn lastig en vaak duur; variabele recordlengte maakt het snel gevaarlijk.
Gelijktijdigheid Simpel voor single-writer of duidelijke batch-fases met duidelijke input/output. Meerdere writers/lezers zonder strikte afspraken geeft race conditions, corruptie of “half werk” dat al zichtbaar is.
Consistentie & herstel Je kunt met duidelijke run-grenzen en checkpoints voorspelbaar herstellen. Zonder transactie-idee krijg je partiële outputs; “is dit bestand compleet?” wordt een applicatievraag.

[[flowchart-placeholder]]

Drie patronen om beperkingen te omzeilen (zonder te doen alsof het een database is)

Patroon 1: Werk sequentieel met expliciete run-grenzen

Als files je datalaag zijn, is het verstandig om je verwerking te ontwerpen alsof sequentiële toegang de default is. Dat betekent: lees inputs in grote stromen, doe bewerkingen die locality respecteren (bijvoorbeeld sorteren op sleutel), en schrijf resultaten als nieuwe outputs. Dit is precies het historische sort → merge → write-denken dat ontstond uit ponskaarten en tape, maar het is ook vandaag een performance- en betrouwbaarheidspatroon.

De kern is expliciete run-grenzen. In plaats van “één bestand waar steeds aan geknutseld wordt”, maak je een output die je als “af” kunt markeren: bijvoorbeeld door een tijdelijke bestandsnaam te gebruiken en pas op het einde te renamen, of door per run een eigen directory te maken. Het doel is dat downstream systemen nooit hoeven te gokken of ze een gedeeltelijke write lezen.

Pitfall: veel teams vertrouwen op “het bestand bestaat, dus het is klaar.” Dat is een klassieke bron van bugs: een consumer ziet het bestand halverwege ontstaan en start al met lezen. Best practice is daarom: maak completion expliciet (atomische rename, manifest, of duidelijke success markers) en ontwerp retries alsof failures normaal zijn.

Patroon 2: Gebruik immutable + delta’s als je toch updates nodig hebt

Soms moet je “updates” doen: late-arriving events, correcties, of een slowly changing dimension-achtig effect. In files is het vaak beter om die updates te modelleren als delta’s (append-only) en periodiek te consolideren via een rebuild/merge. Dit is het tape-achtige master/transaction patroon in moderne kleding: een grote basisdataset en een log van veranderingen die je gecontroleerd samenvoegt.

Dit patroon is krachtig omdat het aansluit bij de sterke kant van files: sequential throughput. Je betaalt de update niet als veel kleine in-place writes, maar als een periodieke batch die toch al in je pipeline past. Bovendien is herstel eenvoudiger: als een consolidatie faalt, heb je oude basis + deltas nog.

Misvatting: “Delta’s maken alles automatisch sneller.” Niet per se. Als je delta’s blijven groeien zonder consolidatie, verschuift de pijn naar reads: elke query moet basis + N delta’s combineren. Best practice is dus: bepaal een ritme of drempel voor consolidatie, en meet wanneer reads te duur worden.

Patroon 3: Minimaliseer kleine willekeurige reads door layout en clustering

Als je wél vaak gerichte toegang nodig hebt, is het verstandig om al in je file-organisatie na te denken over locality. Op schijf-achtige media is het idee: betaal latency één keer, en lees daarna zoveel mogelijk nuttigs per blok. In file-termen betekent dat: data die je samen nodig hebt, staat ook fysiek bij elkaar, en je hoeft niet tientallen plekken te raken voor één antwoord.

Praktisch uit zich dat in keuzes als: partitioneren op een sleutel die je typisch filtert (bijvoorbeeld datum), outputs schrijven in grotere, aaneengesloten bestanden in plaats van duizenden mini-files, en ordening/clustering op een tweede sleutel om scans korter te maken. Het exacte mechanisme verschilt per platform, maar het principe is steeds hetzelfde: tem random access door data te groeperen.

Pitfall: “Meer splitsen is altijd beter paralleliseerbaar.” Vaak ontstaat dan het tegenovergestelde: een small files-probleem met enorme metadata overhead en veel open/close-operaties. De workload lijkt parallel, maar de storage- en file-metadata laag wordt de bottleneck.

Twee herkenbare voorbeelden uit data engineering

Voorbeeld 1: Nachtelijke batch die instort door ‘random’-gedrag

Stel je hebt een pipeline die elke nacht miljoenen events verwerkt en een rapportage-dataset bouwt. Een eerste implementatie doet een join door voor elk event een klein lookupje te doen in een referentiebestand op disk. Dat voelt logisch (“het is maar één record”), maar operationeel creëer je duizenden tot miljoenen kleine random reads. Latency gaat domineren: elke lookup betaalt overhead (positioneren, metadata, cache misses), waardoor je totale runtime explosief stijgt.

Een file-vriendelijk ontwerp gebruikt het sequentiële patroon. Je schrijft de events eerst weg in een goed streambaar formaat en sorteert (of groepeert) op de join-sleutel. Je doet hetzelfde met de referenties, zodat je een merge join-achtig proces kunt draaien: twee gesorteerde stromen die je in één pass combineert. Daarna schrijf je een nieuwe outputset weg als snapshot, met een duidelijke “klaar”-markering zodat downstream niet halverwege leest.

De impact is meestal: voorspelbare runtimes en betere throughput, tegen de prijs van minder flexibiliteit voor “even één record fixen.” Die beperking is niet een fout; het is het eerlijke gevolg van kiezen voor files en sequentiële verwerking. Als je die trade-off expliciet maakt, voorkom je dat je onbewust een database-workload op een file-architectuur forceert.

Voorbeeld 2: Een “één klant profiel” API op files: waarom het snel misleidend is

Stel je wilt voor een API-request één klantprofiel ophalen, en je hebt klantdata als bestanden opgeslagen. In een testomgeving werkt het: je zoekt de klant-id, je scant een bestand, klaar. Maar bij groei wordt het traag, omdat je van nature richting random access gaat: je wilt immers niet steeds alles scannen. Zonder index-structuur eindig je met óf scans (duur) óf allerlei losse bestanden/partities die je alsnog moet doorzoeken (ook duur, plus metadata overhead).

Een pragmatische aanpak is om de file-laag alleen te gebruiken voor wat hij goed kan: bulk opslag en batchbouw van een “serve”-vriendelijke structuur. Je bouwt periodiek een nieuw snapshot waarin records op klant-id geclusterd staan (locality), en je publiceert die snapshot atomisch. Reads worden dan voorspelbaarder omdat je minder verspreide I/O doet, en je restore/rollback is simpel: terug naar een vorige snapshot.

De beperking blijft: echte OLTP-achtige eisen (veel gelijktijdige updates, sterke consistentie, transacties) botsen met files. Als je die eisen echt hebt, is “meer files slimmer organiseren” vaak symptoombestrijding. De juiste les is: herken het punt waarop je een storage-primitive probeert te laten doen wat een database hoort te doen.

De kern die je moet onthouden

Bestandsystemen zijn een krachtig fundament voor data engineering zolang je hun natuur respecteert: sequentiële throughput is hun superkracht, en “database-achtige” garanties zijn meestal niet ingebouwd. Als je data als immutable outputs en streaming passes ontwerpt, krijg je eenvoud, herstelbaarheid en schaal. Als je punt-updates, sterke consistentie of veel random lookups verwacht, moet je expliciet extra structuren toevoegen of een ander type systeem kiezen.

In de volgende les, je’ll take this further with Van datasilo naar database door hardware [20 minutes].

Last modified: Tuesday, 17 February 2026, 6:41 AM