Block size, ratio and point-lookup cost
Measure your machineNeeds Python with zstandard
Problem
A compressed file is split into blocks so that a reader can decode part of it. The block size trades two things. Larger blocks give the compressor more context, so the ratio rises; but a point lookup must decode the whole block that holds its row, so its cost grows with the block. The ratio stops rising once the block holds enough repetitions for the compressor to learn the data's patterns.
Measure the three effects of block size with a real codec.
The question. How do the compression ratio and the cost of one point lookup change with the block size?
Predict
The ratio rises steeply from 1 KB to about 64 KB and then flattens, while the decode time per lookup grows roughly in proportion to the block size.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
block-size-ratio-and-point-lookup.py # /// script # requires-python = ">=3.10" # dependencies = ["zstandard"] # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Block size: compression ratio against point-lookup cost. 200,000 JSON rows compressed with Zstd in blocks of 1 KB to 1 MB: the ratio, and the time to decode the one block a point lookup needs. """ import argparse, json, random, time import zstandard as zstd p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--rows", type=int, default=200_000, help="Rows.") p.add_argument("--blocks", type=lambda s: [int(v) for v in s.split(",")], default=[1024, 4096, 16384, 65536, 262144, 1048576], help="Block sizes, bytes.") p.add_argument("--seed", type=int, default=0, help="Random seed.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() random.seed(args.seed) rows_ = [json.dumps({"parcel": f"P{random.randint(0, 10**6)}", "depot": random.choice(["RTM", "LYN", "HAM"]), "status": random.choice(["SCANNED", "LOADED", "DELIVERED"]), "t": 1700000000 + i}).encode() for i in range(args.rows)] data = b"\n".join(rows_) c, d = zstd.ZstdCompressor(level=3), zstd.ZstdDecompressor() rows = [] for block in args.blocks: comp = [c.compress(data[i:i + block]) for i in range(0, len(data), block)] ratio = len(data) / sum(len(x) for x in comp) t0 = time.perf_counter() for _ in range(200): d.decompress(comp[len(comp) // 2]) us = (time.perf_counter() - t0) / 200 * 1e6 rows.append({"block": block, "ratio": ratio, "lookup_us": us}) print(f"block {block:>8} B ratio {ratio:5.2f} one lookup decodes {block} B in {us:7.1f} us") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"blocks": rows}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run block-size-ratio-and-point-lookup.py uv run block-size-ratio-and-point-lookup.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --rows200,000 | Rows. |
| --blocks1024, 4096, 16,384, 65,536, 262,144, 1,048,576 | Block sizes, bytes. |
| --seed0 | Random seed. |
Our run
Data table
| Block size (bytes) | Ratio |
|---|---|
| 1,020 | 4.25 |
| 4,100 | 7.04 |
| 16.4K | 8.38 |
| 65.5K | 8.71 |
| 262K | 8.14 |
| 1.05M | 8.53 |
Data table
| Block size (bytes) | µs per lookup |
|---|---|
| 1,020 | 4.18 |
| 4,100 | 3.36 |
| 16.4K | 8.29 |
| 65.5K | 28.3 |
| 262K | 127 |
| 1.05M | 497 |
block 1024 B ratio 4.25 one lookup decodes 1024 B in 4.2 us
block 4096 B ratio 7.04 one lookup decodes 4096 B in 3.4 us
block 16384 B ratio 8.38 one lookup decodes 16384 B in 8.3 us
block 65536 B ratio 8.71 one lookup decodes 65536 B in 28.3 us
block 262144 B ratio 8.14 one lookup decodes 262144 B in 126.8 us
block 1048576 B ratio 8.53 one lookup decodes 1048576 B in 497.0 usWhat we found
The ratio rose from 4.25 at 1 KB to 8.38 at 16 KB and 8.71 at 64 KB, then stayed flat. A lookup cost about the same at 1 KB and 4 KB (4.18 and 3.36 µs, where the fixed cost of starting a decode dominates) and then grew in proportion to the block, to 497 µs at 1 MB.
Read it
Choose the block size where the ratio curve flattens for scans, and a small block for lookups. Past the knee a larger block buys nothing in size and costs every point read linearly. That is why row stores use small compressed pages and column stores large ones: the workload, not the codec, picks the block size.