Skip to content
About
Main content

Make memcmp sort your numbers correctly

Measure your machineNeeds Python

Problem

Key-value stores and sort-based engines compare keys as raw bytes (memcmp: byte by byte, unsigned). For that to sort numbers correctly, the encoding must be order-preserving. is not: negative numbers start with a 1 bit and sort after the positives. Flipping the sign bit fixes integers. Floats need more: flip the sign bit of positive numbers and invert every bit of negative ones, so that more-negative values sort first. Composite keys need a separator that cannot appear inside a field, or ("ab", "c") and ("a", "bc") become the same bytes.

Prove the sign-bit flip and the float rule by testing them. (Python compares bytes the same way as memcmp: byte by byte, unsigned.) Then try the composite key ("ab","c") versus ("a","bc") with and without the 0x00 0xFF escape and 0x00 0x01 terminator, and check that ordering and uniqueness both hold.

The question. Which encodings make byte order equal numeric order, and does escaping keep composite keys unique and ordered?


Predict

Concatenating the parts of composite keys without escaping: are the keys still unique?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    make-memcmp-sort-your-numbers-correctly.py52 linesDownload
    View the code
    make-memcmp-sort-your-numbers-correctly.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Make memcmp sort your numbers correctly.
    
    Encode signed integers and floats so that comparing the bytes (unsigned,
    byte by byte, like memcmp) gives numeric order: flip the sign bit of an
    integer; for a float, invert all bits if negative, else set the sign bit.
    Then composite string keys, with and without an escape and a terminator.
    """
    import argparse, json, random, struct
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--values", type=int, default=10_000, help="Values per test.")
    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)
    MASK = (1 << 64) - 1
    def enc_i64(n): return struct.pack(">Q", (n & MASK) ^ (1 << 63))
    def enc_f64(x):
        b = struct.unpack(">Q", struct.pack(">d", x))[0]
        b = (~b & MASK) if (b >> 63) else (b | (1 << 63))
        return struct.pack(">Q", b)
    ints = [random.randint(-10**12, 10**12) for _ in range(args.values)]
    floats = [random.uniform(-1e6, 1e6) for _ in range(args.values)]
    naive = lambda n: struct.pack(">q", n)
    naive_f = lambda x: struct.pack(">d", x)
    
    def concat(parts): return b"".join(s.encode() for s in parts)
    def escaped(parts): return b"".join(s.encode().replace(b"\x00", b"\x00\xff") + b"\x00\x01" for s in parts)
    keys = [("ab", "c"), ("a", "bc"), ("a", "b"), ("ab", ""), ("a", "")]
    rows = [
        {"test": "integers, plain two's complement", "sorted_correctly": sorted(ints, key=naive) == sorted(ints)},
        {"test": "integers, sign bit flipped", "sorted_correctly": sorted(ints, key=enc_i64) == sorted(ints)},
        {"test": "floats, plain IEEE bytes", "sorted_correctly": sorted(floats, key=naive_f) == sorted(floats)},
        {"test": "floats, sign rule", "sorted_correctly": sorted(floats, key=enc_f64) == sorted(floats)},
        {"test": "composite keys, plain concatenation unique", "sorted_correctly": len({concat(k) for k in keys}) == len(keys)},
        {"test": "composite keys, escaped + terminated unique", "sorted_correctly": len({escaped(k) for k in keys}) == len(keys)},
        {"test": "composite keys, escaped + terminated order", "sorted_correctly": sorted(keys, key=escaped) == sorted(keys)},
    ]
    for r in rows:
        r["result"] = "yes" if r.pop("sorted_correctly") else "no"
        print(f"{r['test']:<46} {r['result']}")
    print("('ab','c') and ('a','bc') concatenated:", concat(("ab", "c")), concat(("a", "bc")), "-> the same bytes")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"tests": rows}}, f)
    
  3. 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 make-memcmp-sort-your-numbers-correctly.py
    uv run make-memcmp-sort-your-numbers-correctly.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--values10,000 Values per test.
--seed0 Random seed.

Our run

Does byte order match the intended order?

Does byte order match the intended order?
EncodingCorrect
integers, plain two's complementno
integers, sign bit flippedyes
floats, plain IEEE bytesno
floats, sign ruleyes
composite keys, plain concatenation uniqueno
composite keys, escaped + terminated uniqueyes
composite keys, escaped + terminated orderyes
Reference runApple M2 · 8 cores · 8 GB
Output · uv run make-memcmp-sort-your-numbers-correctly.py
integers, plain two's complement               no
integers, sign bit flipped                     yes
floats, plain IEEE bytes                       no
floats, sign rule                              yes
composite keys, plain concatenation unique     no
composite keys, escaped + terminated unique    yes
composite keys, escaped + terminated order     yes
('ab','c') and ('a','bc') concatenated: b'abc' b'abc' -> the same bytes
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What we found

Plain two's complement sorted integers wrongly (no); with the sign bit flipped, correctly (yes). Raw IEEE bytes sorted floats wrongly (no); with the sign rule, correctly (yes). Plain concatenation made composite keys collide (unique: no); escaped and terminated, they were unique (yes) and in order (yes).


Read it

An moves the comparison logic into the bytes, so the storage engine never needs to know the types. The cost is that the encoding is now part of the data's meaning: change it and every stored key sorts differently. The two failures are silent in different ways. Wrong sign handling puts negative values in the wrong place in every range scan, and unescaped composite keys make two different keys equal.