Elke query die je draait heeft een leven: hij wordt geboren in een SELECT, reist omlaag door catalogus, log, bestanden en CPU, en komt thuis als rijen op een dashboard. De snelste queries zijn de queries die onderweg bijna niets aanraken, omdat de hele stack draait om één economie: sluit dingen uit met kleine, goedkope metadata voordat je iets groots en duurs aanraakt. Verplaats beslissingen en samenvattingen, geen data.

Als het platform gezond is, werkt die economie bijna geruisloos: kaarten sluiten het grootste deel van de tabel ongelezen uit, een cache onthoudt waar het netwerk al voor betaald heeft, en de CPU krijgt de data precies in de vorm die hij prettig vindt. Maar op elk stuk van de route ligt een obstakel dat een goedwillende mens kan achterlaten: een verouderde config, een schedule die niemand zich herinnert, één regel Python op de verkeerde plek. Als een query traag is, is een van de lagen blind geworden, bijna altijd door iets wat de eigenaren van de tabel deden, of niet meer deden, op het moment van schrijven. Ken de route, en je kunt een trage query diagnosticeren door per laag één vraag te stellen, in plaats van naar een groter cluster te grijpen.

Laten we één query dus helemaal naar beneden volgen en hem thuisbrengen:

SELECT customer_id, SUM(amount)
FROM   sales.orders
WHERE  order_date = '2026-08-01'
GROUP BY customer_id;

Maak kennis met orders, de tabel die hij moet oversteken: drie jaar geschiedenis, 2,1 miljard rijen, 50 kolommen, 480 GB Parquet verdeeld over 8.400 bestanden, geclusterd op order_date. De query wil twee kolommen, filtert op een derde, en geeft om één dag van ongeveer duizend. Een naïeve engine leest 480 GB en filtert in het geheugen. Volg het scorebord na elke laag om te zien wat de echte leest.

In het spel: 8.400 bestanden · 480 GB · 50 kolommen · de query wil drie kolommen en één dag

Laag 0: De weigering

De reis heeft een stap voordat er überhaupt een engine aan te pas komt, en het is de enige met een perfecte score: een query die nooit draait, leest helemaal niets. Voordat je optimaliseert hoe het platform deze query uitvoert, kijk eerst eerlijk of hij wel uitgevoerd moet worden. Sommige aanroepen verdienen het om geweigerd te worden.

De duurste queries in een workspace zijn meestal niet traag, maar frequent. Het klassieke geval is een schedule dat losgekoppeld is van de data die het leest: een dashboard dat elke tien minuten ververst op een tabel die een nachtelijke pipeline één keer bijwerkt. Van de 144 dagelijkse runs rekenen er 143 tot op de byte het antwoord van gisteren opnieuw uit. Niemand heeft dat besloten; iemand accepteerde twee jaar geleden een standaard verversingsinterval, het schedule overleefde zijn maker, en het platform verbrandt trouw compute aan vragen waarvan het antwoord niet veranderd kan zijn.

Laag 0 is dus een vraag, één keer gesteld tijdens het ontwerp: hoe vaak verandert het antwoord echt, en wie wacht erop? De verversingsfrequentie van een query hoort af te hangen van hoe vaak de data binnenkomt, niet van een standaardwaarde in een dropdown. Een dagelijkse tabel verdient een dagelijkse verversing, getriggerd door het afronden van de pipeline in plaats van door een klok, zodat de twee nooit uit elkaar kunnen lopen.

Hoe het blind wordt

Schedules worden één keer ingesteld en nooit meer bekeken, terwijl de pipelines eronder van ritme veranderen, worden samengevoegd of verdwijnen. Een kwartaalcheck van je meest gedraaide queries tegenover de updatefrequentie van de tabellen die ze lezen, is de performance-review met de grootste hefboom die er bestaat, want een schedule verwijderen is precies honderd procent beter dan het versnellen.

