---
title: "HeliosDB Lite v3.6.3 — From v3.6.1, Through the Batched Executor Migration"
description: "v3.6.1 → v3.6.3 in one comparison. Window functions −25%, INSERT throughput −17 to −31%, CTE −13%. Time-travel index validated at 39 000× over linear scan. Four silently-red regression tests now green."
date: 2026-04-26
updated: 2026-04-26
tag: "Benchmarks"
canonical: https://www.heliosdb.com/blog/lite-3-6-3-batched-executor-migration.html
---

# HeliosDB Lite v3.6.3 — From v3.6.1, Through the Batched Executor Migration

Four days ago we [published the v3.6.1 monthly review](https://www.heliosdb.com/blog/lite-3-6-1-april-2026-performance-review.html). Two releases have shipped since: v3.6.2 (window aggregates + bench un-rot + crypto lazy init) and v3.6.3 (the batched-executor migration that the v3.6.1 release notes flagged as the next big lever). This post is the joined-up view: **v3.6.1 → v3.6.3**, three weeks of release work in one comparison.

Headline: window functions are 25% faster than v3.6.2 once they actually execute (more on that below); INSERT throughput is 17–31% faster across single-row and multi-row VALUES; CTE is 13% faster; the time-travel optimisation bench has been resurrected and shows the v3.6.1 BTreeMap snapshot index is genuinely **39 000× faster** than linear scan at 10K versions; four silently-red regression tests are now green.

## What v3.6.2 Shipped (April 24)

v3.6.2 was a smaller release between v3.6.1 and v3.6.3. The pieces relevant to this benchmark conversation:

- **Window aggregates wired through.** The v3.6.1 caveats called out that the bench was running `SUM()`-as-a-window-function but the engine's window layer only supported ranking / offset / positional functions — so the 83 µs number was error-path overhead, not a genuine cost. v3.6.2 lands aggregate window functions; the bench now genuinely executes the workload, and v3.6.2's window-function row reports **99 µs** as the honest cost. v3.6.3 then drops it to 74 µs on the back of the batched-executor migration.
- **Bench un-rot.** Several benches had silently rotted across the v3.6.0 RocksDB → NativeBackend cutover. v3.6.2 + v3.6.3 between them got every registered cargo bench compiling against default features.
- **Crypto lazy init** — PBKDF2 iterations now compute on first use rather than at startup; ~30 ms shaved off cold-start time.
- **Cursor split** — cursor state separated from the executor session so cursor restore on failover (Lite-side hook) gets a cleaner contract.
- **Bounds hardening (6 tests) and crash recovery hardening (12 tests)** pinned in v3.6.2 — still green in v3.6.3.

## v3.6.3 (April 26) — The Batched Executor Migration

The v3.6.1 pass introduced the `RecordBatch` type and a default `next_batch()` impl on `PhysicalOperator` (which loops `next()`). v3.6.3 actually wires the protocol through:

| Operator | Has `next_batch()` | What you get |
| --- | --- | --- |
| **ScanOperator** | yes | Single timeout check / batch instead of per-row; projection map dereferenced once. |
| **FilterOperator** | yes | Pulls a child batch, evaluates the predicate in a tight loop, returns a smaller batch — keeps L1 hot for the predicate evaluator. |
| **ProjectOperator** | yes | Same loop benefit; falls back to per-row for DISTINCT / DISTINCT ON edge cases (state-dependent). |
| **LimitOperator** | yes | Drops offset prefix in O(1) on whole batches; truncates the final batch to the remaining quota. |
| **Top-level `Executor::execute`** | yes | Drains via `next_batch()` so the chain above actually fires. |

Operators that still pull row-by-row (Aggregate, Sort, HashJoin) inherit the default batch impl, which is just `next()` in a loop. They remain correct; they just don't get the additional perf in this release.

## v3.6.1 → v3.6.3 — Joined-Up Comparison

The most useful view is end-to-end: where post 14 left you in v3.6.1, vs where v3.6.3 lands today. v3.6.2 is included as the intermediate point so the source of each delta is unambiguous. Bold rows are the big wins from the batched-executor migration.

| Category | v3.6.1 | v3.6.2 | v3.6.3 | Net (v3.6.1 → v3.6.3) |
| --- | --- | --- | --- | --- |
| CREATE TABLE | 71.5 µs | 71.5 µs | 74.5 µs | +4% |
| CREATE INDEX | 180.0 µs | 180.0 µs | 190.0 µs | +6% |
| REFRESH MATERIALIZED VIEW | 43.8 ms | 43.8 ms | **39.85 ms** | **−9%** |
| INSERT single row | 37.3 µs | 37.3 µs | **30.8 µs** | **−17%** |
| INSERT multi-row VALUES | 234.0 µs | 234.0 µs | **162.5 µs** | **−31%** |
| INSERT…SELECT | 53.5 ms | 53.5 ms | 53.0 ms | −1% |
| UPSERT | 662.3 µs | 662.3 µs | **618.0 µs** | **−7%** |
| UPDATE with subquery | 315.0 ms | 315.0 ms | **293.5 ms** | **−7%** |
| Point lookup | 68.5 µs | 68.5 µs | **60.5 µs** | **−12%** |
| Aggregation | 34.9 µs | 34.9 µs | 49.3 µs ⚠ | +41% (noise — see caveats) |
| INNER JOIN | 281.5 µs | 281.5 µs | **255.5 µs** | **−9%** |
| LEFT JOIN | 252.5 µs | 252.5 µs | **235.5 µs** | **−7%** |
| Multi-table JOIN (4) | 470.5 µs | 460.0 µs | **446.5 µs** | **−5%** |
| Scalar subquery | 29.7 µs | 29.7 µs | **25.0 µs** | **−16%** |
| CTE | 810 µs | 810 µs | **703.5 µs** | **−13%** |
| Recursive CTE | 35.1 µs | 35.1 µs | 33.7 µs | −4% |
| Window functions | 83 µs ⚠^1 | 99.0 µs | **74.0 µs** | **genuine execution, batched** |
| UNION | 25.5 µs | 25.5 µs | 23.2 µs | −9% |
| DISTINCT | 21.1 µs | 21.1 µs | 39.1 µs ⚠ | +85% (noise) |
| ORDER BY + LIMIT | 147.5 µs | 147.5 µs | 167.5 µs ⚠ | +14% (noise) |
| CASE expressions | 33.6 µs | 33.6 µs | 32.5 µs | −3% |
| Transaction control | 47.0 µs | 47.0 µs | **41.3 µs** | **−12%** |
| Prepared statements | 142.0 µs | 142.0 µs | 137.5 µs | −3% |

^1 v3.6.1 reported window functions at 83 µs but the bench was using `SUM()` as a window function while the engine's window layer only supported ranking / offset / positional functions — that 83 µs was error-path overhead. v3.6.2 added aggregate window functions, the bench now genuinely executes (99 µs), and v3.6.3's batched executor brings it to 74 µs.

## Deep Dive 1: Where the v3.6.3 Wins Concentrate

The categories that improved most are the ones whose execution chain is dominated by Scan / Filter / Project / Limit:

- **Window functions (Scan → Filter → Project → window): −25%** in v3.6.3 vs v3.6.2.
- **Multi-row INSERT (Project of VALUES rows): −31%.** Big win because each VALUES row is now batched through Project rather than walked one-by-one.
- **INSERT single (Project): −17%.**
- **JOINs (Filter feeds the build/probe sides): −7% to −9%.** HashJoin itself isn't batched yet, but its inputs are.
- **Point lookup, scalar subquery, transaction control: −12% to −16%.**

Categories that didn't move (Aggregate, Sort, HashJoin themselves) are exactly the ones whose own operators still iterate row-by-row. The v3.6.3 release notes call this out explicitly — those operators remain correct, they just inherit the default batch impl which is `next()` in a loop. Migrating them is the next big lever.

## Deep Dive 2: time_travel_optimization Bench Rewrite

This one is notable because it validates a previous release's claim that we couldn't measure at the time. The v3.6.0 RocksDB → NativeBackend cutover broke the `time_travel_optimization` bench — it called `SnapshotManager::new(Arc<rocksdb::DB>)`, which no longer exists. The bench had been silently red since v3.6.0.

v3.6.3 rewrites it to use `SnapshotManager::new(Arc<NativeBackend>)` and un-feature-gates it. The numbers validate the v3.6.1 BTreeMap snapshot index claim:

| Versions | Linear scan | Indexed (BTreeMap) | Speedup |
| --- | --- | --- | --- |
| 100 | 16.6 µs | 47 ns | **350×** |
| 1 000 | 165 µs | 47 ns | **3 500×** |
| 10 000 | 1.81 ms | 46 ns | **39 000×** |

The indexed path is constant-time, validating that the v3.6.1 `wall_clock_index: BTreeMap<i64 wall_ts, u64 snap_ts>` side-index makes `resolve_as_of` O(log n). At 10 000 versions a linear scan is ~40 000× slower than the indexed lookup. For long-running deployments with extensive time-travel history, this is the difference between AS-OF queries staying sub-millisecond vs degrading linearly with retention.

## Deep Dive 3: The Silently-Red Tests That Are Now Green

v3.6.3 also fixes four regression tests that had been silently red on `main`:

- **3 SCRAM auth tests**. Three assertions had been silently red since the PBKDF2 iteration count was raised from 4 096 to 100 000 (NIST SP 800-132 compliance, v3.5.8). The asserts hard-coded the old iteration count. Now updated.
- **1 VACUUM test** — a regression test for `VACUUM` on a missing table. `VACUUM nonexistent_table` previously returned `Ok(...)` with zero rows affected (silent no-op on typo). The handler now errors with `VACUUM: table 'X' does not exist`. Matches PostgreSQL behaviour.

Both are correctness fixes in their own right: the auth tests validate iteration-count compliance, and the VACUUM no-op was a small but real footgun where typos in admin scripts wouldn't surface.

## The Three Flagged Regressions — Are They Real?

Three rows in the v3.6.3 column went the wrong way: Aggregation +41%, DISTINCT +85%, ORDER BY+LIMIT +14%. Worth being explicit about these.

All three are sub-100 µs queries running 20 iterations, with high coefficient of variation on the small fixture. Multi-run medians on the same host show all three within ±5% of v3.6.2 — which is to say, the noise on these particular shapes (small-cardinality sort, small-cardinality aggregation) drowns out any signal that would explain a real regression. The v3.6.3 release notes call this out.

The honest framing: **we don't have a story for why these specific runs landed where they did, and we're not going to invent one.** The fixture is too small to draw a regression conclusion from, and 20 iterations is not a release-bar number of samples. We are tracking these for the next benchmark cycle on a larger fixture — if any of them stays elevated under proper sampling, we'll dig in.

## Test Totals

- **Library tests: 1580 / 1580** passed (up from 1575 in v3.6.0; the new tests come from window aggregates, bounds hardening, crash-recovery hardening, delete fast-path, and the time-travel rewrite).
- All cargo benches now compile against default features (5 of 7 run; `conflict_detection` needs `--features sync-experimental`; `sync_benchmark` uses raw RocksDB API and needs a rewrite). v3.6.3 disabled cargo's bench auto-discovery (`autobenches = false`) so the four unregistered legacy benches in `benches/` don't break the workspace.

## Reproducibility

```
cargo test --release --test performance_trace_benchmark -- --nocapture
cargo test --release --test window_aggregate_tests -- --nocapture
cargo test --release --test delete_fastpath_bench -- --nocapture

cargo bench --bench time_travel_optimization 'comparison'
cargo bench --bench simd_benchmark 'l2_distance/simd'
```

## What's Next

The roadmap implied by this release is clear:

- **Migrate Aggregate, Sort, and HashJoin to `next_batch()`.** The default impl is correct but row-by-row; these are the operators that didn't move in v3.6.3.
- **Re-measure the three flagged regressions** on a larger fixture (1000+ iterations) to either confirm they're noise or find the actual cause.
- **Resurrect `sync_benchmark`** against NativeBackend — the rewrite that `time_travel_optimization` got in v3.6.3.

[Earlier in this series:](https://www.heliosdb.com/blog/lite-3-6-1-april-2026-performance-review.html) the v3.6.1 monthly review, where we promised the batched executor as the next big lever. v3.6.3 is the delivery.

HeliosDB Lite is source-available under SSPL-1.0. Repository, full CHANGELOG, and benchmark code: [github.com/dimensigon/HDB-HeliosDB-Lite](https://github.com/dimensigon/HDB-HeliosDB-Lite).
