Skip to content
About
Main content

Row order dominates the codec

Why does the order of the rows change the compression ratio more than the choice of codec?

The encoder looks at the values

Serialization settled where the description of the data lives (in a schema, in a registry, in a ). From here on we assume that question is answered. We now have a homogeneous run of values: one column, one type, many values.

What is new in this stage is that the encoder gets to look at the data before it decides how to represent it.

This is the first time here that the representation depends on the values and not only on the type. Until now the schema fixed the layout: an int64 took eight bytes because it was declared int64. Here the encoder inspects a block of values and may find that all 128 values lie between 1,000,000 and 1,000,255. Then it spends eight bits on each value. Nothing in the schema said that. The schema said 64 bits. The schema was not wrong, but it was loose: it gave an upper bound, and people mistook the upper bound for a description.

The new constraint is that the choice must be made from a bounded of data (one block), must be recorded somewhere so the reader can undo it, and can turn out to be wrong if the distribution changes later. The constraint that stops mattering is field (where a field starts and ends) and field identity. The governing question becomes:

Given the real distribution of a column, what is the fewest bits per value that still lets you decode at memory-bandwidth speed, and, better, that lets you answer the query without decoding at all?

The last part of that sentence is the reason this stage exists separately from general-purpose compression (General-purpose compression). Many introductions miss it.

means: you can still do useful work (compare, search, add up) on the encoded form.


The floor that everything is measured against

Before looking at any technique, let us set a numeric target. We use : the minimum average number of bits per symbol. For values drawn from a distribution p, the entropy is

how often each value occurs
the minimum average number of bits per symbol
Data table
Entropy of a two-valued column, by how often the commoner value occurs
Frequency of the commoner value pEntropy H (bits per value)
0.51
0.5061
0.5121
0.5190.999
0.5250.998
0.5310.997
0.5370.996
0.5440.994
0.550.993
0.5560.991
0.5620.989
0.5690.986
0.5750.984
0.5810.981
0.5870.978
0.5940.974
0.60.971
0.6060.967
0.6120.963
0.6190.959
0.6250.954
0.6310.95
0.6370.945
0.6440.94
0.650.934
0.6560.928
0.6620.922
0.6690.916
0.6750.91
0.6810.903
0.6870.896
0.6940.889
0.70.881
0.7060.874
0.7120.866
0.7190.857
0.7250.849
0.7310.84
0.7370.831
0.7440.821
0.750.811
0.7560.801
0.7620.791
0.7690.78
0.7750.769
0.7810.758
0.7870.746
0.7940.734
0.80.722
0.8060.709
0.8120.696
0.8190.683
0.8250.669
0.8310.655
0.8370.64
0.8440.625
0.850.61
0.8560.594
0.8620.578
0.8690.561
0.8750.544
0.8810.526
0.8870.508
0.8940.489
0.90.469
0.9060.449
0.9120.428
0.9190.407
0.9250.385
0.9310.362
0.9370.338
0.9440.313
0.950.287
0.9560.26
0.9620.231
0.9690.201
0.9750.169
0.9810.135
0.9870.098
0.9940.056
10.001

"Order-0" means we only use how often each value occurs, and we ignore which values come before or after it. The minimum size for a column of N values is then N × H bits.

This is the economic case for the whole stage: lightweight encodings capture nearly all of the order-0 gain at almost no CPU cost. They leave the general-purpose codec only the last few percent, plus whatever structure of higher order remains. It is also why the pipeline order in Bytes become an output is forced and not just a habit.

But these encodings go beyond order-0, and this is where their real power is. Take one million distinct 64-bit timestamps. Every value is unique, so the order-0 entropy is log₂(10⁶) ≈ 20 bits per value. That is a hard floor for any order-0 method, including a dictionary. Now sort the timestamps. The differences between neighbours are small and regular. The conditional entropy (the entropy of a value given the value before it) falls to two or three bits. The data and the information are the same, but the achievable size is about 7× smaller (20 ÷ 3). This gain came from a decision that has nothing to do with the codec.

This leads to the first topic. It is the answer to Nikhil's problem.


Row order: the decision that dominates the codec

Most lists of encodings do not mention sort order. In practice, , often by a factor of ten, and it costs nothing at read time.

Let us derive this for (RLE, explained fully in a moment: store a value and how many times it repeats). It is the clearest case.

  • A column has k distinct values and N rows. If the rows are in random order, the number of runs is about N × (1 − 1/k). For any k above a few, this is essentially N: every row starts a new run. The ratio is 1 (no gain), and there is even a small expansion.
  • If the rows are sorted, there are exactly k runs. The ratio is N / k.