Het bewijs: een verversingsteller die een versieteller voorbijracet. DESCRIBE HISTORY laat zien dat de tabel gisteren één versie verder ging; de runhistorie van het dashboard toont 144 verversingen in hetzelfde venster. De andere 143 berekenden een antwoord opnieuw dat niet veranderd kon zijn.

Wie het onderhoudt

Het platform vangt een deel hiervan voor je op. De result cache geeft een herhaalde query op ongewijzigde data terug zonder iets uit te voeren, en een materialized view levert een vooraf berekend resultaat, incrementeel ververst volgens het schedule of de trigger die je opgeeft in plaats van telkens vanaf nul. Beide leggen "het antwoord is niet veranderd" vast in het platform. Maar geen van beide kan een schedule annuleren; de beslissing dat een vraag niet gesteld hoeft te worden, blijft mensenwerk.

In het spel: alles · de enige besparing die niets kost, is niet draaien

Laag 1: Pruning

De query is door laag 0 gekomen; hij verdient het om te draaien. Wat daarna gebeurt is één idee, drie keer toegepast op steeds kleinere schaal: raadpleeg voordat je iets groots leest eerst een klein kaartje ervan, en sluit alles uit wat het kaartje kan uitsluiten.

De eerste kaart is de transaction log, die vastlegt welke van onze 8.400 bestanden op dit moment de tabel vormen en, bij elke bestandsverwijzing, de min/max-waarden van dat bestand voor de eerste kolommen. Dat laatste is het wapen. Omdat de tabel geclusterd is op order_date, beslaat elk bestand een smal stuk van de datums, dus de planner loopt de log door (gecachet op het cluster, zonder opslag aan te raken) en vraagt 8.400 keer: kan dit bestand 1 augustus bevatten? Voor alle bestanden op ongeveer 130 na is het antwoord aantoonbaar nee. De grootste besparing van de hele reis is net gebeurd, en er is nog geen enkel bestand geopend.

In het spel: 8.400 bestanden → 130 bestanden · 480 GB → ~7,4 GB

Nu dezelfde truc, één niveau dieper. De engine heeft een shortlist van 130 bestanden en weet niets over hun inhoud, dus haalt hij de volgende kaart op: 130 parallelle range-requests voor alleen de staart van elk bestand, waar de footer een raster van de inhoud bijhoudt, met een byte-offset en een min/max-waarde per row group per kolom. Dat raster beantwoordt twee vragen tegelijk, en geen van beide antwoorden verplaatst data. De eerste is welke kolommen ertoe doen: de query noemt er drie van de vijftig, dus alleen die drie worden ooit opgevraagd, en de andere 47, inclusief de brede blobs shipping_address en line_items, worden nooit netwerkverkeer. De tweede is welke stukken van die drie kolommen ertoe doen: de min/max-waarden sluiten de hoeken van elk bestand uit die buiten 1 augustus vallen, en een derde en laatste kaart, de page index, herhaalt die uitspraak op het niveau van pages. Het eerste antwoord heet projection, het tweede skipping; samen laten ze een paar honderd megabyte over.

De engine kijkt nu naar een precieze boodschappenlijst van byte-ranges, zonder iets anders gelezen te hebben dan kaarten. En alle kaarten volgen één regel: ze liggen boven het gebied dat ze beschrijven en zijn leesbaar zonder dat gebied aan te raken. De log beschrijft bestanden, de footer beschrijft row groups, de page index beschrijft pages. Begrijp de truc één keer en je begrijpt ze alle drie.

In het spel: 7,4 GB → ~310 MB, verdeeld over 3 kolommen van 130 bestanden

Hoe het blind wordt

Pruning heeft vier vijanden, allemaal zelf veroorzaakt.

Verspreiding. Statistieken kunnen alleen uitsluiten wat bij elkaar staat. Ongeclusterd geschreven beslaat elk bestand januari tot en met december, en alle drie de kaarten worden tegelijk blind.

Fragmentatie. Duizenden piepkleine bestanden, die elk kaartleeswerk kosten voordat hun data überhaupt bekeken wordt. Streaming ingest en frequente kleine writes produceren dit continu.

