Skip to content

Internals

CSR format

How edges are stored for fast traversal

KiteDB uses Compressed Sparse Row (CSR) format to store graph edges. CSR is a standard format for sparse matrices: each node's neighbors are stored next to each other, so traversal is fast and the memory overhead is small.

The problem with naive edge storage

Consider a graph with 100,000 nodes and 1 million edges.

Adjacency matrix

100K × 100K = 10 billion entries

Actual edges: 1M (0.01% used)

99.99% of the matrix is empty space

Linked adjacency lists
ABCnull
BDnull

Problem: pointer chasing. Each hop goes to a random memory location.

Cache miss ~100 ns

Cache hit ~1 ns

1,000 edges × 100 ns = 100 μs spent waiting on RAM

The CSR solution

CSR stores all edges in two flat arrays: offsets and destinations. Edges are found by array index rather than by following pointers, and the arrays only hold edges that exist.

CSR layout

Graph

A → B, CB → DC → AD → (none)
1

Concatenate every node's destinations:

destinations
BC
A
D
B
A
C
2

Record where each node's edges start:

offsets
0A
2B
3C
4D
4end

Highlighted: node A's edges, and the two offsets that bound them.

How traversal works

Finding a node's neighbors takes two array lookups:

Who does A connect to?

start = offsets[0] = 0

end = offsets[1] = 2

destinations[0:2] = [B, C]

Who does D connect to?

start = offsets[3] = 4

end = offsets[4] = 4

destinations[4:4] = [] (no edges)

Algorithm

start = offsets[node]

end = offsets[node + 1]

return destinations[start:end]

Why it's fast: memory layout

Linked listscattered

B0x1000
C0x5F00
D0x2A00

Random locations, so each hop can miss the cache.

CSRcontiguous

B0x1000
C+4
D+8
A+C

Sequential addresses, so the CPU prefetcher can load ahead.

After the first access, B, C, D, and A are already in the CPU cache.

Bidirectional edges

KiteDB stores edges in both directions, so traversal is fast either way:

Out-edgesA → B

out_offsets = [0, 2, 3, 4, 4]

out_dst = [B, C, D, A]

Who does A follow?

In-edgesA ← C

in_offsets = [0, 1, 2, 3, 4]

in_src = [C, A, A, B]

Who follows A?

Tradeoff: 2× edge storage, in exchange for O(1) traversal in both directions.

Edge types and sorting

Real graphs have different edge types (follows, likes, knows). KiteDB stores edge types in a parallel array, sorted within each node:

out_dst
BCDA
out_etype
0100

0 = KNOWS

1 = LIKES

Edges are sorted by (etype, dst) within each node, so:

  • Binary search finds a specific edge type
  • Scanning stops once it passes the wanted type
  • Getting A's KNOWS edges doesn't scan all of A's edges

Edge existence check

To check if edge A→B exists with type KNOWS:

TypeScript
function hasEdge(src: NodeID, etype: EdgeType, dst: NodeID): boolean {  const start = offsets[src];  const end = offsets[src + 1];  // Binary search for etype within [start, end)  const typeStart = binarySearchStart(etypes, start, end, etype);  const typeEnd = binarySearchEnd(etypes, start, end, etype);  // Binary search for dst within type range  return binarySearch(destinations, typeStart, typeEnd, dst);}// Complexity: O(log k) where k = number of edges from src

Performance comparison

OperationCSRLinked list
Start traversalO(1) – two array lookupsO(1) – follow pointer
Iterate k neighborsO(k) – sequential readO(k) – but cache misses
Edge existenceO(log k) – binary searchO(k) – linear scan
Cache behaviorExcellent – prefetcher worksPoor – random access

Next steps