Skip to content
About
Main content

Where parallelism comes from and what bounds it

Why does adding cores stop helping, and what is the ceiling for data, pipeline and task parallelism?

How is work divided, to whom, and when?

The first three articles asked what a boundary between costs. Parallelism asks a different question, with a different currency:

The costs are no longer CPU cycles. They are idle cores and wasted work.

The new constraint is about information, and it is what makes this topic hard: you cannot know how long a piece of work will take before you run it. The only forecast available is the planner's estimate of how many rows each step will produce. That estimate is wrong by some factor, and its error multiplies with every join in the plan (a 2× error at each of five joins can become a 32× error at the top). Every mechanism in this article is one of three things:

  • a bet placed on the forecast (static assignment),
  • a correction applied after observing reality (dynamic assignment), or
  • a hedge that pays for insurance with duplicated work ().

There is no fourth option, because there is no fourth source of information.

The constraint that stops mattering is the cost of one boundary. At cluster scale, handing a task to a worker costs about a millisecond. The question is no longer "is the handoff cheap?" but "was anyone waiting?"


The one bound, in three costumes

There are three well-known kinds of parallelism: data, pipeline and . They are three ways of finding parallelism, and each one runs into the same ceiling with a different denominator. If we state the ceiling once and then apply it three times, three vague warnings become three numbers you can compute.

The work-span bound. Let T₁ be the total work (the time one worker would need to do everything). Let T∞ be the span, also called the : the time needed along the longest chain of steps that depend on each other, even if you had unlimited workers. With p workers, no schedule can beat T₁/p (the work must be shared), and none can beat T∞ (the chain must be run in order). So:

total work in core-seconds
the longest chain of dependent steps
workers
Data table
Lower bound on elapsed time by workers (T₁ = 1,000 core-s, T∞ = 50 s)
Workers pElapsed time ≥
11,000 s
1.09917 s
1.19841 s
1.3772 s
1.41708 s
1.54649 s
1.68596 s
1.83546 s
2501 s
2.18460 s
2.37422 s
2.59387 s
2.82355 s
3.07325 s
3.35299 s
3.65274 s
3.98251 s
4.34230 s
4.73211 s
5.16194 s
5.62178 s
6.13163 s
6.68150 s
7.29137 s
7.94126 s
8.66115 s
9.44106 s
10.397.2 s
11.289.1 s
12.281.8 s
13.375 s
14.568.8 s
15.863.1 s
17.357.9 s
18.853.1 s
20.550 s
22.450 s
24.450 s
26.650 s
2950 s
31.650 s
34.550 s
37.650 s
4150 s
44.750 s
48.750 s
53.150 s
57.950 s
63.150 s
68.850 s
7550 s
81.850 s
89.150 s
97.250 s
10650 s
11550 s
12650 s
13750 s
15050 s
16350 s
17850 s
19450 s
21150 s
23050 s
25150 s
27450 s
29950 s
32550 s
35550 s
38750 s
42250 s
46050 s
50150 s
54650 s
59650 s
64950 s
70850 s
77250 s
84150 s
91750 s
1K50 s

Example. A job has T₁ = 1,000 core-seconds and its longest chain takes T∞ = 50 seconds. With 10 workers the bound is max(100, 50) = 100 s. With 100 workers it is max(10, 50) = 50 s. With 1,000 workers it is still 50 s. Past 20 workers (1,000 ÷ 50) more cores buy nothing.

Amdahl's law (which says that the part of a program that must run serially limits the speedup of the whole) is this bound in a special case: the "serial fraction" is a critical path that happens to be one unsplittable block of code. The span form is more useful, because in a real query the unbreakable chain is usually not a section of serial code. It is a big , a slow stage, or a long path through the plan.

SourceWhere the parallelism comes fromThe unbreakable chainComputable ceiling
The same operator on disjoint input partitionsThe largest partitionSpeedup ≤ 1/f, where f is the largest partition's share of the work
Different operators, on different data, at the same timeThe slowest stage, repeated once per item ≤ 1/max(stage time); speedup ≤ pipeline depth d
Task parallelismIndependent subtrees of the planThe critical path through the plan's dependency graphSpeedup ≤ T₁/T∞

