Why this matters

Almost every line-of-business application paginates:

  • CRM: SELECT … FROM leads ORDER BY created_at LIMIT 25 OFFSET 500
  • ERP: SELECT … FROM invoices LEFT JOIN customers … LIMIT 50 OFFSET 10000
  • Admin UIs: SELECT … FROM audit_log ORDER BY ts DESC LIMIT 100 OFFSET 50000

On most databases, the cost of OFFSET grows linearly with depth. At 1k rows it’s fine; at 100k it’s a support ticket. HeliosDB Nano 3.12 pushes the OFFSET skip into the storage engine and does it without deserialising skipped rows — so the 10 000th page costs the same as the 1st.


Measured numbers

Same hardware, same connection protocol (psycopg over Unix socket), same table, same queries. 100 000 rows, 20 samples per shape plus 3 warm-up, median reported.

Offset pagination: ORDER BY id LIMIT 10 OFFSET M

Offset depthHeliosDB Nano 3.12PostgreSQL 13Speedup
032 µs49 µs1.5×
10037 µs57 µs1.5×
1 00032 µs144 µs4.5×
10 00036 µs1 194 µs33×
99 99032 µs10 688 µs334×

Nano is flat — depth 99 990 costs the same as depth 0. PostgreSQL scales linearly with offset because every skipped tuple still goes through visibility checks and deserialisation.

Keyset pagination: WHERE id > $last ORDER BY id LIMIT 10

This is the canonical “do-it-right” pagination shape. Nano and PostgreSQL both handle it with index seeks, so the numbers are close.

Cursor positionHeliosDB Nano 3.12PostgreSQL 13
first page31 µs84 µs
after id=10 00132 µs50 µs
after id=99 99033 µs69 µs

LEFT OUTER JOIN + LIMIT + OFFSET

The shape SQLAlchemy emits for select(Lead, Company.name).outerjoin(Company).limit(N).offset(M):

Offset depthHeliosDB Nano 3.12PostgreSQL 13Speedup
038 µs83 µs2.2×
1 00038 µs768 µs20×
10 00034 µs6 838 µs201×

Row-constructor keyset: WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT 10

The idiomatic “Markus Winand” keyset pattern, now supported natively in Nano v3.12:

CursorHeliosDB Nano 3.12PostgreSQL 13Speedup
middle of 100k rows33 µs10 369 µs314×

Why HeliosDB Nano is fast at deep offsets

Three things, in order of impact:

  1. Storage-level skip without deserialising. The RocksDB iterator advances to the offset-th matching key without decoding the bincode tuple, decrypting the page, or resolving dictionary / content-addressed references. On PostgreSQL, every skipped tuple still pays for MVCC visibility checks and heap-tuple decoding.
  2. Top-K operator (sql::executor::topk::TopKOperator). When the plan is Limit(Sort(…)), Nano streams the input through a bounded heap of size k = limit + offset — O(N log k) instead of O(N log N), O(k) memory instead of O(N).
  3. Row-constructor comparison ((created_at, id) < ($1, $2)) is a first-class operator, not a macro-expansion into created_at < $1 OR (created_at = $1 AND id < $2). That means the planner and the evaluator both see the tuple directly, which opens the door to future index-range pushdown.

Reproduce

Every number on this page comes from the bundled bench script. It uses the standard PostgreSQL wire protocol, so it runs unchanged against Nano, PostgreSQL, CockroachDB, YugabyteDB, or anything else that speaks PG wire.

git clone https://github.com/dimensigon/HDB-HeliosDB-Nano
cd HDB-HeliosDB-Nano
pip install 'psycopg[binary]'

# Against HeliosDB Nano (Unix socket, 100k rows)
./target/release/heliosdb-nano start --memory \
    --pg-socket-dir /tmp --port 5432 &
python3 benches/external/pagination_bench.py \
    --host /tmp --port 5432 --user postgres --dbname heliosdb \
    --name "HeliosDB Nano" --rows 100000 --out nano.json

# Against PostgreSQL
PGPASSWORD=postgres python3 benches/external/pagination_bench.py \
    --host localhost --port 5432 --user postgres \
    --name "PostgreSQL 16" --rows 100000 --out pg.json

# Side-by-side
python3 benches/external/pagination_bench.py --compare nano.json pg.json

For Oracle or MS SQL Server, use the same queries through the respective native CLI (sqlplus or sqlcmd) with a timer wrapper — we haven’t bundled the proprietary drivers, but the shapes are identical and easy to port.


What this means for your app

CRMs

The “leads table with filters” page stays sub-millisecond at page 1 000. No keyset migration needed — just LIMIT/OFFSET.

ERPs

Invoice search, order history, audit log — all the “paginate with joins” endpoints run at keyset-speed even with offset.

Admin dashboards

Django admin, Rails admin, React Admin — drop Nano in behind postgresql://, get constant-time paging without changing a line of framework code.

SQLAlchemy apps

Drop the try/except wrappers and Python-side slicing. .limit(N).offset(M) just works at SQL speed, for any M.


Caveats, honestly

  1. PostgreSQL 13, not 16. We tested against the version available on our build box. The fundamental cost model (OFFSET is O(M) heap-tuple decodes) is the same in PG 14/15/16, but exact numbers differ — expect a 10–30 % improvement on newer PG. The qualitative shape (linear growth with offset) is unchanged.
  2. Fsync off on both. We disabled fsync on PostgreSQL to level the bulk-seed time — the metric we care about is read latency, so the flag doesn’t affect the measured numbers, but it’s disclosed for completeness.
  3. Embedded workload shape. 100 000-row table fits entirely in the OS page cache on both systems. For cold-cache reads on a billion-row table, the gap would narrow — storage I/O dominates. We’ll publish that measurement once we have the hardware.
  4. Oracle / MS SQL Server numbers: not yet measured by us. Port the benchmark and send us your results — we’ll publish them here with attribution.

Numbers generated on 2026-04-17. Benchmark script: benches/external/pagination_bench.py. HeliosDB Nano v3.12.0 release build.

Paginate without paying for depth

Drop-in PostgreSQL / MySQL wire compatibility. Your ORM, your queries, your framework — no code changes. Just faster.

Get HeliosDB Nano Learn more