πŸ—œοΈ Data Compression

Codecs, Stacking Strategies & Storage Efficiency in ClickHouse

πŸ—οΈ Why Column Stores Champion Compression

Row-oriented databases store one row's columns contiguously. Column-oriented databases store one column's values contiguously. That single difference changes everything for compression.

Row-Oriented Storage

id:1101 ts:11700 val:142.5 cat:1A
id:2102 ts:21701 val:243.1 cat:2B
id:3103 ts:31702 val:341.8 cat:3A
id:4104 ts:41703 val:442.9 cat:4A
Each row mixes types. Compressing requires scanning across mixed data types β€” defeats most specialized codecs.

Column-Oriented Storage

id
101102103104
ts
1700170117021703
val
42.543.141.842.9
cat
ABAA
Same type = same codec. A column of timestamps uses the same compression for every value β€” optimal codec selection becomes possible.
πŸ“Š

Column Homogeneity

All values in a column share the same data type and often similar semantic meaning. This predictability lets specialized codecs work at maximum efficiency.

πŸ”

Repetition Amplification

Low-cardinality columns have enormous repetition at the byte level. A column of status codes (200, 404, 500) compresses far better than the same data scattered across rows.

πŸ“ˆ

Sequential Pattern Discovery

Timestamps increment by fixed amounts. Sensor readings change gradually. Network IDs have prefix patterns. Column storage exposes these patterns for codec optimization.

🧊

Memory Footprint Reduction

Compressed data stays compressed in memory. ClickHouse's vectorized execution processes compressed data directly β€” fewer memory reads, more cache-friendly scans.

⚑ The ClickHouse Compression Philosophy

ClickHouse optimizes for compression ratio first, decompression speed second. In OLAP workloads, data is read far more often than it is written, and queries scan millions of rows. Getting more data per read is the primary goal.

🎯 Primary Goal: Maximum Ratio

Better compression means more data fits in disk I/O buffers, more data stays in CPU cache, and less data needs to be read from storage. The savings compound across every query.

βš–οΈ Acceptable Tradeoff: CPU Time

Decompression CPU cost is predictable and bounded. With modern CPUs doing 5–10 GB/s LZ4 decompression, the CPU cost is almost always worth the I/O savings.

πŸ“‰ Secondary Goal: Encode Speed

Data is written once, read many times. ClickHouse tolerates slower encoding in exchange for faster decoding β€” writes happen during ingestion windows, reads happen constantly.

🎚️ Codec Selectability

Unlike most databases with one or two compression options, ClickHouse exposes per-column codec selection β€” letting you match the compressor to the data pattern.

Philosophy Aspect ClickHouse Approach Typical RDBMS Approach
Compression priority Ratio over speed β€” ZSTD default Speed over ratio β€” LZ4 or no compression
Codec selection Per-column, user-specified Global, automatic, or none
Specialized codecs Delta, Gorilla, DoubleDelta, T64 None β€” generic only
Codec stacking Up to 4 codecs per column Single codec or none
Block-level optimization Per-granule codec selection (8192 rows) Table or page-level only
Memory compression Full support β€” processed in-place Usually decompress-on-read

πŸ—œοΈ Generic Compression Algorithms

ClickHouse supports several general-purpose compression algorithms. These are the workhorses β€” they're the fallback when specialized codecs don't apply, and they often work very well as the final stage in a codec stack.

LZ4 Default

Compression Speed
⚑⚑⚑⚑⚑
Decompression Speed
⚑⚑⚑⚑⚑
Ratio
πŸ“¦πŸ“¦πŸ“¦
CPU Cost
🟒🟒

Use when: You need the fastest possible decompression. Good default for frequently-accessed hot data where I/O is the bottleneck.

ALTER TABLE events MODIFY COLUMN timestamp_codec LZ4;

ZSTD Recommended

Compression Speed
⚑⚑⚑
Decompression Speed
⚑⚑⚑⚑
Ratio
πŸ“¦πŸ“¦πŸ“¦πŸ“¦
CPU Cost
🟑🟑🟑