Data parallelism, and the number that should govern your intuition

What it is. Split the input into partitions, run a copy of the operator on each partition, then combine the results. Example: sum kwh per tariff_zone by giving each of 640 cores a slice of readings. The mechanism is simple. Its ceiling is not.

Elapsed time equals the time of the largest partition. Ideal time is total work divided by p. So speedup is capped at (total work) ÷ (largest partition) = , no matter how many cores you buy.

Real distributions. Key frequencies in real data are roughly Zipfian: a few keys are very common and most keys are rare. For a with exponent 1, the top key's share is 1/H_n, where H_n is the n-th harmonic number, approximately ln n + 0.577 for n distinct keys.

  • With a million distinct keys: H_n ≈ ln(1,000,000) + 0.577 = 13.8 + 0.577 ≈ 14.4. The top key holds 1/14.4 ≈ 7% of the rows, and the maximum data-parallel speedup is about 14.
  • With a billion distinct keys: H_n ≈ 20.7 + 0.577 ≈ 21.3. The ceiling rises to about 21.
distinct keys
1 + 1/2 + 1/3 + … + 1/K
Data table
Best possible speedup of a Zipf (α ≈ 1) join, by distinct keys
Distinct keys KSpeedup ceiling
102.88×
12.63.11×
15.83.34×
203.57×
25.13.8×
31.64.03×
39.84.26×
50.14.49×
63.14.72×
79.44.95×
1005.18×
1265.41×
1585.64×
2005.87×
2516.1×
3166.33×
3986.56×
5016.79×
6317.02×
7947.25×
1K7.48×
1.26K7.72×
1.58K7.95×
2K8.18×
2.51K8.41×
3.16K8.64×
3.98K8.87×
5.01K9.1×
6.31K9.33×
7.94K9.56×
10K9.79×
12.6K10×
15.8K10.2×
20K10.5×
25.1K10.7×
31.6K10.9×
39.8K11.2×
50.1K11.4×
63.1K11.6×
79.4K11.9×
100K12.1×
126K12.3×
158K12.6×
200K12.8×
251K13×
316K13.2×
398K13.5×
501K13.7×
631K13.9×
794K14.2×
1M14.4×
1.26M14.6×
1.58M14.9×
2M15.1×
2.51M15.3×
3.16M15.5×
3.98M15.8×
5.01M16×
6.31M16.2×
7.94M16.5×
10M16.7×
12.6M16.9×
15.8M17.2×
20M17.4×
25.1M17.6×
31.6M17.8×
39.8M18.1×
50.1M18.3×
63.1M18.5×
79.4M18.8×
100M19×
126M19.2×
158M19.5×
200M19.7×
251M19.9×
316M20.1×
398M20.4×
501M20.6×
631M20.8×
794M21.1×
1B21.3×

The ceiling grows as the logarithm of the number of distinct keys. A thousand times more keys buys only 1.5× more speedup. In other words, it barely grows at all.

This is the sharpest case of the across the platform. Every partitioner assumes that keys are spread evenly. That assumption is false in a specific, measurable way, and the falsehood turns directly into a closed-form ceiling on the very thing you bought the cluster for.


Pipeline parallelism

What it is. Overlap stages. While stage 2 works on item i, stage 1 already produces item i + 1. Like a car wash: while car 1 is being dried, car 2 is being rinsed and car 3 is being soaped. This is the only form of parallelism available when the operators cannot be partitioned. It is also the only one that reduces time to the first result rather than time to the last result.

Two ceilings, both tight

  • Depth. With d stages you get at most d× speedup. That is a small number, because real plans have only single-digit stages per pipeline.
  • Balance. Steady-state throughput is 1/max(stage time). Example: three stages take 1 ms, 2 ms and 1 ms per item. Throughput is 1 item per 2 ms, not 1 per 1 ms. The pipeline wastes half of the fast stages' time, whatever its depth. Unlike data parallelism, you cannot fix this by adding workers to the slow stage unless that stage is also data-parallel. That is why real engines always combine pipeline parallelism with data parallelism rather than choosing one.

