0%
Big Data Engines: Spark, Lakehouse, and Streaming at Scale
The Shape of the Problem
Distributed Execution
Spark
Streaming at Scale
Pipelines in Production
Warehouse Engines, Snowflake and BigQuery
Every modern cloud warehouse is built on one structural decision, and almost everything else follows from it. Storage and compute are separate services that talk over a network.
This sounds like an implementation detail. It is not. It changes what you can buy, what you can tune, what fails, and what you are billed for. Understanding the consequences of that one split is most of what it takes to operate these engines well.
The three layers
It helps to name the layers explicitly, because vendors use different words for the same things.
The storage layer holds table data in a columnar format in object storage. It is durable, effectively unbounded, replicated by the cloud provider, and priced per terabyte per month. It has no opinion about queries. In Snowflake this is internal storage holding micro partitions. In BigQuery it is the managed storage service holding Capacitor files.
The compute layer runs queries. It is a set of worker processes with local memory and local disk, reading from the storage layer over the network and writing results back. It is priced by time or by bytes, it can be resized, and it holds nothing permanently. In Snowflake this is a virtual warehouse. In BigQuery it is a pool of slots.
The service layer sits above both. It holds metadata, authenticates users, parses and optimises queries, tracks transactions, manages the result cache, and hands work to compute. It is operated by the vendor, you cannot size it, and it is a shared multi tenant service. Most of the behaviour people find surprising in these engines comes from this layer.
What decoupling actually changes
Three things become possible that were not possible in a shipped appliance or a cluster you ran yourself.
Compute scales without moving data. Doubling query capacity is a configuration change, not a redistribution. In a shared nothing database, adding a node means rebalancing data across nodes, which takes hours and degrades the cluster while it happens. Here it takes seconds, because the new workers read the same object storage the old ones did.
Compute can be zero. A warehouse with no queries running can hold no compute at all and cost nothing beyond storage. This is the single largest practical difference from an always on cluster, and it is why the economics work for intermittent workloads.
Many computes can share one dataset. Several independent compute pools can read the same tables at the same time without coordinating, because none of them owns the data. A reporting workload and a data science workload can be isolated from each other completely while querying identical tables, which is impossible when the nodes holding the data are also the nodes running the queries.
The price of a remote read
None of that is free, and the cost is latency.
A local disk read is measured in tens of microseconds. An object storage read is measured in tens of milliseconds, which is roughly a thousand times longer. No amount of engineering removes that gap, so these engines are built to hide it rather than close it.
They hide it in four ways. They read in large blocks, so a single request returns megabytes rather than kilobytes. They read many blocks concurrently, so throughput comes from parallelism rather than from latency. They cache aggressively on local disk, so a repeated read of the same data may not touch object storage at all. And they prune, so the majority of blocks are never requested in the first place.
The consequence is a performance profile that surprises people coming from transactional databases. These engines are excellent at scanning a hundred gigabytes and terrible at fetching one row by key. A query that touches a single record can take half a second, which is a thousand times slower than the same lookup in a transactional store and perfectly normal here.
Why compute is disposable
Because compute holds nothing permanent, it can be destroyed and recreated freely. This has three operational consequences worth internalising.
A restart loses only cache. Resizing, suspending or replacing a compute pool costs you warm local disk and nothing else. There is no data to migrate and no state to drain. The first queries after a restart are slower because the cache is cold, and that is the whole penalty.
Failures are retried rather than repaired. If a worker dies mid query, the engine can hand its share of the work to another worker, because the input is in object storage and is still there. You do not get this property in an engine where the dead node held the only copy of a partition.
Capacity decisions become reversible. Choosing the wrong size is cheap, because changing it is a single operation with no migration. This should make you more willing to experiment with sizing than you would be with a cluster you had to rebuild.
The metadata service is the quiet dependency
The layer nobody sizes is the layer that most often explains an unexplainable slowdown.
Metadata operations are centralised. Listing partitions, resolving a view, checking permissions, taking a transaction snapshot and recording a commit all happen in the service layer, which is shared with other customers. When a query takes four seconds to return a trivial result, the time is usually here rather than in compute.
This is also where concurrency limits on data definition statements come from. Two sessions altering the same table serialise on a metadata lock, and a pipeline that issues thousands of small writes to one table spends most of its time waiting for that lock rather than writing bytes. The fix is always to batch the writes, never to add compute.
What separation does not buy
It is worth being clear about the limits, because the architecture is often oversold.
It does not make a badly written query fast. Scanning a table with no filter reads the whole table whatever the architecture.
It does not make small queries cheap in latency terms. Fixed overhead per query is in the hundreds of milliseconds, so a workload of many tiny queries is a poor fit regardless of size.
It does not remove the need to lay data out well. Pruning is the dominant performance lever, and pruning depends entirely on physical layout, which is discussed later in this lesson.
And it does not prevent cost surprises. In fact it makes them easier, because the thing that used to be a capital purchase you argued about for a quarter is now a dropdown anybody can change.
Reading the architecture before the bill
A useful habit when you first meet one of these platforms is to answer four questions before looking at a single query.
Where is the data, and who pays for it. Is it in the vendor's managed storage, priced per terabyte, or in your own object storage read through an external table, priced by your cloud bill plus a read fee.
What is the unit of compute, and when does it accrue. Per second of a running pool, or per byte scanned, or per reserved capacity slot regardless of use.
Who can create compute. If any analyst can start a large pool, your spend is bounded by politeness rather than by policy.
What the per query floor is. Every platform has a minimum billable unit, and knowing it tells you immediately whether a workload of small frequent queries will be economic.
Where the two major engines diverge
The architecture described here is shared. The differences are in how compute is presented and billed, and they are large enough to change design decisions.
One model gives you named, sized, explicitly managed compute pools that you start, stop and resize. You decide how much parallelism exists and you pay for the time it is running whether queries arrive or not.
The other model hides the pool entirely and bills by the data a query reads, with an option to reserve capacity instead. You never size anything, and you control cost by controlling how much data each query touches.
Neither is better. They put the lever in different places, and the rest of this lesson is about which lever is which.