Use when: You want the best balance of ratio and speed. ClickHouse's default for most scenarios. ZSTD level 1 is default, but you can tune the level (1–22) for more ratio at more CPU cost.

-- Default ZSTD level 1 ALTER TABLE events MODIFY COLUMN data_codec ZSTD(1); -- Higher ratio, more CPU ALTER TABLE events MODIFY COLUMN data_codec ZSTD(3);

LZMA

Compression Speed
⚑
Decompression Speed
⚑⚑
Ratio
πŸ“¦πŸ“¦πŸ“¦πŸ“¦πŸ“¦
CPU Cost
πŸ”΄πŸ”΄πŸ”΄πŸ”΄

Use when: Data is archived and rarely accessed. Maximum compression ratio matters more than decode speed. Very high CPU usage β€” not recommended for hot data.

ALTER TABLE archive MODIFY COLUMN old_data_codec LZMA;

Zlib (gzip)

Compression Speed
⚑⚑
Decompression Speed
⚑⚑
Ratio
πŸ“¦πŸ“¦πŸ“¦πŸ“¦
CPU Cost
🟑🟑🟑

Use when: Compatibility with other tools matters, or when you need gzip-compatible output. Slower than LZ4 or ZSTD with similar ratio to ZSTD level 1.

ALTER TABLE logs MODIFY COLUMN payload_codec Zlib;

Ziqng

Compression Speed
⚑
Decompression Speed
⚑⚑⚑
Ratio
πŸ“¦πŸ“¦πŸ“¦πŸ“¦
CPU Cost
🟑🟑🟑🟑

Use when: Maximum compression on cold archive data. Better ratio than ZSTD level 1, but significantly slower. Niche use case.

ALTER TABLE archive MODIFY COLUMN cold_data_codec Ziqng;

πŸ“Š Generic Codec Benchmark (approximate)

Codec Encode Speed (MB/s) Decode Speed (MB/s) Compression Ratio CPU Cost
LZ4 ~800 ~2500 2–4Γ— Low
ZSTD (level 1) ~350 ~900 3–6Γ— Medium
ZSTD (level 3) ~180 ~850 4–8Γ— Medium-High
Zlib ~100 ~300 3–5Γ— Medium
LZMA ~30 ~200 5–10Γ— High
Ziqng ~20 ~350 5–9Γ— High

* Benchmarked on Intel Skylake-class hardware. Your hardware will vary. Codec performance depends heavily on data entropy β€” high-randomness data compresses poorly with all algorithms.

πŸ”¬ Specialized Codecs β€” The ClickHouse Secret Sauce

Generic compression treats data as opaque bytes. ClickHouse's specialized codecs understand the semantics of your data β€” timestamps increment, floats drift slowly, IDs share prefixes. This understanding lets them achieve dramatically better ratios than generic algorithms.

Specialized codecs work by detecting and exploiting data patterns. They don't just compress β€” they transform data into a more compact representation based on what the data means, not just what it contains.

Most specialized codecs are lossless β€” the original data is perfectly reconstructed. They're designed to stack on top of each other, with each codec in the chain handling a specific pattern.

Codec Families
Delta Family Delta β†’ DoubleDelta β†’ T64 Monotonic sequences
Gorilla Family Gorilla Similar floating point values
Dictionary Encoded Low-cardinality strings
Generic LZ4 β†’ ZSTD β†’ LZMA Final stage compression

πŸ”“ DEFAULT β€” No Compression

No-op Any data type

Stores data uncompressed. Sometimes the right choice β€” data that's already encrypted or randomly distributed won't compress further, and compression adds CPU overhead for no benefit.

When to use DEFAULT:
  • UUID columns β€” random by design, no compression benefit
  • Already-compressed data (JPEG, video, binary blobs)
  • Columns with very high cardinality (>1M unique values)
  • Data in memory-only tables where I/O isn't a concern
clickhouse-sql
-- UUIDs don't compress well
CREATE TABLE events (
    event_id UUID DEFAULT,
    user_id UUID DEFAULT,
    payload String DEFAULT  -- already compressed external data
) ENGINE = MergeTree()
ORDER BY (event_id);

