use spg_engine::Engine;
const MATCHES: i64 = 50;
fn build(rows: i64) -> Engine {
build_with(rows, MATCHES)
}
fn build_with(rows: i64, scheduled: i64) -> Engine {
let mut eng = Engine::new();
eng.execute(
"CREATE TABLE outbound_queue (
id BIGINT PRIMARY KEY,
scheduled_at BIGINT,
status TEXT NOT NULL,
payload TEXT NOT NULL
)",
)
.expect("create");
eng.execute("CREATE INDEX idx_outbound_scheduled_at ON outbound_queue (scheduled_at)")
.expect("index");
let mut sql = String::with_capacity(1 << 20);
let mut i = 1;
while i <= rows {
sql.clear();
sql.push_str("INSERT INTO outbound_queue VALUES ");
let end = (i + 4_999).min(rows);
for g in i..=end {
if g > i {
sql.push(',');
}
if g <= scheduled {
sql.push_str(&format!("({g},{},'pending','p{g}')", g * 1000));
} else {
sql.push_str(&format!("({g},NULL,'pending','p{g}')"));
}
}
eng.execute(&sql).expect("seed");
i = end + 1;
}
eng
}
fn explain(eng: &mut Engine, sql: &str) -> String {
let out = eng
.execute(&format!("EXPLAIN {sql}"))
.unwrap_or_else(|e| panic!("EXPLAIN failed: {e:?}"));
format!("{out:?}")
}
fn head(plan: &str) -> String {
plan.split("\\n")
.next()
.unwrap_or(plan)
.chars()
.take(120)
.collect()
}
fn time_ms(eng: &mut Engine, sql: &str, reps: u32) -> f64 {
let mut best = f64::MAX;
for _ in 0..reps {
let t0 = std::time::Instant::now();
let out = eng.execute(sql).expect("query");
best = best.min(t0.elapsed().as_secs_f64() * 1000.0);
core::hint::black_box(&out);
}
best
}
const EQ: &str = "SELECT id FROM outbound_queue WHERE scheduled_at = 1000";
const RANGE: &str = "SELECT id FROM outbound_queue \
WHERE scheduled_at IS NOT NULL AND scheduled_at <= 1755000000 \
ORDER BY scheduled_at LIMIT 100";
fn shape_sweep() {
let shapes: [(&str, &str); 8] = [
(
"equality",
"SELECT id FROM outbound_queue WHERE scheduled_at = 1000",
),
(
"range alone",
"SELECT id FROM outbound_queue WHERE scheduled_at <= 1755000000",
),
(
"range + IS NOT NULL",
"SELECT id FROM outbound_queue WHERE scheduled_at IS NOT NULL AND scheduled_at <= 1755000000",
),
(
"range + ORDER BY",
"SELECT id FROM outbound_queue WHERE scheduled_at <= 1755000000 ORDER BY scheduled_at",
),
(
"range + ORDER BY + LIMIT",
"SELECT id FROM outbound_queue WHERE scheduled_at <= 1755000000 ORDER BY scheduled_at LIMIT 100",
),
("the reported query", RANGE),
(
"wide range (all rows match)",
"SELECT id FROM outbound_queue WHERE id <= 999999999",
),
(
"wide range, other direction",
"SELECT id FROM outbound_queue WHERE id >= 0",
),
];
println!("\n== which shape loses the index?");
println!(
" {:<28} {:>9} {:>9} {:>8}",
"shape", "20k ms", "160k ms", "x"
);
for (label, sql) in shapes {
let mut small = build(20_000);
let mut big = build(160_000);
let a = time_ms(&mut small, sql, 5);
let b = time_ms(&mut big, sql, 5);
let verdict = if b / a > 4.0 { "SCAN" } else { "index" };
println!(" {label:<28} {a:>9.3} {b:>9.3} {:>7.2} {verdict}", b / a);
}
}
fn main() {
println!("== what the PLAN says (20,000 rows, 50 scheduled)");
let mut eng = build(20_000);
println!(" equality : {}", head(&explain(&mut eng, EQ)));
println!(" range : {}", head(&explain(&mut eng, RANGE)));
println!("\n== does the estimate move with selectivity? (20,000 rows in every case)");
for scheduled in [50i64, 5_000, 10_000] {
let mut e = build_with(20_000, scheduled);
let plan = explain(
&mut e,
"SELECT id FROM outbound_queue WHERE scheduled_at <= 1755000000",
);
let est = plan
.split("rows=")
.nth(1)
.map(|r| {
r.chars()
.take_while(char::is_ascii_digit)
.collect::<String>()
})
.unwrap_or_else(|| "?".into());
println!(" {scheduled:>6} of 20,000 match -> rows={est}");
}
println!("\n== what the EXECUTOR does");
println!(" matching rows are held at {MATCHES}; only the table grows.");
println!(" flat => an index walk. linear => a scan.");
println!(" {:<10} {:>10} {:>10}", "rows", "range ms", "equality ms");
let mut first: Option<f64> = None;
for rows in [20_000i64, 40_000, 80_000, 160_000] {
let mut e = build(rows);
let r = time_ms(&mut e, RANGE, 5);
let q = time_ms(&mut e, EQ, 5);
let ratio = first.map_or(1.0, |f: f64| r / f);
if first.is_none() {
first = Some(r);
}
println!(" {rows:<10} {r:>10.3} {q:>10.3} (range x{ratio:.2} of the first)");
}
shape_sweep();
}