releaseshybriddb3 min read

HybridDB 0.7.0 — filters that run before the scan, analytics that cost nothing until used

The week we found a 352× speedup hiding in plain sight, taught the vector store to filter before searching, and made rollback 20× faster — by batching.

Two small stories from this release, each about a different kind of laziness done right.

Filters that run before the scan

The old way of filtering a vector search was: fetch the top rows unfiltered, then filter in Python, hoping the right answer was still in the window. That’s not a bug you see in a demo — a single-user store behaves fine. It’s a bug you see in production, the day a second tenant arrives: 150 rows from tenant A crowd out tenant B’s answer entirely, and the query returns nothing for a question you know you have the answer to.

0.7.0 pushes scalar-column filters into the Chroma scan itself — where={"user_id": "u2"} means the vector index never sees other tenants’ rows. The Python post-filter stays as the last line of defense (correctness is layered, never assumed), and keyword mode finally honors operator filters ({"$gte": 50}) instead of silently ignoring them.

The fix reads like a one-liner. The debugging that found the one-liner took a month.

Analytics that cost nothing until used

Our analytics story had a hidden tax: opening the database eagerly mirrored every table into DuckDB — even the ones the app never queried analytically. The fix is the lazy version of correctness: mirrors are now created on the first OLAP query. A table you never touch with olap costs you nothing. A table you do? The mirror is fast — 58–352× on real query shapes at 1M rows, because columnar scans over WHERE clauses are what DuckDB is for.

And the write path never noticed: sync overhead is within noise of a 2k-rows/s journaled insert path.

The rollback that got 20× faster by batching

Versioned-table rollback (shipped in 0.6.0) had a performance confession: removal-heavy rollbacks looped per removed row through public delete() — one SQLite connection, one transaction, ~10 statements per row. 1,980ms for 1,000 rows. The fix is set-based: one transaction, chunked DELETE … WHERE pk IN (…), tombstones via executemany. 92ms. Update-heavy restores got the same treatment: 3,931ms → 107ms at 100k rows.

The lesson we keep re-learning: per-row calls to a database are almost never the right way to talk to a database.

Full detail: changelog · the road here began with versioned tables.