Om alles vragen. SELECT * schakelt projection per definitie uit, en een timestamp die als string is opgeslagen kan nauwelijks op bereik gepruned worden.

De ondoden. Met deletion vectors markeert een DELETE rijen alleen als dood. Geen enkele kaart kan een gemarkeerde rij uitsluiten, dus elke scan blijft ze lezen en weggooien totdat OPTIMIZE de bestanden fysiek herschrijft. Veel teams doen dat nooit.

Het bewijs: in het query profile het aantal gelezen bestanden tegenover het aantal gepruneerde bestanden. Een selectieve query die bijna elk bestand leest, is een blinde laag 1, en DESCRIBE DETAIL met duizenden bestanden is de fragmentatievariant.

Wie het onderhoudt

Liquid clustering houdt rijen bij elkaar op de sleutels die je kiest (CLUSTER BY, of CLUSTER BY AUTO om sleutels te leren uit de queryhistorie op managed tables met predictive optimization aan), OPTIMIZE voegt kleine bestanden samen, en predictive optimization plant beide in. De clusteringsleutel kiezen blijft een menselijke beslissing: de kolom waarop je queries echt filteren en joinen, en dat is kennis over je workload, niet over je data. Het schema en de SELECT-lijst zijn helemaal van jou.

Laag 2: Bytes verplaatsen

Pas nu wordt er iets zwaars verplaatst. De workers voeren de boodschappenlijst uit: één ranged GET per overgebleven byte-range, honderden tegelijk onderweg, één page per keer gedecomprimeerd. Tel wat van wat afhangt, in plaats van de requests zelf, en je ziet de architectuurhandtekening van een lakehouse: drie opeenvolgende golven, elk een zwerm parallelle requests die pas kan vertrekken als de vorige zwerm antwoord heeft gegeven. Eerst de log (welke bestanden), dan de footers (welke byte-ranges), dan de data. Drie gestapelde round-trips naar opslag op de schaal van een datacenter, of de tabel nu tien bestanden heeft of tienduizend.

Dat is de volle prijs, en hier zit het echte mechanisme van deze laag: hij zorgt dat je die nooit twee keer betaalt. Alles wat de golven opleverden wordt warm gehouden, zolang het cluster leeft en de cache ruimte heeft, en precies dat is de kleine lettertjes waar de blinde vlek hieronder over gaat. De log en de footers blijven in het geheugen van het cluster, en de 310 MB aan data-pages belanden in de disk cache, de eigen lokale opslag van het cluster, microseconden weg in plaats van milliseconden. Als het dashboard deze query over vijf minuten ververst, worden golf één en twee uit het geheugen beantwoord, golf drie uit lokale opslag, en wordt het netwerk helemaal niet geraadpleegd. Het onderscheid dat hier telt is niet snelle tegenover trage queries; het is koude tegenover warme reads, en de hele taak van deze laag is ervoor zorgen dat elke byte hooguit één keer op de dure manier wordt opgehaald.

De disk cache stapelt bovendien met de result cache uit laag 0 tot een hiërarchie van luiheid: de result cache slaat de query over, de disk cache slaat het netwerk over, en pruning verkleint wat geen van beide kon overslaan. De snelste byte is er een die je niet ophaalt; de op een na snelste is er een die je al hebt opgehaald.

Opgehaald: ~310 MB · één keer over het netwerk, ms · herhaald lezen uit lokale opslag, µs

Hoe het blind wordt

Een cache die te kort leeft: clusters die zo klein zijn, of zo agressief automatisch afsluiten, dat de cache nooit lang genoeg overleeft om voor iemand warm te zijn. Een cache is een investering, en een cluster dat elke tien minuten stopt, schrijft die investering steeds weer af. De andere klassieker is de reflexmatige .persist(): een handmatige override van caching die het platform al beter doet, waarbij een tweede kopie van wat de disk cache al heeft wordt opgeslagen in het geheugen dat de query nodig heeft om uit te voeren. Het enige wat hij kan cachen en de disk cache niet, is een duur berekend tussenresultaat dat binnen een job wordt hergebruikt, en zelfs dat schrijf je meestal beter naar een tijdelijke tabel.