πŸ“Š Delta β€” Monotonic Sequence Compression

Preprocessor Integers, Dates, DateTime, UInt/N

Stores the difference between consecutive values instead of raw values. For monotonically increasing sequences (timestamps, auto-increment IDs), differences are tiny β€” a timestamp delta might be just 1 instead of 1704067200.

How Delta encoding works:
1704067200, 1704067201, 1704067202, 1704067203, 1704067204
5 Γ— 4 bytes = 20 bytes
β†’
1704067200, 1, 1, 1, 1
Can now compress to ~5 bytes with Delta(4)
Key insight: Delta stores the first value as-is, then stores subsequent values as value[i] - value[i-1]. Small deltas compress dramatically better than large raw numbers.
Best use cases:
  • Timestamps with regular intervals (1s, 1ms, 1ΞΌs)
  • Monotonically increasing IDs (user_id, order_id)
  • Sequence numbers in event logs
  • Sensor readings taken at fixed intervals
clickhouse-sql
-- Delta(4) stores deltas as 4-byte integers
-- Good for timestamps where deltas fit in 4 bytes
CREATE TABLE sensor_events (
    timestamp DateTime64(3) CODEC(Delta(4)),
    sensor_id UInt32 CODEC(Delta(4)),
    temperature Float32 CODEC(Gorilla)
) ENGINE = MergeTree()
ORDER BY (sensor_id, timestamp);
Limitation: Delta only works well when deltas are small. If your timestamps have irregular gaps (seconds vs. hours), delta will store large values and gain little. For irregular timestamps, consider DoubleDelta or T64.

πŸ“ˆπŸ“Š DoubleDelta β€” Predictable Rate-of-Change

Preprocessor Integers, DateTime, DateTime64

Stores the delta of deltas β€” the second-order difference. For sequences that change at a constant rate (timestamps incrementing by exactly 1ms, counters incrementing by exactly 1), DoubleDelta achieves extraordinary compression.

DoubleDelta on regularly-spaced timestamps:
Original:
1000000000, 1000000001, 1000000002, 1000000003
β†’
After Delta:
1000000000, 1, 1, 1
β†’
After DoubleDelta:
1000000000, 0, 0, 0
Almost all zeros β€” compresses beautifully with minimal bits!
Perfect for: Metrics that arrive at regular intervals β€” server timestamps, sensor readings with fixed sampling rates, usage counters incremented by fixed amounts.
clickhouse-sql
-- DoubleDelta(4) for 4-byte timestamp deltas
-- Excellent for regular-interval time series
CREATE TABLE metrics (
    timestamp DateTime64(3) CODEC(DoubleDelta),
    metric_name LowCardinality(String),
    value Float64 CODEC(Gorilla)
) ENGINE = MergeTree()
ORDER BY (metric_name, timestamp);

-- DoubleDelta shines for DateTime64(3) at millisecond precision
-- Ratio improvement over raw: 10-50Γ— typical

πŸ—ƒοΈ T64 β€” Transform for 64-byte Alignment

Preprocessor Signed/Unsigned Int: 8, 16, 32, 64-bit

T64 is a byte-order transformation that rearranges integer data for better compression. It processes 64-byte blocks, transposing data so that similar bytes across values are grouped together β€” improving dictionary-based compression that follows it.

T64 is typically used as a preprocessing step before other codecs β€” it reorganizes data into a form that downstream codecs can exploit more effectively.

clickhouse-sql
-- T64 as a preprocessing codec
-- Reorganizes bytes for better downstream compression
CREATE TABLE events (
    event_id UInt64 CODEC(T64, ZSTD(1)),
    user_id UInt64 CODEC(T64, LZ4),
    timestamp DateTime64(3) CODEC(DoubleDelta, ZSTD(1))
) ENGINE = MergeTree()
ORDER BY event_id;
Limitation: T64 requires the data to be aligned to 64-byte boundaries. It's most effective on integer types β€” using it on Float32/Float64 has limited benefit since the byte layout of floats doesn't group as well.

🦍 Gorilla β€” Floating Point Time Series Compression

