Skip to content
About
Main content

Skew and elasticity

What do you do when one piece of work is simply too big, and when does adding machines mid-query pay off?

The first step: classify the long tail

All three failure modes (imbalance, , ) produce the same picture: a long at the end of the stage. The fix for one is useless against the others. So diagnose before you fix. Plot per-task duration (y axis) against per-task input bytes (x axis):

SignatureCauseFixFix that will not work
Bytes similar, durations vary randomlyImbalance or environment noiseFiner granularity, dynamic assignment, Splitting
Bytes similar, one task far slowerStraggler: bad node, contention, cold Speculative duplicateSplitting partitions
Duration proportional to bytes, bytes very unequalSkewSplit the unit; replicate the other sideSpeculation: the duplicate is equally large
One long tail, three causesschematic

That scatter plot is the entire triage. It is cheap to produce from per-task counters, and it can be shown wrong by data (it is falsifiable): if the points do not line up as predicted, your diagnosis is wrong.


Salting

Salting adds a random suffix to a hot key so that it becomes k sub-keys spread over k tasks. For an aggregation it is legal when the reduction is , and it needs a second merge pass. For a join, every sub-key must still meet all the matching rows of the other side, so that side's rows are replicated k times. Either way it requires knowing in advance which keys are hot. How a join survives a hot key, and the Zipf arithmetic behind how big the hottest key gets, are query processing's; routing a key that outweighs a partition is data movement's.


Adaptive partition splitting

This does the same thing without foresight. After the producing stage finishes, the engine reads the actual per-partition output sizes and assigns several consumer tasks to byte ranges of one oversized partition.

It has a hard dependency on the file format. The partition must be range-readable: a consumer must be able to read "bytes 0 to 100 MB of partition 7". This is true with sorted, consolidated shuffle files (The shuffle) and not true with arbitrary per-pair streams. A runtime capability is determined by a storage-format decision made one article later. This is the tightest coupling here, no mechanism can correct an imbalance smaller than the unit it can address, and here that unit is set by the file format.


Heavy-hitter separation

This splits the plan rather than the data.

  1. Detect the hot keys with a sketch (a small summary structure, here a count-min sketch or heavy-hitter counter, built for this job).
  2. Route the hot keys down a or replicated path, where no shuffle is needed.
  3. Send the long tail of ordinary keys down the normal hash path.

Two execution strategies for two distributions, in one query. It is the right response to a bimodal input (data that has two very different populations), and structurally the same idea as a hybrid join.


Why join skew is worse than it looks

For a key with L rows on one side and R on the other, the join produces L × R rows, so join skew is the product of the two sides' skew: a key 10× oversized on both sides is 100× the fair share of work (two-sided skew). Input skew measured on either side alone understates it.


Who owns what

Detecting skew from runtime statistics and rewriting the plan in response belongs to the planner's adaptive execution. The runtime machinery belongs to this mechanism: splitting a partition across consumers, replicating the other side, and arbitrating the commit. Choosing a partition key that does not skew is a data-design and planning decision. The bill for a bad key arrives here.


Elastic parallelism

Two facts motivate elasticity: the right is not known until the data is seen, and capacity has a price that changes with time. Two requirements make it possible, and one piece of arithmetic makes it mostly useless inside a single query.

Requirement 1: work must be reassignable. This is trivial with a queue, easy with partition-level tasks over , and hard with stateful whose state must be moved to new workers. (Rescaling of stateful operators in a continuous stream is another topic; here we mean only queries with a beginning and an end.)

Requirement 2: partition count must be independent of worker count. This forces the design that everyone converged on: over-partition. Choose p partitions well above the number of workers, so adding a worker means handing out more partitions, not re-dividing anything.

The price of over-partitioning is set by the per operation. If dispatching a task costs F ≈ 10 ms and you want at most 1% overhead, the minimum task duration is F ÷ 0.01 = 1 second. So the number of partitions cannot exceed (total work) ÷ (1 s). That gives a hard ceiling on elasticity:

The arithmetic that should temper expectations. Scaling out during a query pays only if the remaining work saved is more than the cost of getting new workers. Going from p to p′ workers saves remaining/p − remaining/p′, and costs a startup S (provisioning, cold caches, no data locality).

core-seconds still to do
workers before and after
provisioning, cold caches, no data locality

That makes it useless for the queries people most want to rescue (the ones already three-quarters done and running late). It is useful only for long queries where the need is detected early.

So in practice elasticity lives between queries, not inside one. A warehouse that scales up because a queue of queries formed applies the same mechanism where the remaining work is large and the acquisition latency is spread over many queries. That is autoscaling at the level of a whole workload. The honest conclusion for query runtimes is that intra-query elasticity is a narrow win, not a general one.