Het bewijs: in het query profile de bytes gelezen uit cache die bijna op nul staan, voor een query die de hele dag draait.

Wie het onderhoudt

De disk cache is automatisch; niemand tunet hem, en dat is precies de bedoeling. Je hendels zijn indirect: compute uit een instancefamilie met "Delta cache accelerated" voor klassieke clusters (het label in de keuzelijst voor hardware met de lokale opslag die de cache nodig heeft), autoterminatievensters die passen bij hoe vaak de data echt opnieuw gelezen wordt, en serverless beide keuzes voor je laten maken.

Laag 3: Uitvoering

De pages worden gedecomprimeerd tot kolomvectoren: aaneengesloten arrays met de waarden van één kolom. Omdat de page index alleen pages kon uitsluiten die aantoonbaar geen rijen van 1 augustus bevatten, dragen de overgebleven pages nog wat buren van aangrenzende datums mee: zeg 25 miljoen gescande rijen om de 2 miljoen te vinden die matchen. Alles tot nu toe ging over data niet aanraken; deze laag moet eindelijk 25 miljoen waarden aanraken, en het hele vak past in één zin: houd de CPU gevoed, en houd hem bezig met echt werk.

Hem gevoed houden is de helft van de afspraak die bij de layout ligt. Een moderne CPU kan waarden veel sneller vergelijken dan het RAM ze kan aanleveren, dus de bottleneck is aanvoer, geen rekenwerk. Omdat een vector één aaneengesloten blok is, neemt elke opgehaalde cache line de volgende vijftien waarden gratis mee, en de prefetcher ziet een perfect voorspelbare stap en haalt alvast vooruit op, zodat de data al klaarstaat als de CPU erom vraagt. Geen wachten op geheugen, het lot dat rijgebaseerde engines treft die van object naar object pointers najagen.

Photon is de andere helft: een engine die gebouwd is om die aanvoer te verwerken zonder een cyclus te verspillen aan iets anders dan het werk. Zijn loops compileren naar SIMD-instructies die acht of zestien datums per cyclus vergelijken, zonder branches die verkeerd voorspeld kunnen worden; waar een kolom dictionary-encoded is, lopen gelijkheidschecks op de kleine integer-codes zonder ooit een string te decoderen; en de administratie die een klassieke engine per waarde betaalt (functieaanroepen, typechecks, null-checks) wordt één keer per batch van duizenden betaald. De layout levert aan, Photon verwerkt, en samen maken ze ons filter, waar de SQL nominaal over gaat, bijna de goedkoopste stap in het leven van de query: milliseconden per core. Elke worker telt daarna lokaal amount per klant op, en de query is één stap van klaar.

Onderweg: 25 M rijen gescand → 2 M matches → deelsommen per klant, per worker

Hoe het blind wordt

Eén regel Python in het hete pad maakt alles ongedaan. Photon kan je functie niet uitvoeren, dus vectoren worden weer opgebroken in rijen en naar een apart Python-proces gestuurd, waar een interpreter de functie per rij aanroept (nieuwere runtimes bundelen het versturen via Arrow, maar de interpretatie per rij blijft): elke kost die de gevectoriseerde loop moest vermijden, in één klap terug. Erger nog: de optimizer kan niet in de functie kijken, dus WHERE my_udf(order_date) = ... kan nooit min/max-pruning worden, en de UDF maakt ook de kaarten van laag 1 blind: de tabel van 480 GB wordt een volledige scan die een Python-loop voedt. De stillere versie van dezelfde wond: een expressie die Photon niet ondersteunt valt stil terug op klassieke JVM-Spark, correct maar niet gevectoriseerd. De voorkeursvolgorde ligt vast: SQL-expressies en ingebouwde functies eerst, pandas UDF's als tweede, Python per rij nooit.

