Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 50

Run-Length Encoding for Data Compression

Recommended resources

Hello! Welcome to the next lesson in our module on Columnar Storage and Execution.

In our previous lesson, we explored how dictionary encoding allows for highly efficient execution of aggregate queries by operating on small integer codes instead of large, original values. This principle of "late materialization" is a cornerstone of high-performance analytical systems.

Today, we will add another fundamental compression algorithm to your toolkit: Run-Length Encoding (RLE). Your learning outcome for this lesson is to implement run-length encoding (RLE) for compressing sorted or low-cardinality columns. While dictionary encoding is excellent for columns with a moderate number of unique values, RLE shines in situations where data is sorted or has very few distinct values, leading to long, repetitive sequences.

By the end of this lesson, you will:

  • Understand the core principle of Run-Length Encoding.
  • Recognize why RLE is particularly effective for sorted or low-cardinality data.
  • Be able to implement the RLE algorithm.
  • Learn about common data structures used to store RLE-compressed data.

1. The Core Idea of Run-Length Encoding

Run-Length Encoding is a straightforward and intuitive lossless compression technique. The main idea is to replace consecutive sequences (or "runs") of identical values with a single pair representing the value and the length of the run.

For example, a sequence of characters like: WWWWAAXXXXX

Can be compressed into a shorter representation like: (W, 4), (A, 2), (X, 5)

This simple transformation can lead to significant space savings when long runs are present.

Run-Length Encoding (RLE) Illustration
This image from DuckDB's documentation illustrates the basic concept of RLE. A sequence of repeating characters is replaced by pairs of (character, count).

Now, let's watch a short video that explains this concept within the context of columnar databases and highlights a crucial prerequisite for RLE to be effective.

09 - Database Compression (CMU Databases / Spring 2020)

This segment from a CMU Database Systems lecture provides a clear explanation of RLE and demonstrates why data sorting is so important for its effectiveness.

Please watch from 00:28:15 to 00:34:24. As you watch, focus on two key points: The simple triplet structure used to store an encoded run: (value, start_offset, count). The dramatic difference in compression ratio between unsorted and sorted data.

As the video clearly demonstrated, RLE's performance is highly dependent on the data's layout.

  • For unsorted data with alternating values (e.g., M, F, M, F, M, F...), RLE can actually increase the data size because each "run" has a length of one.
  • For sorted data (e.g., F, F, ..., M, M, ...), RLE achieves a massive compression ratio by collapsing all identical values into a small number of run-length pairs.

This is why RLE is a perfect fit for columns that are either explicitly sorted (like a primary key or a sort key in a data warehouse) or have very low cardinality (e.g., a "status" column with values like 'PENDING', 'COMPLETE', 'FAILED'), as even unsorted low-cardinality data tends to have natural runs.


2. Implementing the RLE Algorithm

Now that you understand the "what" and "why," let's focus on the "how." Given your proficiency in C++, we can look at a direct, practical implementation.

The core logic of an RLE encoder involves iterating through the input data, keeping track of the current value and a counter. When the value changes (or the data ends), the encoder writes the previous value and its count to the output and resets the counter for the new value.

The following resource provides a simple C-style pseudocode for an RLE_Encode function that compresses a character string.

Fast Retrieval with Column Store using RLE Compression ...

This research paper includes a straightforward C++ pseudocode implementation of RLE. We will focus only on this implementation detail.

Please read section 4.1, 'Run Length Encoding Flow Chart and Psuedocode for proposed Work'. Focus specifically on the RLE_Encode function pseudocode. Don't worry about the surrounding text, flow charts, or the search subroutine.

Let's break down the logic from the paper's pseudocode. It's a bit condensed, so let's analyze a cleaner version of the same algorithm:

void RLE_Encode(char* input_str, char* output_str) {
    int input_idx = 0;
    int output_idx = 0;

    while (input_str[input_idx] != '\0') {
        char current_value = input_str[input_idx];
        int run_length = 1;

        // Inner loop: count the length of the run
        while (input_str[input_idx + 1] == current_value) {
            run_length++;
            input_idx++;
        }

        // Write the value and its count to the output
        output_str[output_idx++] = current_value;
        // This converts the integer count to a character, e.g., 3 -> '3'
        output_str[output_idx++] = (char)(((int)'0') + run_length);

        // Move to the next character in the input
        input_idx++;
    }
    output_str[output_idx] = '\0'; // Terminate the output string
}

