SurrealDb vs RocksDb

This commit is contained in:
Andrus Salumets 2025-05-01 16:03:37 +02:00
parent 670a7568a5
commit 535ae29d19
13 changed files with 518 additions and 1 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
Cargo.lock
target/
.vscode
.vscode
.idea

View File

@ -0,0 +1,42 @@
[package]
name = "surrealdb_bench"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.98"
clap = { version = "4.5.37", features = ["derive", "help", "color"] }
rocksdb = "0.23.0"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
surrealdb = { version = "2.2.2", features = ["kv-rocksdb", "kv-mem", "allocator"] }
tempfile = "3.19.1"
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread", "sync"] }
regex = "1.11.1"
[profile.release]
lto = true
strip = true
opt-level = 3
panic = 'abort'
codegen-units = 1
[[bin]]
name = "surrealdb_select_all"
path = "src/bin/surrealdb_select_all.rs"
[[bin]]
name = "surrealdb_parent_to_child_graph"
path = "src/bin/surrealdb_parent_to_child_graph.rs"
[[bin]]
name = "surrealdb_parent_to_child_pointers"
path = "src/bin/surrealdb_parent_to_child_pointers.rs"
[[bin]]
name = "rocksdb_select_all"
path = "src/bin/rocksdb_select_all.rs"
[[bin]]
name = "rocksdb_parent_to_child"
path = "src/bin/rocksdb_parent_child.rs"

31
surrealdb_bench/README.md Normal file
View File

@ -0,0 +1,31 @@
# SurrealDB Benchmarks
This project contains a set of binaries designed to benchmark and compare the performance of SurrealDB and RocksDB.
## Binaries
### SurrealDB Binaries
1. **`surrealdb_select_all`**
- Selecting all records from a SurrealDB database. It uses SurrealDB SQL: "SELECT * FROM block"
2. **`surrealdb_parent_to_child_graph`**
- Measures the performance of querying child-to-parent relationships using SurrealDB graph model(child->parent edges)
3. **`surrealdb_parent_to_child_pointers`**
- Measures the performance of querying child-to-parent relationships by querying parents by their key
### RocksDB Binaries
1. **`rocksdb_select_all`**
- Benchmarks the performance of selecting all records from a RocksDB database.
2. **`rocksdb_parent_to_child`**
- Measures the performance of querying child-to-parent relationships by querying parents by their key
## Usage
To run the benchmarks, you can use the following commands:
```bash
./run_benchmarks.sh
```

View File

@ -0,0 +1,35 @@
#!/bin/bash
BUILD_MODE="release"
BIN_DIR="target/$BUILD_MODE"
BENCHES=(
"surrealdb_select_all"
"surrealdb_parent_to_child_graph"
"surrealdb_parent_to_child_pointers"
"rocksdb_select_all"
"rocksdb_parent_to_child"
)
# Read blocks_count from the command-line argument or default to 1000000
BLOCKS_COUNT=${1:-1000000}
echo "Building all binaries..."
cargo build --$BUILD_MODE
if [ $? -ne 0 ]; then
echo "Error: Build failed."
exit 1
fi
echo "Running benchmarks with blocks_count=$BLOCKS_COUNT"
for BIN in "${BENCHES[@]}"; do
"$BIN_DIR/$BIN" --blocks-count "$BLOCKS_COUNT"
if [ $? -ne 0 ]; then
echo "Error: $BIN failed to execute."
exit 1
fi
done
echo "All benchmarks executed successfully."

View File