Het bewijs: de Photon-dekking in het query profile, en de doorlooptijd die geconcentreerd zit in operators daarbuiten.

Wie het onderhoudt

Photon staat standaard aan op SQL warehouses en serverless. Er valt niets te tunen, alleen dingen die je niet in de weg moet leggen.

Laag 4: Het geheugen verlaten

Alles tot nu toe gebeurde zonder dat één rij de worker verliet die hem las: pruning koos de bytes, het ophalen bracht ze naar lokaal RAM, en Photon filterde en telde ze daar meteen op. GROUP BY customer_id maakt een einde aan dat comfort. De aankopen van klant 42 op 1 augustus zitten in de bestanden waar de writes toevallig terechtkwamen, dus in het geheugen van verschillende workers, en een totaal kan alleen worden berekend waar de rijen samenkomen. Rijen moeten dus het geheugen verlaten, en het goedkope deel van de query is voorbij. De geplande uitgang is de shuffle: elke worker wisselt rijen uit met elke andere, zodat elke sleutel op precies één plek terechtkomt. Wat eerst een geheugenkopie binnen één machine was, wordt een netwerkoverdracht tussen machines, 10 tot 100 keer trager dan RAM.

smal: filter, project, transformworker 1worker 2worker 3geen uitwisselingresultaat 1resultaat 2resultaat 3elke worker heeft genoeg aan zijn eigen databreed: group by key, joinworker 1worker 2worker 3keys a–hkeys i–qkeys r–zgekleurde lijnen zijn rijen over het netwerk

Smalle operaties houden elke worker onafhankelijk; brede operaties verdelen rijen opnieuw per sleutel, en de kruisende lijnen zijn echte data over de lijn. Het meeste querytuningwerk is een poging om het rechterpaneel kleiner te maken of te laten verdwijnen.

De engine doet hard zijn best om die uitgang klein te houden, en voor onze query lukt dat spectaculair: omdat elke worker in laag 3 al vooraf heeft geaggregeerd, gaan er deelsommen per klant over het netwerk, een paar megabyte, in plaats van 2 miljoen ruwe rijen. Hetzelfde instinct zit achter de andere grote maatregelen: een join met een kleine tabel wordt gebroadcast, zodat de grote kant nooit beweegt, en AQE past het plan halverwege de query aan op basis van gemeten groottes in plaats van schattingen.

Over de lijn: ~2 MB aan deelsommen, niet 2 M ruwe rijen → 20 K resultaatrijen naar de client

Er is ook een tweede, ongeplande uitgang uit het geheugen: spill, als de werkset van een worker groter wordt dan het RAM en overloopt naar lokale schijf. Dat gebeurt meestal precies hier, omdat de shuffle werksets samenbrengt: alle half miljoen rijen van klant NULL die op één ongelukkige worker belanden. In de Spark UI lopen de twee uitgangen door elkaar, maar houd de diagnose gescheiden: shuffle is de netwerkuitgang, kleiner te maken met broadcast-hints, pre-aggregatie en layout; spill is de schijfuitgang, op te lossen met meer geheugen per taak of door de scheve sleutel te repareren.

Hoe het blind wordt

Twee klassiekers. Een handmatig ingestelde spark.sql.shuffle.partitions uit 2021 legt de datavolumes van dat jaar vast en beperkt AQE met verouderde getallen. En de scheve sleutel die je net tegenkwam: de query is zo traag als zijn slechtste sleutel.

Het bewijs: shuffle- en spill-bytes in de Spark UI, en een stage waarvan de traagste taak vele malen langer duurt dan de mediaan; dat verschil is de scheve sleutel.

Wie het onderhoudt

