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
| Lever | Knob class | Goal |
|---|---|---|
| Memory | RELATA_*_RAM_MB, RELATA_*_MAX_BYTES | Hot data resident |
| Parallelism | rayon (graph), worker pools, scatter-gather | Use all cores |
| Disk I/O | compaction strategy, WAL group-commit, spill format | Reduce 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 GBPer-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 serverKey perf wins:
- Plan cache hit ratio > 90% — same SQL templates reuse the verdict.
- Result cache hit ratio > 50% for read-heavy patterns.
- PK lookups use the live-index — O(1) always.
- 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=100000Key perf wins:
- Columnar aggregate path (COUNT/SUM/MIN/MAX over a column) is 5–10× the row path.
- Per-column bloom filters prune disk segments pre-decode.
- HLL + CMS sketches accelerate
COUNT(DISTINCT)and frequency estimates. - Result cache +
WITH CACHE TTL 300for 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=8Key perf wins:
- Group commit — N concurrent writers coalesce into ≤1 fsync per ~10 ms window when
RELATA_WAL_SYNC=interval(the default). - 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 GBKey perf wins:
- CSR cache default-on — 5–50× on multi-op workflows.
- rayon parallelism — 8–32× on multi-core.
- PLL hub labeling — 100–1000× on distance queries.
- 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 | lenientKey perf wins:
- int8 quantization (default) — 4× memory savings, ±0.4% recall.
- Pre-filter vs post-filter adaptive threshold at 25% selectivity.
RELATA_SEARCH_PRESET=strictfor precision queries;lenientfor recall.HYBRID_SEARCH … WHEREpushdown — a selective equality/INpredicate 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 10Use LIMIT aggressively
-- Good: ordered LIMIT walks the index, returns early
SELECT * FROM Event ORDER BY ts DESC LIMIT 10Use 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 <1717200000000000000Cluster tuning
Scatter-gather
# Bound fan-out parallelism
RELATA_SCATTER_MAX_PARALLEL=64
# Per-peer timeout (milliseconds)
RELATA_SCATTER_PEER_TIMEOUT_MS=10000Cache 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-pattern | Why it's slow | Fix |
|---|---|---|
SELECT * on wide tables | Materialises every column | Project only what you need |
COUNT(*) on a type with no SummaryStore | Full scan | Use the SummaryStore fast path (auto for un-filtered) |
Recursive CTEs without LIMIT | Unbounded iteration | Always cap with MAX_RECURSIVE_ITERS |
OFFSET > 1000 | Linear skip | Use cursor pagination (AFTER) |
| Inserting row-by-row | 1 fsync per row | Batch via /ingest |
| Vector search without pre-filter | ANN over full index | Use search_filtered with an allowlist |
| Graph algorithm on a cold CSR | Full rebuild per call | Enable 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 --rustAlways benchmark on the target hardware with the target workload.