@ -0,0 +1,66 @@
use anyhow::Error;
use clap::Parser;
use rocksdb::DB;
use serde_json::{self, from_slice};
use std::time::Instant;
use surrealdb_bench::common::{Args, Block};
use surrealdb_bench::rocksdb::{setup_blocks, setup_db};
use tempfile::TempDir;
fn main() -> Result<(), Error> {
println!("----------------------------------------");
println!("[BENCHMARK START] ROCKSDB PARENT-TO-CHILD");
let args = Args::parse();
let temp_dir = TempDir::new()?;
let db = setup_db(&temp_dir)?;
let start_setup = Instant::now();
setup_blocks(&db, args.blocks_count)?;
let duration_setup = start_setup.elapsed();
println!(
"[BENCHMARK SETUP] {} BLOCKS TOOK {:?}",
args.blocks_count, duration_setup
);
let start = Instant::now();
let target_hash = format!("block{}", args.blocks_count - 1);
let ancestors = get_ancestors(&db, &target_hash, args.blocks_count)?;
assert_eq!(ancestors.len(), args.blocks_count);
let duration = start.elapsed();
println!(
"[BENCHMARK RESULT] PARENT-TO-CHILD TRAVERSAL ({} BLOCKS) TOOK: {:?}",
args.blocks_count, duration
);
println!("----------------------------------------\n");
Ok(())
}
/// Fetch a block from the database by its hash.
fn get_block(db: &DB, hash: &str) -> Result<Option<Block>, Error> {
let key = hash.as_bytes();
Ok(match db.get(key)? {
Some(value) => Some(from_slice(&value)?),
None => None,
})
}
/// Retrieve all ancestors of a block by following the parent pointers.
fn get_ancestors(db: &DB, hash: &str, count: usize) -> Result<Vec<Block>, Error> {
let mut ancestors = Vec::with_capacity(count);
let mut current_hash = hash.to_string();
while !current_hash.is_empty() {
if let Some(block) = get_block(db, &current_hash)? {
current_hash = block.parent.clone();
ancestors.push(block);
} else {
break;
}
}
Ok(ancestors)
}

View File

@ -0,0 +1,52 @@
use anyhow::Error;
use clap::Parser;
use rocksdb::DB;
use serde_json::{self, from_slice};
use std::time::Instant;
use surrealdb_bench::common::{Args, Block};
use surrealdb_bench::rocksdb::{setup_blocks, setup_db};
use tempfile::TempDir;
#[tokio::main]
async fn main() -> Result<(), Error> {
println!("----------------------------------------");
println!("[BENCHMARK START] ROCKSDB SELECT ALL");
let args = Args::parse();
let temp_dir = TempDir::new()?;
let db = setup_db(&temp_dir)?;
let start_setup = Instant::now();
setup_blocks(&db, args.blocks_count)?;
let setup_duration = start_setup.elapsed();
println!(
"[BENCHMARK SETUP] {} BLOCKS TOOK {:?}",
args.blocks_count, setup_duration
);
let start_query = Instant::now();
let blocks = fetch_all_blocks(&db, args.blocks_count)?;
assert_eq!(blocks.len(), args.blocks_count);
let query_duration = start_query.elapsed();
println!(
"[BENCHMARK RESULT] SELECTING {} BLOCKS TOOK {:?}",
args.blocks_count, query_duration
);
println!("----------------------------------------\n");
Ok(())
}
fn fetch_all_blocks(db: &DB, count: usize) -> Result<Vec<Block>, Error> {
let mut blocks = Vec::with_capacity(count);
let iter = db.iterator(rocksdb::IteratorMode::Start);
for item in iter {
let (_, value) = item?;
let block: Block = from_slice(&value)?;
blocks.push(block);
}
Ok(blocks)
}

View File