Specialized Float32, Float64

Gorilla is a purpose-built codec for floating point data that changes gradually β€” the hallmark of time series from sensors, metrics, and monitoring systems. It's the codec that Facebook's Gorilla paper made famous, and ClickHouse implements it directly.

Gorilla exploits two patterns common in time series:

  1. Leading zero sharing: If consecutive floats share the same leading bits, only the differing bits are stored
  2. XOR compression: If value[i] XOR value[i-1] is small, store just the XOR result
Gorilla XOR compression example:
value[i]:
0x41973333 (20.9)
XOR
value[i-1]:
0x41974333 (20.95)
=
XOR:
0x00001000 (stores only this!)
Best for: Temperature sensors, stock prices, CPU metrics, system observability data β€” any floating point series where consecutive values are similar.
When Gorilla excels:
  • Sensor readings that change gradually (temperature, pressure)
  • Financial time series with small per-tick changes
  • CPU/memory utilization metrics (similar values across time)
  • Any Float32/Float64 where consecutive values often share leading bits
clickhouse-sql
CREATE TABLE temperature_sensors (
    sensor_id UInt32,
    timestamp DateTime64(3) CODEC(DoubleDelta, ZSTD(1)),
    temperature Float64 CODEC(Gorilla),       -- slowly varying β†’ great with Gorilla
    humidity Float64 CODEC(Gorilla),         -- also slowly varying
    battery_voltage Float64 CODEC(Gorilla)  -- gradual discharge, Gorilla works too
) ENGINE = MergeTree()
ORDER BY (sensor_id, timestamp);
Limitation: Gorilla works poorly on random or rapidly changing floats β€” network packet sizes, GPS coordinates, UUID-encoded floats. If consecutive values differ significantly, Gorilla stores full values and gains nothing.

πŸ”— Codec Stacking β€” The ClickHouse Advantage

This is where ClickHouse's compression system gets really powerful. You can chain up to 4 codecs per column β€” each codec in the chain handles a specific pattern, transforming data progressively until the final generic codec squeezes out the last bits.

Input Data
Raw column values
↓
Specialized Codec (1st)
Transforms data exploiting semantic patterns
Delta β†’ removes large baseline values
↓
Specialized Codec (2nd)
Further transforms the already-transformed data
Gorilla β†’ XOR of similar floats
↓
Generic Codec (last)
Final compression stage
ZSTD(1) β†’ dictionary + entropy coding
↓
Compressed Data
Stored on disk in compressed form
raw_timestamp[i] - raw_timestamp[i-1] β†’ small_delta β†’ XOR_with_previous β†’ compressed_bits
Combined effect: 10-50Γ— compression on time series data

πŸ’‘ Common Codec Stack Patterns

Timestamps (DateTime64)
DoubleDelta, ZSTD(1)
Why: DoubleDelta exploits the regular interval pattern. ZSTD provides final compression. This is the go-to stack for time series timestamps.
Typical ratio: 10–50Γ—
Timestamps + Floats
DoubleDelta, ZSTD(1) for timestamps, Gorilla, ZSTD(1) for floats
Why: Temperature, pressure, metrics β€” values that change slowly. Gorilla exploits XOR patterns in similar floats. DoubleDelta handles the regular timestamps.
Typical ratio: 8–30Γ— on floats
Auto-increment IDs
Delta(8), ZSTD(1)
Why: IDs increment monotonically. Delta(8) stores tiny 1-4 byte deltas instead of 8-byte raw IDs. ZSTD final stage cleans up.
Typical ratio: 10–20Γ—
Low-Cardinality Strings
Delta(4), ZSTD(3) or ZSTD(3)
Why: Low-cardinality string columns (country codes, status strings) often encode better if first run through Delta if there's a natural sort order, otherwise ZSTD alone.
Typical ratio: 5–15Γ—
High-Cardinality Strings (URLs, JSON)
ZSTD(3) β€” only option that helps for random data
Why: URLs, long strings, JSON payloads have no exploitable semantic patterns. ZSTD with higher level gives best ratio among options.
Typical ratio: 1.5–4Γ—
Counter / Metric Values
T64, Delta(8), ZSTD(1)
Why: T64 reorganizes bytes, Delta removes large base values, ZSTD compresses the small deltas. Best for monotonically increasing counters.
Typical ratio: 10–30Γ—

