lance-format / lance-format/lance

Huge performance regression between 2.0 and 2.1 formats in ordered scan

Open
#7,860 2 comments 0 reactions 1 assignee View on GitHub

@westonpace is already working on this.

Since Jul 22, 2026.

performance
Dominant language
Rust
Stars
7.1k
Forks
852
Avg merge
3d 18h
Merged PRs (30d)
272

Description

I have a particular data and query shape, that has a huge regression between the 2.0 and 2.1 on-disk formats when using the default materialization settings.

The data shape looks something like:

  • id column (high cardinality, random)
  • type column (very low cardinality, in this repro only a single value)
  • a decent number of sparse list columns (mostly null), with medium cardinality

Then a query reads the table with ordering by id, while filtering by type (which does not actually filter out any rows, but is needed to trigger the issue).
This performs a lot slower (~80x) with the 2.1 format compared to 2.0.

50000 rows, 52 columns, pylance 8.0.0
format 2.0 late_materialization= None: total   0.05s, first batch  0.031s, median of rest  0.003s
format 2.1 late_materialization= None: total   4.19s, first batch  0.688s, median of rest  0.679s

Forcing early materialization fixes the performance of the query on either version:

50000 rows, 52 columns, pylance 8.0.0
format 2.0 late_materialization=False: total   0.03s, first batch  0.018s, median of rest  0.002s
format 2.1 late_materialization=False: total   0.02s, first batch  0.012s, median of rest  0.002s

It also triggers with sparse string columns instead of sparse lists, to a lesser extent, but still significant (~10x instead of ~80x)

Here is a repro script:

import random
import shutil
import statistics
import time
from pathlib import Path

import lance
import pyarrow as pa

num_rows = 50_000
value_pool = [f"value_{i}" for i in range(1000)]

cols = {
    "id": pa.array([random.randint(0, num_rows) for _ in range(num_rows)]),
    "kind": pa.array(["a"] * num_rows),
}
for c in range(50):
    cols[f"attr_{c:03}"] = pa.array(
        [[random.choice(value_pool)] if random.random() < 0.1 else None for _ in range(num_rows)],
        type=pa.list_(pa.utf8()),
    )
table = pa.table(cols)

for version in ("2.0", "2.1"):
    path = Path(f"/tmp/lance_sorted_scan_{version}")
    if path.exists():
        shutil.rmtree(path)
    lance.write_dataset(table, path, data_storage_version=version, max_rows_per_file=num_rows)

print(f"{num_rows} rows, {len(table.schema)} columns, pylance {lance.__version__}")
for version in ("2.0", "2.1"):
    ds = lance.dataset(f"/tmp/lance_sorted_scan_{version}")
    for late in (False, None):
        scanner = ds.scanner(order_by=["id"], filter="kind = 'a'", batch_size=8192, late_materialization=late)
        t0 = prev = time.perf_counter()
        times = []
        for batch in scanner.to_batches():
            now = time.perf_counter()
            times.append(now - prev)
            prev = now
        print(
            f"format {version} late_materialization={late!s:>5}: total {time.perf_counter() - t0:6.2f}s, "
            f"first batch {times[0]:6.3f}s, median of rest {statistics.median(times[1:]):6.3f}s"
        )

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.