@ -0,0 +1,58 @@
use anyhow::Error;
use clap::Parser;
use serde::Deserialize;
use std::time::Instant;
use surrealdb::Surreal;
use surrealdb::engine::local::Db;
use surrealdb_bench::common::{Args, Block};
use surrealdb_bench::surrealdb::{Relation, insert_blocks, insert_edges, setup_db};
use tempfile::TempDir;
#[derive(Debug, Deserialize)]
struct ParentBlock {
#[serde(default)]
parent: Vec<Block>,
}
#[tokio::main]
async fn main() -> Result<(), Error> {
println!("----------------------------------------");
println!("[BENCHMARK START] SURREALDB PARENT-TO-CHILD GRAPH");
let args = Args::parse();
let temp_dir = TempDir::new()?;
let db = setup_db(&temp_dir).await?;
let setup_duration = Instant::now();
let blocks = insert_blocks(&db, args.blocks_count).await?;
insert_edges(&db, blocks, Relation::Parent).await?;
let setup_duration = setup_duration.elapsed();
println!(
"[BENCHMARK SETUP] {} BLOCKS (INCLUDING EDGES) TOOK {:?}",
args.blocks_count, setup_duration
);
let start_query = Instant::now();
let parent_blocks = fetch_parent_blocks(&db).await?;
let parent_blocks: Vec<Block> = parent_blocks.into_iter().flat_map(|pb| pb.parent).collect();
assert_eq!(parent_blocks.len(), args.blocks_count - 1);
let query_duration = start_query.elapsed();
println!(
"[BENCHMARK RESULT] PARENT-TO-CHILD TRAVERSAL ({} BLOCKS) TOOK {:?}",
args.blocks_count, query_duration
);
println!("----------------------------------------\n");
Ok(())
}
/// Fetches parent blocks from the database by following the parent relation.
async fn fetch_parent_blocks(db: &Surreal<Db>) -> Result<Vec<ParentBlock>, Error> {
let query = "SELECT ->parent->block.* AS parent FROM block";
let mut response = db.query(query).await?.check()?;
let blocks: Vec<ParentBlock> = response.take(0)?;
Ok(blocks)
}

View File

@ -0,0 +1,64 @@
use anyhow::Error;
use clap::Parser;
use std::time::Instant;
use surrealdb::Surreal;
use surrealdb::engine::local::Db;
use surrealdb_bench::common::{Args, Block};
use surrealdb_bench::surrealdb::{insert_blocks, setup_db};
use tempfile::TempDir;
#[tokio::main]
async fn main() -> Result<(), Error> {
println!("----------------------------------------");
println!("[BENCHMARK START] SURREALDB PARENT-TO-CHILD POINTER");
let args = Args::parse();
let temp_dir = TempDir::new()?;
let db = setup_db(&temp_dir).await?;
let start_setup = Instant::now();
insert_blocks(&db, args.blocks_count).await?;
let setup_duration = start_setup.elapsed();
println!(
"[BENCHMARK SETUP] {} BLOCKS (INCLUDING EDGES) TOOK {:?}",
args.blocks_count, setup_duration
);
let start_query = Instant::now();
let target_hash = format!("block{}", args.blocks_count - 1);
let ancestors = get_ancestors(&db, &target_hash, args.blocks_count).await?;
assert_eq!(ancestors.len(), args.blocks_count);
let query_duration = start_query.elapsed();
println!(
"[BENCHMARK RESULT] PARENT-TO-CHILD TRAVERSAL ({} BLOCKS) TOOK {:?}",
args.blocks_count, query_duration
);
println!("----------------------------------------\n");
Ok(())
}
/// Select a specific block by its hash from the database.
async fn get_block(db: &Surreal<Db>, hash: &str) -> Result<Option<Block>, Error> {
db.select(("block", hash)).await.map_err(Into::into)
}
/// Retrieve all blocks by following the parent pointers from a given block hash.
async fn get_ancestors(db: &Surreal<Db>, hash: &str, count: usize) -> Result<Vec<Block>, Error> {
let mut ancestors = Vec::with_capacity(count);
let mut current_hash = hash.to_string();
while !current_hash.is_empty() {
if let Some(block) = get_block(db, &current_hash).await? {
ancestors.push(block.clone());
current_hash = block.parent;
} else {
break;
}
}
Ok(ancestors)
}

View File

