Skip to content
About
Main content

Z-order floor

Simulate a modelNeeds Python with NumPy

Problem

With U units and min/max statistics, sorting on dimension 1 makes a query on dimension 1 touch about one unit and a query on dimension 2 touch all U. Z-ordering two dimensions gives each unit a square-ish box, so a thin query on either dimension touches about √U units: worse than the best case for each, far better than the worst. For a workload split evenly between the two, the sorted layout costs about 0.5·U per query and Z-order about √U.

Simulate U = 4,096 units over 2 dimensions with random points. Compare three layouts: sorted on dimension 1, Z-order (interleave the bits of the two integer coordinates) and random. For a query on only dimension 2, count the units that overlap the range; then run a dimension-1 query. Compute the cost of a 50/50 workload and check it against √U versus 0.5·U.

The question. How many units does each layout touch for each dimension, and what does a 50/50 mix cost?


Predict

With 4,096 units, how many does Z-order touch for a dimension-1 query?

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.

    z-order-floor.py48 linesDownload
    View the code
    z-order-floor.py
    # /// script
    # requires-python = ">=3.10"
    # dependencies = ["numpy"]
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """The Z-order floor.
    
    A million random 2-D points split into U = 4,096 units under three layouts:
    sorted on dimension 1, Z-order (bits of the two coordinates interleaved) and
    random. Counts the units whose min/max box overlaps a thin slab query on
    dimension 2 alone, and on dimension 1 alone; then the cost of a 50/50 mix,
    against √U and 0.5·U.
    """
    import argparse, json
    import numpy as np
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--units", type=int, default=4096, help="Units U.")
    p.add_argument("--points", type=int, default=1 << 20, help="Points.")
    p.add_argument("--queries", type=int, default=50, help="Queries per dimension.")
    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()
    
    rng = np.random.default_rng(args.seed); pts = rng.integers(0, 1 << 16, (args.points, 2)).astype(np.uint64)
    def z(x):
        out = np.zeros(len(x), dtype=np.uint64)
        for b in range(16):
            out |= ((x[:, 0] >> np.uint64(b)) & np.uint64(1)) << np.uint64(2 * b); out |= ((x[:, 1] >> np.uint64(b)) & np.uint64(1)) << np.uint64(2 * b + 1)
        return out
    orders = {"sorted on dimension 1": np.lexsort((pts[:, 1], pts[:, 0])), "Z-order": np.argsort(z(pts), kind="stable"), "random": rng.permutation(args.points)}
    rows = []
    for name, o in orders.items():
        u = pts[o].reshape(args.units, -1, 2); mn, mx = u.min(axis=1), u.max(axis=1)
        cost = {}
        for dim in (0, 1):
            vals = rng.integers(0, 1 << 16, args.queries)
            cost[dim] = float(np.mean([((mn[:, dim] <= v) & (v <= mx[:, dim])).sum() for v in vals]))      # a very thin slab: one value
        rows.append({"layout": name, "dim1_query": cost[0], "dim2_query": cost[1], "mix_50_50": (cost[0] + cost[1]) / 2})
    summary = {"sqrt_u": args.units ** 0.5, "half_u": args.units / 2}
    for r in rows: print(f"{r['layout']:<22} dimension-1 query {r['dim1_query']:7.1f} units   dimension-2 query {r['dim2_query']:7.1f}   50/50 mix {r['mix_50_50']:7.1f}")
    print(f"√U = {summary['sqrt_u']:.0f}, 0.5·U = {summary['half_u']:.0f}")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"summary": summary, "tables": {"layouts": 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 z-order-floor.py
    uv run z-order-floor.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--units4096 Units U.
--points1 << 20 Points.
--queries50 Queries per dimension.
--seed0 Random seed.

Our run

Units touched per query (of 4,096)

Units touched per query (of 4,096)
LayoutDimension-1 queryDimension-2 query50/50 mix
sorted on dimension 11.064,0902,050
Z-order13390.9112
random4,0904,0104,050
Reference runApple M2 · 8 cores · 8 GB
Output · uv run z-order-floor.py
sorted on dimension 1  dimension-1 query     1.1 units   dimension-2 query  4093.4   50/50 mix  2047.2
Z-order                dimension-1 query   133.1 units   dimension-2 query    90.9   50/50 mix   112.0
random                 dimension-1 query  4094.3 units   dimension-2 query  4005.7   50/50 mix  4050.0
√U = 64, 0.5·U = 2048
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-20

What we found

Sorted on dimension 1, a dimension-1 query touched 1.06 units and a dimension-2 query 4.09K, a 50/50 mix 2.05K per query (0.5·U = 2.05K). Z-order touched 133 and 90.9, a mix of 112, within a factor of two of √U = 64. Random order touched nearly everything for both.


Read it

Z-order trades the best case for the average: no query is as cheap as on its own sort key, and none is as expensive as on the other's. When the workload is split, √U beats 0.5·U by a wide margin; when one dimension dominates, sort on it.