Performance

This document covers GraphQLite's performance characteristics and optimization strategies.

Benchmarks

Benchmarks on Apple M1 Max (10 cores, 64GB RAM).

Insertion Performance

NodesEdgesTimeRate
100K500K445ms1.3M/s
500K2.5M2.30s1.3M/s
1M5.0M5.16s1.1M/s

Traversal by Topology

TopologyNodesEdges1-hop2-hop
Chain100K99K<1ms<1ms
Sparse100K500K<1ms<1ms
Moderate100K2.0M<1ms2ms
Dense100K5.0M<1ms9ms
Normal dist.100K957K<1ms1ms
Power-law100K242K<1ms<1ms
Moderate500K10.0M1ms2ms
Moderate1M20.0M<1ms2ms

Deep Hop Traversal

Traversal time is independent of graph size - it scales only with the number of paths found.

HopsPaths FoundTime
1-35-125<1ms
46252ms
53,12512ms
615,62558ms

Path count grows as degree^hops. With average degree 5, expect 5^n paths at n hops.

Graph Algorithms

AlgorithmNodesEdgesTime
PageRank100K500K148ms
Label Propagation100K500K154ms
PageRank500K2.5M953ms
Label Propagation500K2.5M811ms
PageRank1M5.0M37.81s
Label Propagation1M5.0M40.21s

Cypher Query Performance

Query TypeG(100K, 500K)G(500K, 2.5M)G(1M, 5M)
Node lookup<1ms1ms<1ms
1-hop<1ms<1ms<1ms
2-hop<1ms<1ms<1ms
3-hop1ms1ms1ms
Filter scan341ms1.98s3.79s
MATCH all360ms2.05s3.98s

Optimization Strategies

Use Indexes Effectively

GraphQLite creates indexes on:

  • nodes(user_id) - Fast node lookup by ID
  • nodes(label) - Fast filtering by label
  • edges(source_id), edges(target_id) - Fast traversal
  • Property tables on (node_id, key) - Fast property access

Queries that leverage these indexes are fast.

Limit Variable-Length Paths

Variable-length paths can be expensive:

-- Expensive: unlimited depth
MATCH (a)-[*]->(b) RETURN b

-- Better: limit depth
MATCH (a)-[*1..3]->(b) RETURN b

Use Specific Labels

Labels help filter early:

-- Slower: scan all nodes
MATCH (n) WHERE n.type = 'Person' RETURN n

-- Faster: use label
MATCH (n:Person) RETURN n

Batch Operations

For bulk inserts, use batch methods:

# Slow: individual inserts
for person in people:
    g.upsert_node(person["id"], person, label="Person")

# Fast: batch insert
nodes = [(p["id"], p, "Person") for p in people]
g.upsert_nodes_batch(nodes)

Graph Caching

GraphQLite can cache the graph structure in memory using a Compressed Sparse Row (CSR) format, providing 1.5-2x speedup for graph algorithms by eliminating repeated SQLite I/O.

SQL Interface

-- Load graph into memory cache
SELECT gql_load_graph();
-- Returns: {"status":"loaded","nodes":1000,"edges":5000}

-- Check if cache is loaded
SELECT gql_graph_loaded();
-- Returns: {"loaded":true,"nodes":1000,"edges":5000}

-- Reload cache after graph modifications
SELECT gql_reload_graph();

-- Free cache memory
SELECT gql_unload_graph();

Python Interface

from graphqlite import graph

g = graph(":memory:")
# ... build graph ...

# Load cache for fast algorithm execution
g.load_graph()  # {"status": "loaded", "nodes": 1000, "edges": 5000}

# Run algorithms (all use cached graph)
g.pagerank()
g.community_detection()
g.degree_centrality()

# After modifying graph, reload cache
g.upsert_node("new_node", {}, "Person")
g.reload_graph()

# Free memory when done
g.unload_graph()

Rust Interface

#![allow(unused)]
fn main() {
use graphqlite::Graph;

let g = Graph::open_in_memory()?;
// ... build graph ...

// Load cache
let status = g.load_graph()?;
println!("Loaded {} nodes", status.nodes.unwrap_or(0));

// Run algorithms with cache
// ... algorithms use cache automatically ...

// Check status
if g.graph_loaded()? {
    g.unload_graph()?;
}
}

Cache Performance

Benchmarks on Apple M1 Max with graph caching enabled:

NodesEdgesAlgorithmUncachedCachedSpeedup
10K50KPageRank13ms7ms1.8x
10K50KLabel Prop13ms7ms1.8x
100K500KPageRank151ms91ms1.6x
100K500KLabel Prop151ms87ms1.7x
500K2.5MPageRank858ms420ms2.0x
500K2.5MLabel Prop863ms412ms2.0x

When to use caching:

  • Running multiple algorithms on the same graph
  • Repeated analysis workflows
  • Interactive exploration where graph doesn't change

When NOT to use caching:

  • Single algorithm call (cache load overhead may exceed benefit)
  • Frequently modified graphs (requires reload after each change)
  • Memory-constrained environments

Result Caching

For application-level caching of algorithm results:

import functools

@functools.lru_cache(maxsize=1)
def get_pagerank():
    return g.pagerank()

Memory Usage

GraphQLite uses SQLite's memory management. Key factors:

  • Page cache: SQLite caches database pages in memory
  • Algorithm scratch space: Algorithms allocate temporary structures
  • Result buffers: Query results are buffered before returning

For large graphs, consider:

# Increase SQLite page cache (default: 2MB)
conn.execute("PRAGMA cache_size = -64000")  # 64MB

Running Benchmarks

Run benchmarks on your hardware:

# Full performance suite
./tests/performance/run_all_perf.sh full

# Cache comparison benchmark
./tests/performance/perf_cache_comparison.sh full

# Quick cache test
sqlite3 :memory: < tests/performance/perf_cache.sql

Available benchmark modes:

  • quick - Fast smoke test (~30s)
  • standard - Default benchmarks (~3min)
  • full - Comprehensive benchmarks (~10min)

Benchmarks cover:

  • Insertion performance
  • Traversal across topologies (chain, tree, sparse, dense, power-law)
  • Algorithm performance (PageRank, Label Propagation, etc.)
  • Query performance (lookup, hop traversals, filters)
  • Cache performance (uncached vs cached algorithms)