πŸ“‹ Stacking Rules & Constraints

1
Maximum 4 codecs per column definition
2
Last codec in the stack must be a generic codec (LZ4, ZSTD, LZMA, etc.)
3
Specialized codecs (Delta, DoubleDelta, Gorilla, T64) should come first, generic last
4
Codecs apply per granule (8192 rows), so data patterns within a granule matter most
5
Changing codec on existing columns requires ALTER TABLE ... MODIFY COLUMN
6
Codec selection is during table creation β€” cannot be changed per-row or per-part

πŸ“ Measuring Your Compression Ratios

ClickHouse provides several ways to inspect actual compression effectiveness. Use these queries to understand how your data is actually compressing and identify opportunities for improvement.

πŸ” system.parts β€” Per-Part Compression Stats

Every part in ClickHouse tracks compressed and uncompressed sizes. Query this to see per-table, per-part actual ratios.

clickhouse-sql
SELECT
    database,
    table,
    partition_id,
    name AS part_name,
    active,
    bytes_on_disk AS compressed_bytes,
    primary_key_bytes_in_memory AS pk_bytes,
    rows,
    ROUND(data_compressed_bytes / data_uncompressed_bytes, 3) AS compression_ratio,
    data_uncompressed_bytes,
    data_compressed_bytes
FROM system.parts
WHERE database = 'default' 
  AND table = 'events'
  AND active = 1
ORDER BY partition_id, name;

πŸ“Š system.columns β€” Column-Level Info

See which columns are storing data and their default codec specifications.

clickhouse-sql
SELECT
    name AS column_name,
    data_type,
    codec_desc AS codec,
    formatReadableSize(data_compressed_bytes) AS compressed_size,
    formatReadableSize(data_uncompressed_bytes) AS uncompressed_size
FROM system.columns
WHERE database = 'default'
  AND table = 'events'
  AND data_compressed_bytes > 0
ORDER BY data_uncompressed_bytes DESC;

πŸ§ͺ testCompression β€” Experimental Codecs

ClickHouse includes a testCompression() table function that compresses a sample of data with different codecs and reports the results.

clickhouse-sql
-- Test different codecs on your data
SELECT
    codec,
    compressed_size,
    uncompressed_size,
    ROUND(compression_ratio, 3) AS ratio,
    encode_time_us,
    decode_time_us
FROM (
    SELECT *
    FROM testCompression('your_table')
    WHERE compression_method NOT LIKE 'Preliminary%'
)
ORDER BY compression_ratio DESC;

πŸ“ˆ Per-Column Ratio Analysis

Identify which columns compress well and which don't β€” helps focus codec optimization effort.

clickhouse-sql
-- Find columns with poor compression (ratio < 2x)
SELECT
    database,
    table,
    name AS column_name,
    data_type,
    codec_desc,
    ROUND(data_uncompressed_bytes / NULLIF(data_compressed_bytes, 0), 2) AS ratio
FROM system.columns
WHERE database = 'default'
  AND table LIKE '%events%'
  AND data_compressed_bytes > 0
HAVING ratio < 2.0
ORDER BY ratio ASC;

🎯 Expected Compression Ratios by Data Type

Data Type Recommended Codec Stack Expected Ratio Notes
Timestamps (DateTime, DateTime64) DoubleDelta, ZSTD(1) 10–50Γ— Best when regular intervals
Float32/Float64 (slowly varying) Gorilla, ZSTD(1) 8–30Γ— Sensor readings, metrics
UInt64 IDs (monotonic) Delta(8), ZSTD(1) 10–20Γ— Auto-increment, event IDs
UInt64 IDs (random) ZSTD(3) 1.2–2Γ— Limited gains on random IDs
LowCardinality(String) ZSTD(3) 5–15Γ— Country codes, status strings
String (high entropy) ZSTD(3) 1–3Γ— JSON, URLs, random strings
Nullable(DateTime) DoubleDelta, ZSTD(1) 8–40Γ— NULL bits pack well

