Skip to content

Qirava DMS docs

Performance: prepared-plan cache

Parsing and planning a query is fast, but workers replay the same route queries over and over, so the engine keeps a prepared-plan cache: a map from exact QQL text to the parsed AST (Stmt). A repeat of the same text clones the cached plan under a shared read lock — no re-tokenizing or re-parsing. The cache is bounded; when full it keeps the warm entries instead of churning. Bound (parameterized) queries deliberately bypass the cache because their resolved AST depends on the bindings, not the text alone. This page explains how the cache works and when it helps.

How the cache works#

The cache is a HashMap<String, Arc<Stmt>> keyed by the exact query text. On execute(qql), the engine first checks the cache under a shared read lock; a hit clones the cached Stmt (cheap, shared) and skips lexing/parsing. A miss parses the text and best-effort inserts the result with a non-blocking try_write, so a busy cache never blocks the query path.

  execute(qql)
     ├─ read lock: cache.get(qql)?  ── hit ─▶ clone Arc<Stmt>, run
     └─ miss ─▶ parse(qql) ─▶ try_write insert (if under cap) ─▶ run
Plan cache lookup

Bound (capacity) behavior#

The cache holds at most 1024 plans. While under the cap, misses are stored; once at the cap, the engine KEEPS the existing warm entries rather than clearing wholesale — clearing-and-recloning on every miss hurt varying-query workloads (like point reads with a changing _id), and skipping the clone-to-store is cheaper. Inserts use a best-effort try_write that simply skips storing if the lock is contended.

Bound queries bypass the cache#

execute_bound(qql, binds) resolves :name placeholders into the AST at parse time, so the resulting plan depends on the bindings, not on the text alone. Caching by text would be wrong (two calls with the same text but different binds produce different plans), so bound queries always re-parse and never touch the cache.

Request

-- u has rows { id:1, age:20 } and { id:2, age:40 }
SELECT id FROM u WHERE age >= 30   -- miss: parse + cache
SELECT id FROM u WHERE age >= 30   -- hit: cloned plan, same result
SELECT id FROM u WHERE age >= 10   -- different text -> distinct plan

Response

// first two -> count 1 (only age 40) ; third -> count 2

Practical guidance#

  • Reuse the SAME query text (with bound params for the varying values) to get both cache hits AND injection safety — note that the bound path skips the text cache but is still cheap.
  • Interpolating changing literals into the text (e.g. WHERE _id = 5 then _id = 6) produces a distinct cache entry per literal; prefer :id binds for varying values.
  • The cache is a transparent performance optimization — results are always identical to an uncached parse.