use nedb_engine::sqljoin::{JoinExec, Strategy};
use nedb_engine::sqlplan::Plan;
use nedb_engine::sqlselect::{execute_explain, parse};
use serde_json::{json, Value};
use std::time::{Duration, Instant};
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0 >> 11
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
}
fn build(n_orders: usize, n_customers: usize, keyspace: u64, seed: u64) -> (Vec<Value>, Vec<Value>) {
let mut rng = Lcg(seed);
let orders = (0..n_orders)
.map(|i| {
let cid = rng.below(keyspace);
let null_key = rng.below(100) < 5;
json!({
"id": i,
"customer_id": if null_key { Value::Null } else { json!(cid) },
"amount": rng.below(1000),
"region": match rng.below(4) { 0 => "us", 1 => "eu", 2 => "apac", _ => "latam" },
})
})
.collect();
let customers = (0..n_customers)
.map(|i| {
json!({
"id": (i as u64) % keyspace,
"name": format!("cust-{i}"),
"tier": match rng.below(3) { 0 => "gold", 1 => "silver", _ => "bronze" },
})
})
.collect();
(orders, customers)
}
fn median(mut d: Vec<Duration>) -> Duration {
d.sort();
d[d.len() / 2]
}
struct Outcome {
elapsed: Duration,
rows: usize,
strategy: Option<Strategy>,
}
fn time_it(sql: &str, orders: &[Value], customers: &[Value], exec: JoinExec, reps: usize) -> Outcome {
let sel = parse(sql).unwrap_or_else(|e| panic!("{sql}: {e:#}"));
let resolve = |t: &str| -> anyhow::Result<Option<Box<dyn nedb_engine::sqlselect::Relation>>> {
Ok(match t {
"orders" => Some(nedb_engine::sqlselect::from_vec(orders.to_vec())),
"customers" => Some(nedb_engine::sqlselect::from_vec(customers.to_vec())),
_ => None,
})
};
let (_, warm, _): (_, _, Plan) = execute_explain(&sel, &resolve, exec).unwrap();
let mut samples = vec![];
let mut strategy = None;
for _ in 0..reps {
let t = Instant::now();
let (_, rows, choices) = execute_explain(&sel, &resolve, exec).unwrap();
samples.push(t.elapsed());
strategy = choices.join_strategy(0);
std::hint::black_box(rows);
}
Outcome { elapsed: median(samples), rows: warm.len(), strategy }
}
fn ms(d: Duration) -> String {
format!("{:>9.2}", d.as_secs_f64() * 1000.0)
}
fn main() {
if cfg!(debug_assertions) {
eprintln!(
"REFUSING to report numbers from a debug build — they are dominated by \
bounds checks and would not be comparable.\n\
Run: cargo run --release --example sqlbench"
);
std::process::exit(2);
}
let shapes: &[(usize, usize, u64, usize)] = &[
(1_000, 500, 250, 5),
(3_000, 1_000, 500, 3),
(8_000, 1_500, 1_500, 1),
];
let workloads: &[(&str, &str)] = &[
("scan", "SELECT o.id, o.amount FROM orders o"),
("filtered scan", "SELECT o.id FROM orders o WHERE o.amount > 900"),
("equality join", "SELECT o.id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id"),
("left join", "SELECT o.id, c.name FROM orders o LEFT JOIN customers c ON o.customer_id = c.id"),
("join + selective pred", "SELECT o.id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.amount > 990"),
("join + broad pred", "SELECT o.id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.amount > 100"),
("join + sort", "SELECT o.id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id ORDER BY c.name, o.id"),
("join + limit", "SELECT o.id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id LIMIT 20"),
("join + pred + limit", "SELECT o.id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.amount > 500 LIMIT 20"),
("non-equality join", "SELECT o.id FROM orders o JOIN customers c ON o.amount > 995"),
];
println!("nedb sqlselect baselines — engine {}", env!("CARGO_PKG_VERSION"));
println!("deterministic inputs, seed 0x51C0_FFEE_0000_0001, median of n reps\n");
for (no, nc, keyspace, reps) in shapes.iter().copied() {
let (orders, customers) = build(no, nc, keyspace, 0x51C0_FFEE_0000_0001);
println!(
"── orders={no} customers={nc} distinct keys={keyspace} reps={reps} \
────────────────"
);
println!(
"{:<24} {:>11} {:>11} {:>8} {:>7} {}",
"workload", "nested(ms)", "hash(ms)", "speedup", "rows", "note"
);
for (label, sql) in workloads {
let nl = time_it(sql, &orders, &customers, JoinExec::NestedLoop, reps);
let h = time_it(sql, &orders, &customers, JoinExec::Hash, reps);
let note = if nl.rows != h.rows {
format!("!! ROW COUNT DIFFERS {} vs {}", nl.rows, h.rows)
} else {
match (nl.strategy, h.strategy) {
(None, None) => "no join".to_string(),
(Some(Strategy::NestedLoop), Some(Strategy::Hash)) => String::new(),
(Some(a), Some(b)) if a == b => {
format!("both ran {a} — no hash path available")
}
(a, b) => format!("strategies {a:?} / {b:?}"),
}
};
let speedup = if h.elapsed.as_nanos() > 0 {
format!("{:>7.2}x", nl.elapsed.as_secs_f64() / h.elapsed.as_secs_f64())
} else {
" n/a".to_string()
};
println!(
"{:<24} {} {} {} {:>7} {}",
label,
ms(nl.elapsed),
ms(h.elapsed),
speedup,
nl.rows,
note
);
}
println!();
}
println!(
"Read the speedup column only on rows with an empty note. A row saying \
\"both ran Nested Loop\"\nis a case with no provable equality key, where \
the two columns time the SAME code twice and\nthe ratio is measurement \
noise — reporting it as a speedup would be a lie."
);
}