πŸ”‘ Primary Key Columns and Compression

Primary key columns appear in every MergeTree data file β€” the primary key index (.idx), the primary key column data (.bin), and the mark file (.mrk). Compression on these columns has outsized impact.

MergeTree Column Files for a Single Part
timestamp.idx
~8KB
Sparse index β€” one entry per granule (8192 rows)
Not compressed (random access needed)
timestamp.mrk
~1KB
Mark file β€” offset pointers into data file
Not compressed
timestamp.bin
Varies
Column data β€” this IS compressed
User-specified codec applies here

Why Primary Key Column Compression Matters More

  • Every query reads primary key columns first β€” they're used for WHERE clause filtering, range scans, and primary key lookups
  • Primary key columns appear in ORDER BY clauses β€” almost every query processes them
  • They're read on every part scan β€” even queries that filter to specific partitions still read primary key data for marking
  • MergeTree sorts by primary key β€” this sorting exposes patterns (monotonic increase, low cardinality) that good codecs exploit

Recommendations for Primary Key Columns

Timestamps in the primary key:
DoubleDelta, ZSTD(1) β€” almost always the right choice. Timestamps in primary keys are sorted and often monotonic.
Low-cardinality dimensions (category, status, country):
ZSTD(1) β€” works well for low-cardinality strings that appear in the primary key. Consider LowCardinality(String) type as well.
High-cardinality IDs in primary key (user_id, order_id):
Delta(8), ZSTD(1) if monotonically increasing. ZSTD(3) if random. Even random IDs compress somewhat with ZSTD.

⏭️ Data Skipping with Compressed Data

ClickHouse's data skipping relies on reading marks (offset pointers) and using the sparse primary key index to determine which granules (8192-row chunks) to decompress and scan. Compression doesn't break this β€” it makes it more important.

πŸ”Ž How Skipping Works with Compression

1
Read Marks
Mark files (.mrk3) are read to find data offsets. Marks are small (~16 bytes each) and not compressed β€” they give ClickHouse the starting byte position for each granule.
2
Primary Key Index Lookup
Sparse index (.idx) is checked. With 8192 rows per granule, an index of ~1000 entries covers 8M rows. The index tells us which granule might contain matching rows.
3
Selective Decompression
Only granules that might contain matching data are decompressed. If the WHERE clause filters to 3 out of 1000 granules, only ~3/1000 of the compressed column data is decompressed.
4
Column Data Scan
Decompressed data is scanned for exact matches. If compression ratio was 10Γ—, the decompressed data is now 10Γ— larger in memory β€” but it was 10Γ— faster to read from disk.

πŸ“Š Compression Impact on Data Skipping

Factor Low Compression (1–2Γ—) Medium Compression (4–8Γ—) High Compression (10–50Γ—)
Disk I/O High β€” reading more raw bytes Balanced β€” good ratio with moderate CPU Low β€” fewer bytes to read
Decompression CPU Low β€” less data to decompress Moderate β€” reasonable workload Higher β€” more data to decompress per match
Cache Efficiency Poor β€” large data doesn't fit in cache Good β€” compressed data fits better Excellent β€” more data fits in cache
Best for Hot data, very frequent access General analytics Wide scans, archival data

πŸ“ Mark Files and Granules

ClickHouse organizes data into granules β€” 8192 consecutive rows that are processed together. Each granule has one entry in the primary key sparse index and one mark (offset pointer).

Rows:
Granule 0
8192 rows
mark[0]
Granule 1
8192 rows
mark[1]
Granule 2
8192 rows
mark[2]
...
Granule N
8192 rows
mark[N]
PK Index:
idx[0]
idx[1]
idx[2]
...
idx[N]

Codecs apply at the granule level β€” each granule can have slightly different compressed size, and codec selection is fixed per column but compression ratio varies per granule based on actual data patterns.

🏷️ Low Cardinality Columns β€” Built-In Efficiency