@ -0,0 +1,49 @@
use anyhow::Error;
use clap::Parser;
use std::time::Instant;
use surrealdb::Surreal;
use surrealdb::engine::local::Db;
use surrealdb_bench::common::{Args, Block};
use surrealdb_bench::surrealdb::{insert_blocks, setup_db};
use tempfile::TempDir;
#[tokio::main]
async fn main() -> Result<(), Error> {
println!("----------------------------------------");
println!("[BENCHMARK START] SURREALDB SELECT ALL");
let args = Args::parse();
let temp_dir = TempDir::new()?;
let db = setup_db(&temp_dir).await?;
let start_setup = Instant::now();
insert_blocks(&db, args.blocks_count).await?;
let start_setup = start_setup.elapsed();
println!(
"[BENCHMARK SETUP] {} BLOCKS TOOK {:?}",
args.blocks_count, start_setup
);
let start_query = Instant::now();
let blocks = fetch_all_blocks(&db).await?;
assert_eq!(blocks.len(), args.blocks_count,);
let query_duration = start_query.elapsed();
println!(
"[BENCHMARK RESULT] SELECTING {} BLOCKS TOOK {:?}",
args.blocks_count, query_duration
);
println!("----------------------------------------\n");
Ok(())
}
/// Fetches all blocks from the database.
async fn fetch_all_blocks(db: &Surreal<Db>) -> Result<Vec<Block>, Error> {
let query = "SELECT * FROM block";
let mut response = db.query(query).await?;
let blocks: Vec<Block> = response.take(0)?;
Ok(blocks)
}

View File

@ -0,0 +1,15 @@
use clap::Parser;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Block {
pub hash: String,
pub parent: String,
pub height: u64,
}
#[derive(Parser)]
pub struct Args {
#[arg(long, default_value_t = 1_000_000)]
pub blocks_count: usize,
}

View File

@ -0,0 +1,3 @@
pub mod common;
pub mod rocksdb;
pub mod surrealdb;

View File

@ -0,0 +1,21 @@
use crate::surrealdb::create_n_blocks;
use anyhow::Error;
use rocksdb::DB;
use serde_json::to_vec;
use tempfile::TempDir;
pub fn setup_db(temp_dir: &TempDir) -> Result<DB, Error> {
Ok(DB::open_default(temp_dir.path())?)
}
pub fn setup_blocks(db: &DB, count: usize) -> Result<(), Error> {
let blocks = create_n_blocks(count);
let mut batch = rocksdb::WriteBatch::default();
for block in &blocks {
let key = block.hash.as_bytes();
let value = to_vec(block)?;
batch.put(key, value);
}
db.write(batch)?;
Ok(())
}

View File

@ -0,0 +1,80 @@
use crate::common::Block;
use anyhow::Error;
use serde::Deserialize;
use surrealdb::Surreal;
use surrealdb::engine::local::{Db, RocksDb};
use tempfile::TempDir;
#[derive(Debug, Deserialize)]
pub enum Relation {
Parent,
Child,
}
pub async fn setup_db(temp_dir: &TempDir) -> Result<Surreal<Db>, Error> {
let db_path = temp_dir.path().join("surreal.db");
let db = Surreal::new::<RocksDb>(db_path).await?;
db.use_ns("nomos").use_db("nms").await?;
Ok(db)
}
pub async fn insert_blocks(db: &Surreal<Db>, count: usize) -> Result<Vec<Block>, Error> {
let blocks = create_n_blocks(count);
let queries: Vec<String> = blocks
.iter()
.map(|block| {
Ok(format!(
"CREATE block:{} CONTENT {};",
block.hash,
serde_json::to_string(block)?
))
})
.collect::<Result<Vec<_>, Error>>()?;
db.query(queries.join(" ")).await?.check()?;
Ok(blocks)
}
pub async fn insert_edges(
db: &Surreal<Db>,
blocks: Vec<Block>,
relation: Relation,
) -> Result<(), Error> {
let blocks = match relation {
Relation::Parent => blocks.into_iter().rev().collect::<Vec<_>>(),
Relation::Child => blocks,
};
let queries: Vec<String> = blocks
.iter()
.filter(|block| !block.parent.is_empty())
.map(|block| match relation {
Relation::Parent => format!(
"RELATE block:{}->parent->block:{}",
block.hash, block.parent
),
Relation::Child => {
format!("RELATE block:{}->child->block:{}", block.parent, block.hash)
}
})
.collect();
if !queries.is_empty() {
db.query(queries.join("; ")).await?.check()?;
}
Ok(())
}
pub fn create_n_blocks(n: usize) -> Vec<Block> {
(0..n)
.map(|i| Block {
hash: format!("block{}", i),
parent: if i == 0 {
String::new()
} else {
format!("block{}", i - 1)
},
height: i as u64 + 1,
})
.collect()
}