Performance tuning

How to tune a Relata deployment for throughput, latency, or cost. Numbers below are starting points measured on commodity hardware — always benchmark your own workload.

Three levers

LeverKnob classGoal
MemoryRELATA_*_RAM_MB, RELATA_*_MAX_BYTESHot data resident
Parallelismrayon (graph), worker pools, scatter-gatherUse all cores
Disk I/Ocompaction strategy, WAL group-commit, spill formatReduce fsync pressure

Memory is usually the highest-impact knob. Parallelism is free if your data fits in RAM. Disk I/O matters when it doesn't.

Adaptive sizing (let Relata choose)

If you're unsure, unset the budget env vars. Relata probes the hardware at startup and splits a single ~75%-of-RAM pool across consumers. The banner at startup logs the chosen split.

Override only the bucket that matters for your workload:

# Graph-heavy workload: give graph more RAM
RELATA_GRAPH_RAM_BUDGET_MB=16384   # 16 GB
 
# OLAP workload: grow the result cache
RELATA_RESULT_CACHE_MAX_BYTES=1073741824   # 1 GB

Per-workload tuning

OLTP (high QPS, small queries)

Goal: sub-5 ms p99 reads, sub-10 ms p99 writes.

# Tighten the query timeout (fail fast)
RELATA_QUERY_TIMEOUT_SECS=5
 
# Cache results for repeat queries
RELATA_RESULT_CACHE_ENABLED=true
RELATA_RESULT_CACHE_TTL_SECS=60      # short TTL for OLTP freshness
RELATA_RESULT_CACHE_MAX_BYTES=268435456   # 256 MB
 
# Rate-limit aware admission
RELATA_RATE_LIMIT_RPS=10000          # free; 100000 server

Key perf wins:

  1. Plan cache hit ratio > 90% — same SQL templates reuse the verdict.
  2. Result cache hit ratio > 50% for read-heavy patterns.
  3. PK lookups use the live-index — O(1) always.
  4. Avoid SELECT * — projection cost matters at high QPS.

OLAP (large analytical scans)

Goal: sub-second BI queries over 100 M+ rows.

# Big exec budget for sorts, joins, aggregates
RELATA_EXEC_RAM_BUDGET_MB=8192       # 8 GB
 
# Big result cache for dashboard workloads
RELATA_RESULT_CACHE_MAX_BYTES=2147483648   # 2 GB
RELATA_RESULT_CACHE_MAX_ROWS=100000

Key perf wins:

  1. Columnar aggregate path (COUNT/SUM/MIN/MAX over a column) is 5–10× the row path.
  2. Per-column bloom filters prune disk segments pre-decode.
  3. HLL + CMS sketches accelerate COUNT(DISTINCT) and frequency estimates.
  4. Result cache + WITH CACHE TTL 300 for dashboard workloads.

Time-series (high ingest)

Goal: 100 K–1 M rows/sec sustained.

# Batched fsync window (RELATA_WAL_SYNC=interval coalesces fsyncs ~every 10 ms)
RELATA_WAL_SYNC=interval
 
# Bigger segment flush threshold = fewer flushes
RELATA_FLUSH_SEGMENT_MAX_ROWS=500000   # default 250 000
 
# Async media ingest for embedding-sidecar workloads
RELATA_EMBED_BATCH_SIZE=64
RELATA_EMBED_CONCURRENCY=8

Key perf wins:

  1. Group commit — N concurrent writers coalesce into ≤1 fsync per ~10 ms window when RELATA_WAL_SYNC=interval (the default).
  2. Batch ingest via /ingest (not row-by-row INSERT) — 10× throughput.

Graph (multi-hop traversals)

Goal: <100 ms p99 for 3-hop traversal over 100 M edges.

# Give graph its own RAM budget (don't share with secondary indexes)
RELATA_GRAPH_RAM_BUDGET_MB=16384     # 16 GB

Key perf wins:

  1. CSR cache default-on — 5–50× on multi-op workflows.
  2. rayon parallelism — 8–32× on multi-core.
  3. PLL hub labeling — 100–1000× on distance queries.
  4. Bidirectional BFS — √2× typical on point-to-point.

Vector (ANN at scale)

Goal: <10 ms p99 over 1 M vectors at 99% recall.

# HNSW parameters (set at index creation, not at runtime)
# M=128, ef_construction=350, ef_search=k.max(10)   (defaults)
 
# Cold tier for >RAM-scale
RELATA_DISKANN_MAX_RESIDENT=1000000   # 1 M vectors RAM-resident
RELATA_VECTOR_COLD_RESIDENT_MAX=200000   # IVF staging cap
 
# Search preset
RELATA_SEARCH_PRESET=balanced   # strict | balanced | lenient