AQE staat standaard aan en regelt in de meeste gevallen de partitiegrootte en het opsplitsen van scheve sleutels; serverless haalt bijna alle knoppen voor geheugengrootte weg. Je overgebleven hendels zijn de hendels die kennis van je workload vereisen: broadcast-hints als de planner het verkeerd inschat, vooraf aggregeren voor een join, NULL-zware sleutels bij de bron repareren, en tabellen indelen op de sleutels waarop ze gewoonlijk gejoind worden, waardoor de shuffle al kleiner wordt voordat hij begint.

Thuis

De query is thuis: twintigduizend resultaatrijen op het dashboard, seconden na SELECT, na 0,06% van de tabel te hebben gelezen, een paar megabyte te hebben geshuffled, en het netwerk voor elke byte precies één keer te hebben betaald. Niets daarvan was geluk: elke snelle stap was een mechanisme dat iemand had ingeschakeld.

De reis: 480 GB in het spel → 310 MB opgehaald → ~2 MB over de lijn → 20 K rijen thuis

De menselijke laag

Zo blijft het niet vanzelf. Elke laag die de query net doorliep, wordt tussen queries scherp gehouden of juist bot: bestanden fragmenteren als er writes binnenkomen, verwijderde rijen blijven liggen, en niets daarvan verschijnt ooit als foutmelding. Elke query blijft het juiste antwoord geven, alleen elke maand een beetje trager.

Onderhoud is het deel van het platform. Op managed tables in Unity Catalog plant predictive optimization OPTIMIZE, VACUUM en het verzamelen van statistieken in op basis van het waargenomen gebruik, het beste argument voor managed tables; op external tables is die agenda van jou, en DESCRIBE HISTORY laat zien wanneer ze voor het laatst echt draaiden (VACUUM verschijnt alleen waar vacuum-logging aanstaat), wat op een verwaarloosde tabel de hele diagnose is. En het deel van het platform groeit: de handmatige partitioneringsschema's, de persist-aanroepen, de getunede shuffle-configs; het platform draait aan die knoppen beter dan wij en stelt ze opnieuw bij als de data verschuift, iets wat wij nooit deden.

Wat het niet kan, is je workload kennen. De beslissingen die nog steeds mensenhanden verdienen, zijn precies de beslissingen die kennis vereisen die de optimizer niet heeft:

  • De query zelf. Lees de SQL of PySpark opnieuw voordat je infrastructuur aanraakt. Noem de kolommen in plaats van SELECT *, filter zo vroeg mogelijk op de geclusterde sleutels, vervang UDF's door ingebouwde expressies, aggregeer voor het joinen. Een herschrijving kost niets, gaat direct live, en lost meer trage queries op dan welke clusterwijziging ook, omdat veel van de blindheid in dit artikel in de eerste plaats door de querytekst werd veroorzaakt.
  • Layout. Op welke kolommen je queries filteren en joinen (clusteringsleutels), wat één rij moet betekenen (grain), en of die veelgebruikte dimensie in de feitentabel gedenormaliseerd moet worden. Beslissingen bij het schrijven die elke pruninglaag tegelijk scherp maken, of blind.
  • Joinstrategie. Het plan herkennen dat had moeten broadcasten, de sleutel die scheef is, de aggregatie die voor de join had moeten gebeuren.
  • Diagnose. De Spark UI goed genoeg kunnen lezen om te bewijzen in welke laag een trage query vastzit voordat je iets aanraakt: gescande bytes wijzen naar laag 1, spill naar laag 4, een heet Python-proces naar laag 3, en het bewijs per laag hierboven vertelt je waar je moet kijken voordat je iets verandert.

Het platform zal steeds meer werk uit handen nemen, en dat is goed: het draait aan de knoppen beter dan wij. Maar het oordeel kan het niet overnemen. De lastigste pijnpunten zijn geen instellingen maar keuzes: waarop je clustert, wat een rij betekent, en of een vraag überhaupt gesteld moet worden. Of een query een goed leven krijgt, wordt bepaald door de mensen die eigenaar zijn van de tabellen waar hij doorheen reist. Nu ken je de route.

De woordenlijst

