use super::{default_factors, Cost, CostEstimator};
use crate::velesql::explain::{IndexLookupPlan, MatchTraversalPlan, PlanNode, VectorSearchPlan};
impl CostEstimator<'_> {
#[must_use]
pub fn estimate_plan_cost(&self, root: &PlanNode) -> Cost {
match root {
PlanNode::VectorSearch(vs) => self.estimate_vector_search_node_cost(vs),
PlanNode::Filter(f) => self.estimate_filter_cost_from_selectivity(f.selectivity),
PlanNode::TableScan(_) => self.estimate_table_scan_cost(),
PlanNode::IndexLookup(plan) => self.estimate_index_lookup_cost(plan),
PlanNode::MatchTraversal(mt) => self.estimate_match_traversal_cost(mt),
PlanNode::Sequence(nodes) => nodes.iter().fold(Cost::default(), |acc, n| {
let c = self.estimate_plan_cost(n);
Cost::new(acc.io_cost + c.io_cost, acc.cpu_cost + c.cpu_cost)
}),
PlanNode::Limit(_)
| PlanNode::Offset(_)
| PlanNode::Join(_)
| PlanNode::GroupBy(_)
| PlanNode::Aggregate(_)
| PlanNode::Sort(_) => self.estimate_limit_offset_cost(),
}
}
fn estimate_vector_search_node_cost(&self, vs: &VectorSearchPlan) -> Cost {
let total = self.stats.total_points.max(self.stats.row_count).max(1) as f64;
let ef = f64::from(vs.ef_search.max(1));
let k = f64::from(vs.candidates.max(1));
let probe = (ef + k) * total.log2().max(1.0);
self.hnsw_cost_from_probe(probe)
}
fn estimate_table_scan_cost(&self) -> Cost {
let total = self.stats.total_points.max(self.stats.row_count).max(1) as f64;
let f = self.factors();
let d = default_factors();
let io_ratio = f.seq_page_cost / d.seq_page_cost;
let cpu_ratio = f.cpu_tuple_cost / d.cpu_tuple_cost;
Cost::new(total * io_ratio, total * cpu_ratio)
}
fn estimate_index_lookup_cost(&self, plan: &IndexLookupPlan) -> Cost {
let total = self.stats.total_points.max(self.stats.row_count).max(1) as f64;
let probe_size = self
.stats
.column_stats
.get(&plan.property)
.map_or(total, |cs| {
let ndv = cs.distinct_values.max(cs.distinct_count);
if ndv > 0 {
(ndv as f64).max(1.0)
} else {
total
}
});
let log_probe = probe_size.log2().max(1.0);
let f = self.factors();
let d = default_factors();
let cpu_ratio = f.cpu_index_cost / d.cpu_index_cost;
Cost::new(0.0, log_probe * cpu_ratio * d.cpu_index_cost)
}
fn estimate_match_traversal_cost(&self, mt: &MatchTraversalPlan) -> Cost {
let avg_degree: f64 = 4.0;
let depth = f64::from(mt.max_depth.max(1));
let total = self.stats.total_points.max(self.stats.row_count).max(1) as f64;
let frontier = avg_degree.powf(depth).min(total);
let f = self.factors();
let d = default_factors();
let edge_ratio = f.cpu_edge_cost / d.cpu_edge_cost;
Cost::new(0.0, frontier * edge_ratio * d.cpu_edge_cost)
}
fn estimate_limit_offset_cost(&self) -> Cost {
let f = self.factors();
let d = default_factors();
let cpu_ratio = f.cpu_tuple_cost / d.cpu_tuple_cost;
Cost::new(0.0, d.cpu_tuple_cost * cpu_ratio)
}
}
#[cfg(test)]
#[path = "plan_cost_tests.rs"]
mod tests;