Low-cardinality columns (columns with few unique values relative to total rows) compress exceptionally well. ClickHouse has both codec-level optimizations and a dedicated LowCardinality data type that encodes values internally.

Option 1: LowCardinality(String) Type

The LowCardinality(String) type tells ClickHouse to use internal dictionary encoding. Strings are replaced with integer indices into a dictionary at write time. This reduces storage dramatically for low-cardinality strings.

clickhouse-sql
-- country_code has only ~200 unique values across 100M rows
CREATE TABLE events (
    event_id UInt64,
    timestamp DateTime64(3) CODEC(DoubleDelta, ZSTD(1)),
    country_code LowCardinality(String),  -- internal dictionary encoding
    status_code UInt16 CODEC(Delta(2), ZSTD(1)),
    value Float64 CODEC(Gorilla)
) ENGINE = MergeTree()
ORDER BY (country_code, timestamp);
Raw String storage:
~200 bytes Γ— 100M = ~19 GB
LowCardinality storage:
~2 bytes Γ— 100M + dictionary = ~200 MB + overhead
Compression ratio:
~95Γ— just from dictionary encoding!

Option 2: Enum / Codes

For columns with very few fixed values (status codes, categories), use Enum8 or Enum16 instead of String. Even lower storage than LowCardinality.

clickhouse-sql
CREATE TABLE http_requests (
    timestamp DateTime64(3) CODEC(DoubleDelta, ZSTD(1)),
    status_code Enum8('200' = 200, '301' = 301, '404' = 404, '500' = 500),
    request_count UInt64 CODEC(Delta(8), ZSTD(1))
) ENGINE = MergeTree()
ORDER BY timestamp;

Option 3: Codec on Regular Column

If you can't change the data type, use codecs to compress low-cardinality numeric data.

clickhouse-sql
-- Integer status codes: 200, 301, 404, 500 (4 unique values)
ALTER TABLE events MODIFY COLUMN status_code CODEC(Delta(2), ZSTD(1));

-- The Delta codec is highly effective for small integers
-- because tiny deltas compress much better than raw values

πŸ“Š Real-World Example: IoT Sensor Time Series

Complete table design for a sensor monitoring system, with codec selection for each column.

clickhouse-sql β€” IoT Sensor Table with Full Codec Strategy
CREATE TABLE iot_sensors (
    -- Primary key columns: frequently used in WHERE, ORDER BY
    device_id UInt64 CODEC(Delta(8), ZSTD(1)),
    timestamp DateTime64(3) CODEC(DoubleDelta, ZSTD(1)),
    
    -- Measurement values: slowly varying floats β†’ Gorilla
    temperature Float64 CODEC(Gorilla, ZSTD(1)),
    pressure Float64 CODEC(Gorilla, ZSTD(1)),
    humidity Float64 CODEC(Gorilla, ZSTD(1)),
    battery_voltage Float64 CODEC(Gorilla, ZSTD(1)),
    
    -- Signal quality: 0-100 integer, low cardinality
    signal_quality UInt8 CODEC(Delta(1), ZSTD(1)),
    
    -- Location: LowCardinality string
    location_id LowCardinality(String),
    
    -- Error code: low cardinality integer β†’ Delta
    error_code UInt16 CODEC(Delta(2), ZSTD(1)),
    
    -- Raw payload: high entropy JSON, ZSTD only option
    raw_payload String CODEC(ZSTD(3))
) ENGINE = MergeTree()
ORDER BY (device_id, timestamp)
PARTITION BY toYYYYMM(timestamp)
TTL timestamp + INTERVAL 90 DAY;

Why these codecs?

  • device_id: Monotonically increasing UInt64 β†’ Delta(8) reduces to tiny deltas
  • timestamp: Regular 1ms intervals β†’ DoubleDelta gets the delta-of-delta close to 0
  • temperature/pressure/humidity: Slowly varying floats β†’ Gorilla XOR gives 8–30Γ— ratio
  • signal_quality: UInt8 (0–100) β†’ Delta(1) handles tiny deltas perfectly
  • location_id: 1000 unique strings β†’ LowCardinality saves ~50Γ— over plain String
  • raw_payload: High-entropy JSON β†’ ZSTD(3) is the only option that helps