The cost is a buffer between every pair of stages. Its required depth comes from : to keep a consumer busy at rate λ across a producer W, you need λ × W items in flight. Example: a consumer takes 50,000 batches per second and the producer's latency is 2 ms: 50,000 × 0.002 = 100 batches must be in flight. Memory for buffers is therefore (buffer depth) × (item size) × (number of edges). If the buffer is too small, the pipeline serializes. If it is too large, you have spent memory to hide variance that you could have reduced.

Stages come from pipeline breakers. Pipeline parallelism requires the upstream operators to be non-blocking. A (a sort, a hash-join build, an aggregation without pre-aggregation) consumes its whole input before it produces anything, so it ends the pipeline by definition. Stages are not a design choice. They are the intervals between pipeline breakers. This is the prerequisite for the whole discussion: you cannot say how many stages overlap until you know where one stage ends.


Task parallelism

What it is. Independent subtrees of the plan run at the same time: two scans feeding a join, two branches of a UNION, or unrelated pipelines in one query. The ceiling is T₁/T∞, total work divided by the critical path. So task parallelism is a property of the shape of the plan, not of the data.

  • A left-deep join tree (each join takes the previous result plus one new table, so the joins form a chain) has a critical path almost equal to its total work. It has almost no task parallelism.
  • A bushy tree (several joins can run independently and meet later) has much more.

This is a real reason to prefer . The planner's cost model minimizes total work, so it will not tell you about this. It is the one place where a plan choice is made for a purely runtime reason.

Memory, not cores, is the real constraint. Running k subtrees at once means k operators' worth of state is in memory at the same time. So concurrency is bounded by:

Example: a worker has 48 GB for query state and each join build side needs 6 GB. At most 8 such operators can be active together, even if there are 16 cores. (How memory is granted, reserved and spilled is the job of memory management and is not covered here. We use only the bound.) The bound also explains a difference between engine designs. Pipelined engines (massively parallel processing engines that stream every edge between stages) have a concurrency limit set by the widest point of their plans, because streaming every edge needs every stage resident at once. Materialized-DAG engines (engines that write each stage's output out before the next stage starts) do not have this limit.


Intra- versus inter-operator parallelism, and the algebraic gate

The two terms are a re-cut of the three kinds above:

  • Intra-operator parallelism is data parallelism inside one operator.
  • Inter-operator parallelism is pipeline plus task parallelism across operators.

We keep the distinction because they have different preconditions, and the precondition for is not physical. It is algebraic.

An operator can be parallelized over partitions if and only if it can be written as:

That is a statement about the operator's algebra, so whether an operator can be parallelized is decided by mathematics (the principle algebra gates the topology: only reductions can be combined in parts or in trees), not by engineering effort.

OperatorDecomposable?Why / how it is done
sum, count, min, max (any distributive aggregate)YesAssociative. Local-then-merge works and the merge is cheap.
averageNot directlyIts components are decomposable, so every engine keeps it as a pair (sum, count).
exact median, exact distinct countNo cheap mergeParallel versions are a full sort, a two-pass algorithm, or an approximation. Sketch structures (small summaries such as HyperLogLog for distinct counts) exist precisely to restore the missing algebra.
global sortNot without helpNeeds either a data-dependent range partition (which needs a sampling pass, because you must know the distribution to pick the boundaries) or a merge tree.
window functionsOnly along the PARTITION BY keyNot at all when there is no such key.

The operator's algebra determines which parallelism is legal. The runtime only determines how well it is used. So the question "how do I parallelize this operator?" is answered by the operator's design, before anything here applies.

Parallelism as a plan node. The second insight is structural. Parallelism can be expressed as a plan node. An sits between two stages. Every other operator stays unaware of parallelism: it sees a stream of and does not know whether it is one instance of one or one of a thousand. That separation is what makes parallel query execution manageable at all. It is also why the gets its own article: the exchange is the only place where parallelism lives, so all its cost concentrates there. The shuffle covers the mechanics.