Key perf wins:

  1. int8 quantization (default) — 4× memory savings, ±0.4% recall.
  2. Pre-filter vs post-filter adaptive threshold at 25% selectivity.
  3. RELATA_SEARCH_PRESET=strict for precision queries; lenient for recall.
  4. HYBRID_SEARCH … WHERE pushdown — a selective equality/IN predicate is pushed into the ANN leg as a candidate allowlist instead of only filtering post-fusion, avoiding the filtered-ANN recall cliff. See Vector parameters for the predicate-shape table and adaptive over-fetch behavior.

True on-disk DiskANN beam search (opt-in, ADR-282)

For corpora that don't fit even the DiskANN warm-tier's resident HNSW graph, RELATA_DISKANN_DISK_RESIDENT=true additionally enables a sector-demand-paged beam search that reads a node's graph adjacency + PQ code as a single 4 KB object-store range read, bounding RAM independently of corpus size:

RELATA_DISKANN_DISK_RESIDENT=true
RELATA_DISKANN_SECTOR_CACHE_MB=256    # process-wide sector LRU cache
RELATA_DISKANN_IO_BUDGET=512          # max sector reads/query (fail-open)
RELATA_DISKANN_RERANK_FACTOR=6        # exact full-vector rerank of top rerank_factor·k
RELATA_DISKANN_IO_FAIL=open           # open (best-effort) | closed (empty on budget exhaustion)

Status: implemented and exercised at bench scale (cargo run -p relata-bench --release -- diskann-true-disk), but not yet wired into live query dispatch and not benchmarked at the 100 M×768-d / recall@10 ≥ 0.95 target the design (ADR-282) calls for — treat the numbers above as tuning knobs for when that lands, not as a validated capacity-planning baseline yet.

Query-level tuning

Use EXPLAIN

PURPOSE 'analytics' EXPLAIN SELECT * FROM Person WHERE name = 'Alice'

Output shows access path (Index vs Full), estimated rows, and selectivity. EXPLAIN ANALYZE shows per-operator actuals.

EXPLAIN HYBRID_SEARCH FROM <Type> QUERY '<text>' LIMIT <n> WHERE <pred> reports the WHERE-pushdown decision for the ANN leg (whether it was applied, the allowlist size, and whether the fetch adaptively over-fetched) — see Vector parameters. EXPLAIN ANALYZE is not yet supported for HYBRID_SEARCH.

Use indexes

Equality on indexed columns is O(log n). Range on indexed columns walks the BTreeMap. CIDR match uses the prefix index. FTS uses BM25 with WAND pruning.

-- Equality index used:
SELECT * FROM Person WHERE email = 'alice@example.com'
 
-- Range index used:
SELECT * FROM Event WHERE ts > '2024-01-01' AND ts < '2024-02-01'
 
-- BM25 index used:
SELECT * FROM Document WHERE MATCH(body, 'governance')
 
-- Vector ANN used:
SELECT * FROM Vec WHERE SIMILAR TO '[0.1,0.2,...]' FIELD=_emb_text LIMIT 10

Use LIMIT aggressively

-- Good: ordered LIMIT walks the index, returns early
SELECT * FROM Event ORDER BY ts DESC LIMIT 10

Use cursor pagination

-- Good: cursor pagination for infinite-scroll UIs
SELECT * FROM Event
  ORDER BY ts DESC
  LIMIT 100 AFTER '<cursor>'

Use AS OF for time travel

-- Good: AS OF uses the bi-temporal index, not a full scan
SELECT * FROM Person AS OF '2024-06-01T00:00:00Z'
 
-- Bad: filtering on system_from manually
SELECT * FROM Person WHERE system_from &lt;1717200000000000000

Cluster tuning

Scatter-gather

# Bound fan-out parallelism
RELATA_SCATTER_MAX_PARALLEL=64
 
# Per-peer timeout (milliseconds)
RELATA_SCATTER_PEER_TIMEOUT_MS=10000

Cache coherence

Gossip-based invalidations piggyback on heartbeats (10 s window). For read-your-writes across nodes, use session affinity (route the same principal to the same node).

Common anti-patterns

Anti-patternWhy it's slowFix
SELECT * on wide tablesMaterialises every columnProject only what you need
COUNT(*) on a type with no SummaryStoreFull scanUse the SummaryStore fast path (auto for un-filtered)
Recursive CTEs without LIMITUnbounded iterationAlways cap with MAX_RECURSIVE_ITERS
OFFSET > 1000Linear skipUse cursor pagination (AFTER)
Inserting row-by-row1 fsync per rowBatch via /ingest
Vector search without pre-filterANN over full indexUse search_filtered with an allowlist
Graph algorithm on a cold CSRFull rebuild per callEnable paged-graph cache

Benchmarking

# Quick gate (1 min)
RELATA_GLOBAL_SCAN_ALLOWED=true cargo run -p relata-bench --release -- full --scale 100k --gate
 
# Full bench suite
RELATA_GLOBAL_SCAN_ALLOWED=true cargo run -p relata-bench --release -- full
 
# 22-interface live-server bench
./scripts/bench.sh --full
 
# Comparative vs other engines (Docker)
./scripts/bench.sh --docker --rust

Always benchmark on the target hardware with the target workload.

See also