rows
distinct values
sortedrandom order
Data table
RLE ratio by distinct values (N = 10⁶ rows)
Distinct values kseriesCompression ratio
2sorted500,000×
2.29sorted437,000×
2.62sorted382,000×
3sorted333,000×
3.44sorted291,000×
3.93sorted254,000×
4.5sorted222,000×
5.15sorted194,000×
5.9sorted169,000×
6.76sorted148,000×
7.73sorted129,000×
8.85sorted113,000×
10.1sorted98,700×
11.6sorted86,200×
13.3sorted75,300×
15.2sorted65,800×
17.4sorted57,400×
19.9sorted50,200×
22.8sorted43,800×
26.1sorted38,300×
29.9sorted33,400×
34.2sorted29,200×
39.2sorted25,500×
44.9sorted22,300×
51.4sorted19,500×
58.8sorted17,000×
67.3sorted14,900×
77.1sorted13,000×
88.2sorted11,300×
101sorted9,900×
116sorted8,650×
132sorted7,550×
152sorted6,600×
174sorted5,760×
199sorted5,030×
227sorted4,400×
260sorted3,840×
298sorted3,360×
341sorted2,930×
391sorted2,560×
447sorted2,240×
512sorted1,950×
586sorted1,710×
671sorted1,490×
768sorted1,300×
879sorted1,140×
1.01Ksorted993×
1.15Ksorted868×
1.32Ksorted758×
1.51Ksorted662×
1.73Ksorted578×
1.98Ksorted505×
2.27Ksorted441×
2.59Ksorted385×
2.97Ksorted337×
3.4Ksorted294×
3.89Ksorted257×
4.46Ksorted224×
5.1Ksorted196×
5.84Ksorted171×
6.69Ksorted150×
7.66Ksorted131×
8.76Ksorted114×
10Ksorted99.7×
11.5Ksorted87.1×
13.2Ksorted76×
15.1Ksorted66.4×
17.2Ksorted58×
19.7Ksorted50.7×
22.6Ksorted44.3×
25.9Ksorted38.7×
29.6Ksorted33.8×
33.9Ksorted29.5×
38.8Ksorted25.8×
44.4Ksorted22.5×
50.9Ksorted19.7×
58.2Ksorted17.2×
66.6Ksorted15×
76.3Ksorted13.1×
87.3Ksorted11.4×
100Ksorted10×
2random order2×
2.29random order1.78×
2.62random order1.62×
3random order1.5×
3.44random order1.41×
3.93random order1.34×
4.5random order1.29×
5.15random order1.24×
5.9random order1.2×
6.76random order1.17×
7.73random order1.15×
8.85random order1.13×
10.1random order1.11×
11.6random order1.09×
13.3random order1.08×
15.2random order1.07×
17.4random order1.06×
19.9random order1.05×
22.8random order1.05×
26.1random order1.04×
29.9random order1.03×
34.2random order1.03×
39.2random order1.03×
44.9random order1.02×
51.4random order1.02×
58.8random order1.02×
67.3random order1.02×
77.1random order1.01×
88.2random order1.01×
101random order1.01×
116random order1.01×
132random order1.01×
152random order1.01×
174random order1.01×
199random order1.01×
227random order1×
260random order1×
298random order1×
341random order1×
391random order1×
447random order1×
512random order1×
586random order1×
671random order1×
768random order1×
879random order1×
1.01Krandom order1×
1.15Krandom order1×
1.32Krandom order1×
1.51Krandom order1×
1.73Krandom order1×
1.98Krandom order1×
2.27Krandom order1×
2.59Krandom order1×
2.97Krandom order1×
3.4Krandom order1×
3.89Krandom order1×
4.46Krandom order1×
5.1Krandom order1×
5.84Krandom order1×
6.69Krandom order1×
7.66Krandom order1×
8.76Krandom order1×
10Krandom order1×
11.5Krandom order1×
13.2Krandom order1×
15.1Krandom order1×
17.2Krandom order1×
19.7Krandom order1×
22.6Krandom order1×
25.9Krandom order1×
29.6Krandom order1×
33.9Krandom order1×
38.8Krandom order1×
44.4Krandom order1×
50.9Krandom order1×
58.2Krandom order1×
66.6Krandom order1×
76.3Krandom order1×
87.3Krandom order1×
100Krandom order1×

With N = 1,000,000 and k = 100 this is a swing from 1× to 10,000×. Same column, same codec, four orders of magnitude, decided only by row order.

For encoding (store the difference from the previous value, explained below) the situation is worse than "no gain". On data with no sequential relation, delta is harmful. The difference of two independent random values has about twice the range of the original values, so you pay about one extra bit per value. Delta is a bet on correlation between neighbours, and losing the bet has a real cost.

Sorting also buys a second benefit through the same act. A file format stores min/max statistics for each chunk of rows (a "zone map"). A query like WHERE event_time > X can skip every chunk whose maximum is below X. But skipping works only when different chunks cover non-overlapping ranges. On a sorted column, every chunk has its own range and skipping is nearly perfect. On an unsorted column, every chunk spans almost the whole domain and skipping is worth exactly nothing. Compression and skipping are two returns on one decision.

The catch: sort order is a scarce resource

You get one physical order per file. Sort by (A, B), and column A gets long runs while column B gets runs only inside each run of equal A values, so the leading column takes almost all of the compression benefit. Which key to choose, how a composite key fails, and when space-filling curves such as Z-order are worth their partial locality are storage layout's questions; what matters here is that the answer decides the ratio.

Streaming ingestion cannot sort globally. Rows arrive in time order, so each file is ordered only locally. Sortedness is something writes consume and only rewriting restores, and the rewrite is compaction: a background job that merges small files into larger, re-sorted files. So . This benefit is almost never counted when people size the budget.