Skip to main content
unbound mongodb at scale

Measuring Compression Impact on Storage and Query Latency

4 min read Chapter 44 of 72

Measuring Compression Impact on Storage and Query Latency

The Symptom

The telemetry collection has grown to 68 GB on disk using Snappy compression. Storage costs are increasing linearly with data volume. The team wants to reduce storage without significantly impacting query performance.

The Cause

Snappy prioritizes speed over compression ratio. For the telemetry data (repetitive numeric values, repeated field names, similar document structures), a better algorithm can achieve significantly higher compression ratios because the data has high redundancy within pages.

The Benchmark

Create identical collections with different compression algorithms and measure storage and query performance:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 2, time = 10)
@Measurement(iterations = 3, time = 15)
@Fork(1)
@State(Scope.Benchmark)
public class CompressionBenchmark {

    private MongoCollection<Document> snappyCollection;
    private MongoCollection<Document> zstdCollection;
    private MongoCollection<Document> noneCollection;

    @Setup
    public void setup() {
        MongoClient client = MongoClients.create("mongodb://localhost:27017");
        var db = client.getDatabase("compression_bench");

        snappyCollection = db.getCollection("readings_snappy");
        zstdCollection = db.getCollection("readings_zstd");
        noneCollection = db.getCollection("readings_none");
    }

    @Benchmark
    public List<Document> readSnappy() {
        return snappyCollection.find(
            Filters.eq("sensorId", "sensor-00042")
        ).sort(Sorts.descending("ts"))
         .limit(100)
         .into(new ArrayList<>());
    }

    @Benchmark
    public List<Document> readZstd() {
        return zstdCollection.find(
            Filters.eq("sensorId", "sensor-00042")
        ).sort(Sorts.descending("ts"))
         .limit(100)
         .into(new ArrayList<>());
    }

    @Benchmark
    public List<Document> readNone() {
        return noneCollection.find(
            Filters.eq("sensorId", "sensor-00042")
        ).sort(Sorts.descending("ts"))
         .limit(100)
         .into(new ArrayList<>());
    }
}

Storage comparison for 10 million identical documents:

CompressionData sizeIndex sizeTotalCompression ratio
None3.4 GB480 MB3.88 GB1.0x
Snappy1.36 GB480 MB1.84 GB2.5x (data only)
Zstd0.68 GB480 MB1.16 GB5.0x (data only)

Query latency comparison (100 documents, warm cache):

Benchmark                           Mode  Cnt    Score    Error  Units
CompressionBenchmark.readNone      avgt    3  280.000 ± 15.000  us/op
CompressionBenchmark.readSnappy    avgt    3  290.000 ± 18.000  us/op
CompressionBenchmark.readZstd      avgt    3  295.000 ± 16.000  us/op

With a warm cache, compression has near-zero impact on read latency because data is decompressed in cache. The 10-15us difference is within the error margin.

Query latency with cold cache (pages must be read from disk and decompressed):

Compressionp50 (cold)p95 (cold)p99 (cold)
None1.2ms3.5ms8.0ms
Snappy1.1ms3.2ms7.5ms
Zstd1.0ms2.8ms6.5ms

Compressed data is actually faster on cold reads because less data is read from disk. Zstd reads 5x less data, and the decompression time (negligible on modern CPUs) is less than the I/O time saved.

The Fix

Switch the telemetry collection to Zstd compression. This requires recreating the collection or using compact with the new settings:

// Option 1: compact with new compression (MongoDB 4.4+)
db.runCommand({
  compact: "readings",
  force: true
  // Note: compact does NOT change compression. Must recreate collection.
})

// Option 2: Create new collection, copy data
db.createCollection("readings_new", {
  storageEngine: {
    wiredTiger: { configString: "block_compressor=zstd" }
  }
})

// Copy data in batches
var batch = [];
db.readings.find().forEach(doc => {
  batch.push(doc);
  if (batch.length >= 10000) {
    db.readings_new.insertMany(batch);
    batch = [];
  }
});
if (batch.length > 0) db.readings_new.insertMany(batch);

// Rename
db.readings.renameCollection("readings_old");
db.readings_new.renameCollection("readings");

For new collections, set Zstd as the default:

// FAST: Create new collections with Zstd compression
database.createCollection("readings",
    new CreateCollectionOptions().storageEngineOptions(
        new Document("wiredTiger",
            new Document("configString", "block_compressor=zstd"))
    ));

The Proof

After migrating the telemetry collection from Snappy to Zstd:

MetricSnappyZstd
Data file size68 GB27 GB
Storage reductionbaseline60%
Read p50 (warm cache)8ms8ms
Read p99 (cold cache)7.5ms6.5ms
Write p503ms3.5ms
Write p9915ms18ms
CPU during checkpoint12%18%

The Trade-off

Zstd uses more CPU during writes (checkpoint and eviction) because compression is CPU-intensive. The 6% CPU increase during checkpoints is acceptable on a 16-core server but may be problematic on a 4-core instance. Write p99 increased from 15ms to 18ms due to the compression overhead during page writes.

For write-heavy workloads on CPU-constrained servers, Snappy remains the better choice. For read-heavy or storage-constrained workloads, Zstd provides significant storage savings with minimal read latency impact.

Index compression is separate from data compression. WiredTiger uses prefix compression for indexes by default, which compresses common key prefixes. This is orthogonal to block compression and should always be enabled.