How does ClickHouse store data on disk, and why does columnar storage help compression?
Quick Answer
ClickHouse stores each column in a separate file within a part directory, with rows grouped into granules of 8192 rows. Columnar storage achieves 5-20x compression because columns contain homogeneous data types where consecutive values are often similar, enabling efficient encoding with LZ4 or ZSTD plus delta/double-delta codecs.
Detailed Answer
Think of packing a suitcase. If you throw in random items (a shoe, a book, a shirt, a mug) like a row-oriented database, you cannot compress efficiently because shapes and sizes vary wildly. But if you pack all shoes together, all books together, and all shirts together (columnar), each group compresses beautifully because items in each group share physical properties. The shoes nest into each other, books stack flat, and shirts fold uniformly.
ClickHouse stores data in parts, which are directories on the filesystem. Each part contains one file per column (column_name.bin) holding compressed column data, a marks file (column_name.mrk2) mapping granule numbers to byte positions in the compressed file, and a primary.idx file storing the ORDER BY key values at granule boundaries. A granule is the minimum unit of data ClickHouse reads and contains 8192 rows by default (configurable via index_granularity).
Internally, column compression works in two stages. First, an optional encoding codec transforms the raw values: Delta encoding stores differences between consecutive values (great for timestamps), DoubleDelta stores differences of differences (great for monotonically increasing sequences), and Gorilla encoding handles floating-point metrics. Second, a general-purpose compression algorithm (LZ4 for speed or ZSTD for ratio) compresses the encoded data. Because columns contain only one data type, the encoded values exhibit patterns that general compression exploits efficiently. An integer column with timestamps might compress from 8 bytes per value down to 0.5 bytes per value.
At production scale, compression ratios directly translate to cost savings and performance. A 10TB raw dataset might compress to 500GB-2TB on disk, reducing storage costs and I/O bandwidth requirements proportionally. ClickHouse reads compressed blocks from disk and decompresses them in CPU cache, meaning compression actually speeds up queries by reducing the bottleneck (disk I/O) at the cost of cheap CPU cycles. The marks file enables ClickHouse to seek directly to the correct byte offset within a compressed column file without scanning from the beginning.
The non-obvious gotcha is that column ordering within the ORDER BY key dramatically affects compression. If you order by (timestamp, user_id), the timestamp column compresses extremely well (delta encoding on sorted timestamps) but user_id values jump around randomly. If you order by (user_id, timestamp), user_id compresses well (many consecutive identical values) and timestamp still compresses reasonably within each user's events. Choose ordering based on both query patterns AND compression efficiency.