De anatomie achter het verhaal, op alfabetische volgorde. Per term: wat het is, en waarom het voor de reis uitmaakt.

AQE (adaptive query execution)

De engine past een query halverwege opnieuw aan op basis van de groottes die hij echt heeft gemeten, en kiest het aantal shuffle-partities en de joinstrategie op basis van de werkelijkheid in plaats van schattingen. Daarom zijn handmatig ingestelde shuffle-configs meestal slechter dan helemaal geen config.

Broadcast join

Als één kant van een join klein is, kopieert de engine die volledig naar elke worker, zodat de grote kant nooit over het netwerk hoeft en een volledige shuffle verandert in een lokale lookup. Een hint (/*+ BROADCAST(t) */) dwingt dit af als de planner het verkeerd inschat.

Column chunk

Alle waarden van één kolom voor één row group, aaneengesloten opgeslagen. De eenheid waarop projection werkt: een kolom die de query niet noemt, is een chunk die nooit wordt opgehaald.

Delta table / transaction log

Een Delta table is een map met gewone, onveranderlijke Parquet-bestanden plus de transaction log: een reeks kleine JSON-commits ("voeg deze bestanden toe, verwijder die"), periodiek samengevoegd in checkpoints, waarvan het afspelen de huidige tabel bepaalt. De log bewaart ook min/max-statistieken per bestand, en dat maakt pruning op bestandsniveau bijna gratis.

Deletion vector

Een kleine bitmap die rijen in een bestand als verwijderd markeert, zonder het bestand te herschrijven. Scans blijven de dode rijen lezen en wegfilteren totdat OPTIMIZE of REORG de bestanden fysiek herschrijft, en de bytes verdwijnen pas uit de opslag na een latere VACUUM.

Disk cache

Workers bewaren automatisch kopieën van de Parquet-data die ze uit cloudopslag halen op hun lokale SSD's, in een gedecodeerd formaat dat sneller opnieuw te lezen is dan het origineel. Herhaalde reads van warme data slaan het netwerk volledig over; daarom is de tweede run van een query vaak meerdere keren sneller dan de eerste, en is .persist() op ruwe tabeldata meestal overbodig.

Footer

De inhoudsopgave die Parquet aan het einde van elk bestand bijhoudt: het schema plus, per row group per kolom, byte-offsets, groottes en min/max-statistieken. Het bestand eindigt met de lengte van de footer gevolgd door het 4-byte magic number van het formaat, dus lezers beginnen bij de staart; engines halen speculatief de laatste tientallen kilobytes op (de standaard van Arrow is 64 KB) in één request, omdat round-trips in cloudopslag meer kosten dan bytes.

Liquid clustering

Het mechanisme van Delta om rijen met vergelijkbare sleutelwaarden fysiek bij elkaar te houden, incrementeel bijgehouden terwijl data binnenkomt, met sleutels die later te wijzigen zijn. Clustering gebeurt op drie momenten: direct tijdens grote writes, incrementeel als OPTIMIZE draait (handmatig of ingepland door predictive optimization), en volledig met OPTIMIZE FULL na een sleutelwijziging. Die nabijheid is wat elke min/max-statistiek de kracht geeft om uit te sluiten, op alle drie de kaartniveaus tegelijk.

Materialized view

Een vooraf berekend queryresultaat dat als tabel wordt opgeslagen en ververst op verzoek, volgens een schedule, of (door te kiezen voor TRIGGER ON UPDATE) wanneer de bronnen veranderen, incrementeel waar de engine dat kan. Gebruikers lezen het antwoord in plaats van het opnieuw te berekenen.

OPTIMIZE

De onderhoudsherschrijving. Hij selecteert bestanden waarvan de fysieke vorm is verslechterd en schrijft ze opnieuw in één atomische log-commit; een bestand komt op drie manieren in aanmerking: te klein (samengevoegd met zijn buren), ongeclusterd (rijen opnieuw geordend op de clusteringsleutels van de tabel), of met genoeg door deletion vectors gemarkeerde rijen om op te schonen. Eén run kan alle drie tegelijk oplossen, en gezonde bestanden worden overgeslagen, waardoor routineruns goedkoop blijven. Oude bestanden worden alleen logisch verwijderd; de bytes wachten op VACUUM.