This implementation encodes a string like "AAABBC" into "A3B2C1". The outer loop picks a character, and the inner loop consumes all subsequent identical characters while incrementing a counter.

Test your understanding!

Imagine you have the input string "XXXXYZZZ". Trace the execution of the RLE_Encode algorithm above. What would be the state of current_value, run_length, and input_idx right after the first run (the 'X's) has been processed and written to the output?

Show answer
  1. The outer loop starts at input_idx = 0. current_value is 'X'.
  2. The inner loop runs three times, incrementing run_length to 4 and input_idx to 3.
  3. The inner loop terminates because input_str[4] ('Y') is not equal to 'X'.
  4. The value 'X' and count '4' are written to the output.
  5. Finally, input_idx is incremented one last time by the outer loop.
  6. The final state before processing 'Y' would be:
    • current_value: 'X' (from the last iteration)
    • run_length: 4 (from the last iteration)
    • input_idx: 4 (ready to process 'Y')

3. Data Structures for RLE Output

The simple string format ("A3B2C1") is just one way to store the RLE output. In real database systems, a more structured approach is common, especially when dealing with data types other than single characters.

A prevalent design is to store the output in two separate arrays:

  1. A values array containing the sequence of unique values that start each run.
  2. A run_lengths array containing the corresponding length of each run.

For an input column [5.5, 5.5, 5.5, 9.0, 9.0, 5.5], this would produce:

  • values: [5.5, 9.0, 5.5] (array of doubles)
  • run_lengths: [3, 2, 1] (array of integers)

This approach has several advantages in a systems context:

  • Type Safety: The values array can be strongly typed (e.g., int32_t, double, string_view), which is cleaner than mixing data and counts in a byte stream.
  • Further Compression: The values and run_lengths arrays can themselves be compressed! For example, if the run_lengths array contains many identical values (e.g., [2, 2, 2, 2, 2]), it could also be run-length encoded. This concept is called cascading compression.
  • Efficient Decompression: As we will see, vectorized hardware instructions (SIMD) can be used to rapidly decompress these two arrays back into the original data.

The following paper describes a system that uses this two-array approach and even discusses vectorized decompression.

BtrBlocks: Efficient Columnar Compression for Data Lakes

The BtrBlocks paper from SIGMOD '23 describes a modern columnar compression framework. We'll look at how it defines and uses RLE.

Read the following short parts: In section 2.2, read 'RLE & One Value' to see the simple (value, count) definition. In section 3.2, read the paragraph 'Cascading compression: An example'. This shows exactly how an input array is decomposed into a value array and a run length array. In section 5, read the paragraph 'Run Length Encoding' and look at Listing 3 (decodeRLEAVX). This gives you an applied view of how a system might implement highly optimized decompression for RLE.

This look into a real system shows that RLE is not just a textbook algorithm but a practical tool with sophisticated implementations designed for modern hardware.


Conclusion

Today you've explored Run-Length Encoding, a simple yet powerful compression technique that is highly effective in columnar databases. You've gone from the high-level concept to a concrete implementation and seen how it's used in real-world systems.

Key Takeaways:

  • Run-Length Encoding (RLE) compresses data by replacing consecutive runs of identical values with a (value, count) pair.
  • RLE's effectiveness is maximized on sorted data or columns with low cardinality, as these conditions create the long runs that RLE can exploit.
  • The core implementation involves a simple loop that iterates through the data, counts consecutive values, and writes the resulting pairs.
  • While a simple string can represent the output, a more robust system design uses two separate arrays for values and run_lengths, enabling type safety and further optimizations.

Preview of the Next Lesson

Having an RLE-compressed column is the first step. The next is to use it. In our next lesson, we will address the learning outcome: Execute a selection query with a predicate on a run-length encoded (RLE) column. You will learn how to find all rows matching a predicate like WHERE status = 'COMPLETE' without having to decompress the entire column first, further building on the theme of "late materialization".