External Sorting
When the data dwarfs main memory, the cost that matters is no longer comparisons but block transfers to and from disk. External merge sort sorts memory-sized runs, then folds them together with a heap-driven -way merge in passes.
╌╌╌╌
Every sort we have met so far, heapsort, mergesort, the linear-time sorts, shares a hidden assumption: the whole array fits in fast memory, and any element is as cheap to touch as any other. When the data is larger than RAM, a database table of a hundred gigabytes on a machine with eight of them, that assumption collapses. The array lives on disk, and the cost of the sort is no longer how many times we compare but how many times we move a block between disk and memory. This lesson is about sorting under that constraint: external, or out-of-core, sorting.
Why the in-memory cost model lies
A disk does not behave like slower random-access memory. Reaching an arbitrary byte costs a seek (moving the head, or, on flash, an erase-block penalty) that is four to five orders of magnitude slower than a memory reference. To amortize that latency, storage is read and written in fixed-size blocks of records at a time; once the head is positioned, the marginal cost of the rest of the block is small. The realistic accounting therefore counts block transfers, not comparisons.
This change of cost model reorders the algorithm rankings. Consider running ordinary quicksort on data that does not fit in memory, with the array paged in and out by the operating system. Its partition step sweeps the array with two pointers that jump unpredictably, and its recursion touches scattered regions. Each stray access can force a fresh block transfer, so an algorithm that is in comparisons can degrade toward one block transfer per comparison, thrashing the disk. The that looked harmless is now multiplied by a constant measured in milliseconds.
The design goal flips accordingly. We want an algorithm whose disk traffic is a handful of linear sequential passes over the data, and we will measure it by counting those passes.
External merge sort
The right strategy descends directly from mergesort, whose merge step is already a sequential streaming operation: it reads two sorted inputs front to back and writes one sorted output front to back, never seeking. That is the access pattern disk handles cheaply. External merge sort is mergesort reorganized into two phases that respect the block model.
Phase 1 — run formation. Read the file one memory-load at a time. Each load of up to records is sorted in memory by any internal sort, heapsort or quicksort, then written back out as a sorted run. A file of records yields about initial runs, each of length (the last possibly shorter). This phase reads the whole file once and writes it once: transfers, two sequential passes.
Phase 2 — merging. We now have many sorted runs and must combine them into one. Merging two runs at a time, as plain mergesort does, would take passes over the data. We do far better by merging runs at once in a single pass, a -way merge, repeating until one run remains.
The k-way merge
The core step is selecting, repeatedly, the smallest record among sorted runs. Naively scanning all run fronts on every step costs per record; across records that is comparisons, throwing away the savings of a large fan-out. The fix is a structure we already own: a min-heap, the priority queue from the heap lesson, holding one candidate, the current front record, from each run. Its root is the global minimum, extracted in , and when a run supplies its next record we sift that in for another .
- 1let be an empty min-heap keyed on record value
- 2for to do
- 3if run is nonempty then
- 4read first record of
- 5tag each candidate with its run
- 6while is nonempty do
- 7smallest across all run fronts
- 8outputappend to the merged run
- 9if run has a next record then
- 10read next record of
- 11refill from the run we drained
The heap never holds more than records, one per run, so it occupies memory regardless of how long the runs are. Each of the output records costs one and at most one , so a -way merge runs in comparisons, improving the per-record cost from to over the naive scan.
How many passes?
This is the figure of merit. Phase 1 produces runs. Each merging pass reads every run once and writes the merged output once, a full sequential sweep of the data, and replaces runs with one, dividing the run count by . We are done when the count reaches .
Each pass moves the entire file, reads and writes, so the total I/O cost is
The base of the logarithm is what matters. Doubling the fan-out from to halves the number of merge passes, because . On a terabyte file with gigabytes of memory the difference between a binary merge and a wide one is the difference between dozens of passes and two or three.
Why not make k enormous?
If a bigger always helps, why not merge all runs in one pass? Because fan-out is bounded by memory. During a merge the records of memory are divided into one input buffer per run plus an output buffer; each buffer must hold at least one block of records to keep the disk traffic sequential. With input buffers and an output buffer we need roughly , so
Push past this and the buffers shrink below a block, the merge starts seeking within each run, and the per-record I/O cost explodes, exactly the thrashing we set out to avoid. So is chosen near : large enough that is typically or , small enough that every buffer is at least one block. There is also a CPU-side tension, the heap costs comparisons, but on disk-bound sorts the I/O term dominates and the comparison cost is secondary.
Replacement selection: longer initial runs
The pass count is , driven by the number of initial runs, . Anything that makes the initial runs longer shrinks and can remove a whole pass. Phase 1 as described caps each run at , the memory size. Replacement selection beats that bound, producing runs of average length .1
The idea treats memory as a min-heap of records that continuously consumes
input and emits output, rather than sorting in fixed batches. Fill the heap with
records. Repeatedly extract the minimum and append it to the current run; then
read the next input record and decide where it goes. If it is the record
just emitted, it can still belong to the current run, so insert it into the heap.
If it is smaller, it cannot, so set it aside (mark it frozen
) to seed the
next run. The current run grows as long as incoming records keep up with the
output; only when the heap is entirely frozen records do we close the run and
start fresh.
- 1fill min-heap with the first input records
- 2last value written to the current run
- 3while is nonempty do
- 4smallest unfrozen record
- 5output to the current run
- 6
- 7if more input remains then
- 8read next input record
- 9if then
- 10still fits this run
- 11else
- 12freeze for the next runmark, keep in memory
- 13if has only frozen records then
- 14close current run; unfreeze all; start a new run
Why on average? Picture the input as a stream and the heap as a window of
records sliding along it. A new record extends the current run whenever it is
no smaller than the last output, which for random data happens about half the time
at any moment, but the records that do extend the run push the window forward
and let still more records qualify. The classic snowplow
argument makes it
precise: a plow clears snow on a circular road while snow keeps falling uniformly;
in steady state the plow always has about twice its own length of snow ahead of it.
The same balance yields runs of expected length , and on already-sorted or
nearly-sorted input a single run covering the entire file.2
Roughly halving the run count this way often saves exactly one merge pass, a sizable fraction of the disk traffic. Modern external sorts combine wide fan-out merging with replacement-selection run formation, and on real workloads the whole sort finishes in a small constant number of passes over the data.
The disk-block connection: B-trees
The same accounting that shapes external sorting shapes external search. When a sorted index is too big for memory, a balanced binary search tree is a poor fit: its height is , and following each child pointer can cost a fresh block transfer, so a lookup costs seeks. A B-tree fixes this with the same fan-out idea external merge sort uses. Pack each node with as many keys as fill one block, of them, so each node has children instead of . The tree's height collapses from to
and a search touches only blocks — the same savings, with the block size playing the fan-out . A wide node is to search what a wide merge is to sorting.
Past the two-parameter model
External merge sort is tuned to two parameters, memory size and block size , that it must be told. Two developments push past that dependence, and one scales the idea across an entire datacenter.
The optimal I/O bound. The pass-count analysis gives block transfers with fan-out . Aggarwal and Vitter (1988) proved this is optimal: no external sorting algorithm, comparison-based, can do asymptotically fewer I/Os. So external merge sort is to the block model what mergesort is to the comparison model — provably the best possible up to constants.
Cache-oblivious sorting. External merge sort needs and hard-coded to
size its buffers. A cache-oblivious algorithm hits the same optimal I/O bound
without knowing or , so a single binary runs optimally across every level
of the memory hierarchy at once — registers, L1, L2, RAM, disk — each with its own
unknown block size. Funnelsort (Frigo, Leiserson, Prokop, and Ramachandran,
1999) achieves this by merging through recursively-built -funnels
whose
sizes form a geometric ladder; the recursion automatically arranges that whatever
the true block size turns out to be, the data movement near that scale is
sequential. It is the same -way-merge idea, made self-tuning.
Sorting a datacenter: TeraSort. When the data outgrows one machine's disks, the
merge fans out across a cluster. The MapReduce sort (and the TeraSort
benchmark built on it) is external merge sort's distributed cousin: a sampling pass
picks splitter keys that partition the key range into roughly equal
bands, each node range-partitions its shard by shipping records to the node owning
their band (the shuffle
), and each node sorts its band locally. Concatenating the
bands in order yields a globally sorted file — a distributed bucket sort whose
splitters play the role of bucket boundaries, and whose per-node local sort is
itself an external merge sort. Sorting a petabyte this way is a standard
industry benchmark.
Solid-state storage. The model here charges a flat cost per block transfer, which suited spinning disks where the seek dominated. Flash storage changes the constants: random reads are nearly as cheap as sequential, but writes are expensive, must happen in large erase blocks, and wear the device out. Modern external sorts on SSDs therefore optimize for write minimization and large sequential writes rather than seek avoidance — the accounting shifts, but the core strategy, few sequential passes with wide fan-out, carries over.1
Takeaways
- On data larger than memory the cost model changes: we count block transfers (I/Os), not comparisons, and seeks dwarf everything, so good algorithms move the disk in long sequential passes.
- External merge sort has two phases: form sorted runs of memory size, then repeatedly -way merge them until one run remains.
- The -way merge uses a min-heap of the run fronts to emit the next smallest record in , giving comparisons and a single sequential pass per merge.
- The number of passes is ; larger fan-out cuts passes, but because every run needs a block-sized buffer in memory, else the merge starts seeking.
- Replacement selection forms initial runs of average length (double the naive bound), shrinking and often removing a whole pass.
- The same block-fan-out idea powers the B-tree: packing keys per node makes search seeks instead of — the same savings applied to search.
Footnotes
- Skiena, The Algorithm Design Manual, §4.6 — External Sorting. Merge sort adapts to disk by sorting memory-sized runs and merging them; replacement selection builds runs averaging twice the memory size. ↩ ↩2
- CLRS, Problem 6-3 and Ch. 11 — heaps for -way merging and the external-memory accounting in block transfers. The replacement-selection
snowplow
analysis gives expected run length . ↩
╌╌ END ╌╌