Page / page index

Een page (ongeveer een megabyte) is een intern segment van een column chunk en de kleinste eenheid van compressie: één waarde lezen betekent de hele page decomprimeren, en nooit meer. De page index dupliceert min/max-statistieken per page vlak bij de footer, zodat pages kunnen worden overgeslagen zonder ze aan te raken.

pandas UDF

Een Python-UDF die hele kolommen ontvangt als pandas/Arrow-arrays in plaats van rij voor rij, zodat data in kolombatches de procesgrens oversteekt en de functie gevectoriseerde NumPy-bewerkingen kan uitvoeren. Dat heelt twee van de drie wonden van een gewone UDF, de serialisatie en de interpretatie per rij, maar niet de derde: de uitvoering verlaat nog steeds Photon, en de optimizer kan nog steeds niet in de functie kijken, dus pruning blijft blind. Het juiste gereedschap als Python echt onvermijdelijk is.

Photon

De gevectoriseerde execution engine van Databricks. Het doel is maximaal nuttig werk per CPU-cyclus, nagestreefd vanaf twee kanten tegelijk: hij genereert de instructies (SIMD-vergelijkingen van veel waarden per cyclus, in strakke loops met weinig branches, met de administratie één keer per batch in plaats van per rij) en hij bepaalt de aanvoer (data blijft in aaneengesloten kolomvectoren van decompressie tot aggregatie, het enige geheugenpatroon dat een CPU-prefetcher volledig kan benutten). De twee kanten hebben elkaar nodig, en daarom verlies je beide tegelijk als je Photon verlaat, vooral via een Python-UDF.

Predictive optimization

Databricks plant OPTIMIZE, VACUUM en ANALYZE automatisch in voor managed tables in Unity Catalog, op basis van het waargenomen gebruik. Op external tables is die agenda van jou.

Result cache

Databricks SQL geeft het opgeslagen resultaat van een eerder uitgevoerde query terug als die herhaald wordt op ongewijzigde data, zonder iets uit te voeren. Op serverless warehouses leeft deze cache op workspaceniveau en overleeft hij herstarts.

Row group

Een horizontale plak van een Parquet-bestand, bepaald door bytes en niet door rijen (standaard 128 MB, van een paar honderdduizend tot een paar miljoen rijen afhankelijk van de rijbreedte), waarbinnen data kolom voor kolom is opgeslagen. De eenheid van parallellisme tussen workers en van overslaan op basis van statistieken binnen een bestand.

Shuffle

De uitwisseling van iedereen naar iedereen die rijen per sleutel opnieuw verdeelt over workers, zodat een join of aggregatie alle rijen voor een sleutel op één plek vindt. Elke worker verdeelt zijn output in lokale shuffle-bestanden; de volgende stage haalt elke bucket op bij elke worker. De duurste verplaatsing in een gedistribueerde query.

Spill

De werkset in het geheugen van een worker (sorteerbuffers, hashtabellen voor aggregatie) die groter wordt dan de toegewezen ruimte en overloopt naar lokale schijf, bewust en per operator, en zichtbaar in de Spark UI in plaats van ongemerkt. Het klassieke symptoom van scheve sleutels of te weinig geheugen.

Vector

De verwerkingseenheid van een engine: een aaneengesloten array met de waarden van één kolom, meestal een paar duizend rijen, plus een validity bitmap voor nulls. Het woord heeft in dit vakgebied twee schalen: deze engine-vectoren, en de hardwarevectoren ter grootte van een register (4 tot 16 waarden) die één SIMD-instructie verwerkt; de engine stuurt zijn grote vectoren door de kleine vectoren van de CPU. Dit artikel gebruikt overal de betekenis van de engine.