Expected compression

Raw data size: ~120 bytes/row
Compressed size: ~6–12 bytes/row
Overall ratio: 10–20Γ— typical
For 1B rows: ~120 GB β†’ ~8–12 GB

πŸ—οΈ MergeTree Storage and Compression Interaction

ClickHouse's MergeTree storage engine is where codec selection actually happens. Understanding how MergeTree stores data helps you make better codec choices.

πŸ“ MergeTree Part File Structure

Each MergeTree part (a batch of sorted rows) contains these files on disk:

Per-Column Files
{column}.bin
Column data β€” THIS is where codecs apply. Compressed column values.
{column}.mrk3
Mark file β€” offset pointers (not compressed). Granule boundaries.
{column}.idx
Primary key sparse index β€” not compressed. One entry per granule.
Primary Key Files
primary.idx
Combined primary key index β€” one entry per granule across all pk columns.
primary.mrk3
Primary key mark file.
Part Metadata
count.txt
Row count in this part.
checksums.txt
File checksums for integrity verification.
columns.txt
Column metadata and codec specifications.

πŸ’‘ Storage Insight: Codec Selection Affects Disk I/O

When ClickHouse reads a column for query processing, it:

  1. Reads mark file (.mrk3) to find granule offsets β€” fast, small, uncompressed
  2. Uses primary key sparse index to skip granules that don't match WHERE clause β€” fast
  3. Reads compressed column data for matching granules only β€” this is where codec matters
  4. Decompresses in-memory β€” CPU cost, but amortized over fast columnar processing
I/O time = (compressed_bytes_read / disk_speed) + (decompressed_bytes / memory_bandwidth Γ— cpu_cycles_per_byte)

Better compression reduces the first term (I/O) at the cost of increasing the second term (decompression CPU). The tradeoff almost always favors better compression in OLAP workloads.

πŸ”„ Part Merges and Compression

When MergeTree merges parts, it recompresses the data. This is when codec choice affects write performance β€” and why you should test codec combinations before production deployment.

βœ“
Merges run in background β€” write performance during ingestion doesn't directly depend on codec compression ratio
βœ“
Better codec = faster merges β€” less data to read and write during the merge process
Changing codecs on existing tables requires ALTER TABLE ... MODIFY COLUMN which triggers a full rewrite
Stack codecs wisely β€” the most effective stacks (DoubleDelta + Gorilla + ZSTD) do more CPU work per byte during merge

πŸ“‹ Codec Quick Reference

Specialized Codecs

CodecBest ForData Types
Delta(N)Timestamps, monotonic integers, regular intervalsInt, DateTime
DoubleDeltaRegular-interval timestamps, constant-rate sequencesInt, DateTime
T64Integer preprocessing before generic codecInt (all sizes)
GorillaTime series floats (temperature, metrics, sensors)Float32, Float64
DEFAULTRandom data (UUIDs, hashes, encrypted blobs)Any

Generic Codecs

CodecRatioSpeedCPU
LZ42–4Γ—Fastest decodeLow
ZSTD(1)3–6Γ—FastMedium
ZSTD(3)4–8Γ—MediumMedium-High
ZSTD(10)5–10Γ—SlowHigh
LZMA5–10Γ—Very slowVery High
Zlib3–5Γ—MediumMedium

πŸ”₯ Common Codec Stacks

DoubleDelta, ZSTD(1) β€” Timestamps (best default for time series)
Gorilla, ZSTD(1) β€” Floating point time series (metrics, sensors)
Delta(8), ZSTD(1) β€” Monotonic UInt64 IDs
T64, Delta(8), ZSTD(1) β€” Counter columns (high-frequency monotonic)
ZSTD(3) β€” High-entropy strings (JSON, URLs, random data)
Delta(2), ZSTD(1) β€” Small integers (status codes, categories)
DoubleDelta, Gorilla, ZSTD(1) β€” Full time series stack (timestamp + float value)