Skip to main content

linkrs_query/optimizer/cost/node_estimators/
scan.rs

1//! Scan Operation Estimator
2//!
3//! Provide a cost estimate for the scanning nodes:
4//! - ScanVertices
5//! - ScanEdges
6//! - IndexScan
7
8use super::NodeEstimator;
9use crate::optimizer::cost::estimate::NodeCostEstimate;
10use crate::optimizer::cost::CostCalculator;
11use crate::optimizer::error::CostError;
12use crate::optimizer::stats::StatsView;
13use crate::planning::plan::core::nodes::access::{IndexScanNode, ScanType};
14use crate::planning::plan::PlanNodeEnum;
15
16/// Scan Operation Estimator
17pub struct ScanEstimator<'a> {
18    cost_calculator: &'a CostCalculator,
19}
20
21impl<'a> ScanEstimator<'a> {
22    /// Create a new scanning estimator.
23    pub fn new(cost_calculator: &'a CostCalculator) -> Self {
24        Self { cost_calculator }
25    }
26
27    /// Estimating the selectivity of index scans
28    pub fn estimate_index_scan_selectivity(&self, node: &IndexScanNode) -> f64 {
29        if node.scan_limits().is_empty() {
30            return 0.1;
31        }
32
33        let mut total_selectivity: f64 = 1.0;
34        for limit in node.scan_limits() {
35            let sel = match limit.scan_type {
36                ScanType::Unique => 0.01,
37                ScanType::Prefix => 0.05,
38                ScanType::Range => 0.1,
39                ScanType::Full => 1.0,
40            };
41            total_selectivity *= sel;
42        }
43        total_selectivity.min(1.0)
44    }
45
46    /// Obtain the tag name from the IndexScan node.
47    fn get_tag_name_from_index_scan(&self, space: &str, node: &IndexScanNode) -> String {
48        // Resolve the tag name in the query space; ids are space-scoped.
49        if let Some(tag_name) = self
50            .cost_calculator
51            .statistics_manager()
52            .get_tag_name_by_id(space, node.tag_id())
53        {
54            return tag_name;
55        }
56
57        // Rollback: Attempt to infer the tag names from the column names in scan_limits.
58        if let Some(limit) = node.scan_limits().first() {
59            let column = &limit.column;
60            if let Some(dot_pos) = column.find('.') {
61                return column[..dot_pos].to_string();
62            }
63        }
64
65        "default".to_string()
66    }
67
68    /// Retrieve the attribute names from the IndexScan node.
69    fn get_property_name_from_index_scan(&self, node: &IndexScanNode) -> String {
70        if let Some(limit) = node.scan_limits().first() {
71            limit.column.clone()
72        } else {
73            "default".to_string()
74        }
75    }
76}
77
78impl<'a> NodeEstimator for ScanEstimator<'a> {
79    fn estimate(
80        &self,
81        stats: &StatsView,
82        node: &PlanNodeEnum,
83        _child_estimates: &[NodeCostEstimate],
84    ) -> Result<(f64, u64), CostError> {
85        let space = stats.space().unwrap_or("");
86        match node {
87            PlanNodeEnum::ScanVertices(n) => {
88                let tag_name = n.tag().map(|s| s.as_str()).unwrap_or("default");
89                let row_count = self
90                    .cost_calculator
91                    .statistics_manager()
92                    .get_vertex_count(space, tag_name);
93                let cost = self
94                    .cost_calculator
95                    .calculate_scan_vertices_cost(space, tag_name);
96                Ok((cost, row_count.max(1)))
97            }
98            PlanNodeEnum::ScanEdges(n) => {
99                let edge_type = n.edge_type().unwrap_or_else(|| "default".to_string());
100                let row_count = self
101                    .cost_calculator
102                    .statistics_manager()
103                    .get_edge_count(space, &edge_type);
104                let cost = self
105                    .cost_calculator
106                    .calculate_scan_edges_cost(space, &edge_type);
107                Ok((cost, row_count.max(1)))
108            }
109            PlanNodeEnum::IndexScan(n) => {
110                let selectivity = self.estimate_index_scan_selectivity(n);
111                let tag_name = self.get_tag_name_from_index_scan(space, n);
112                let property_name = self.get_property_name_from_index_scan(n);
113                let table_rows = self
114                    .cost_calculator
115                    .statistics_manager()
116                    .get_vertex_count(space, &tag_name);
117                let output_rows = (selectivity * table_rows as f64).max(1.0) as u64;
118                let cost = self.cost_calculator.calculate_index_scan_cost(
119                    space,
120                    &tag_name,
121                    &property_name,
122                    selectivity,
123                );
124                Ok((cost, output_rows))
125            }
126            _ => Err(CostError::UnsupportedNodeType(format!(
127                "Scan estimator does not support node type: {:?}",
128                std::mem::discriminant(node)
129            ))),
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::optimizer::cost::config::CostModelConfig;
138    use crate::optimizer::stats::{EdgeTypeStatistics, TagStatistics};
139    use crate::planning::plan::core::nodes::access::graph_scan_node::*;
140    use crate::planning::plan::core::nodes::access::{IndexLimit, ScanType};
141    use linkrs_core::Value;
142    use std::sync::Arc;
143
144    fn create_test_calculator() -> CostCalculator {
145        let stats_manager = Arc::new(crate::optimizer::stats::StatisticsManager::new());
146        let config = CostModelConfig::default();
147        CostCalculator::with_config(stats_manager, config)
148    }
149
150    fn create_test_calculator_with_stats() -> CostCalculator {
151        let stats_manager = Arc::new(crate::optimizer::stats::StatisticsManager::new());
152
153        let tag_stats = TagStatistics {
154            tag_name: "Person".to_string(),
155            vertex_count: 1000,
156            avg_out_degree: 5.0,
157            avg_in_degree: 5.0,
158            ..TagStatistics::default()
159        };
160        stats_manager.update_tag_stats("test", tag_stats);
161
162        let edge_stats = EdgeTypeStatistics {
163            edge_type: "friend".to_string(),
164            edge_count: 5000,
165            avg_out_degree: 3.0,
166            avg_in_degree: 2.0,
167            max_out_degree: 10,
168            max_in_degree: 8,
169            unique_src_vertices: 1000,
170            out_degree_std_dev: 2.0,
171            in_degree_std_dev: 1.5,
172            degree_gini_coefficient: 0.3,
173            hot_vertices: Vec::new(),
174            ..EdgeTypeStatistics::default()
175        };
176        stats_manager.update_edge_stats("test", edge_stats);
177
178        let config = CostModelConfig::default();
179        CostCalculator::with_config(stats_manager, config)
180    }
181
182    #[test]
183    fn test_scan_vertices_estimation() {
184        let calculator = create_test_calculator_with_stats();
185        let estimator = ScanEstimator::new(&calculator);
186
187        let mut node = ScanVerticesNode::new(1, "test_space");
188        node.set_tag("Person");
189        let plan_node = PlanNodeEnum::ScanVertices(node);
190
191        let child_estimates = vec![];
192        let result = estimator.estimate(
193            &calculator.stats_view(Some("test")),
194            &plan_node,
195            &child_estimates,
196        );
197
198        assert!(result.is_ok());
199        let (cost, output_rows) = result.expect("Estimates should be successful");
200        assert!(cost > 0.0);
201        assert_eq!(output_rows, 1000);
202    }
203
204    #[test]
205    fn test_scan_edges_estimation() {
206        let calculator = create_test_calculator_with_stats();
207        let estimator = ScanEstimator::new(&calculator);
208
209        let node = ScanEdgesNode::new(1, "friend");
210        let plan_node = PlanNodeEnum::ScanEdges(node);
211
212        let child_estimates = vec![];
213        let result = estimator.estimate(
214            &calculator.stats_view(Some("test")),
215            &plan_node,
216            &child_estimates,
217        );
218
219        assert!(result.is_ok());
220        let (cost, output_rows) = result.expect("Estimates should be successful");
221        assert!(cost > 0.0);
222        assert_eq!(output_rows, 5000);
223    }
224
225    #[test]
226    fn test_index_scan_estimation() {
227        let calculator = create_test_calculator_with_stats();
228        let estimator = ScanEstimator::new(&calculator);
229
230        let mut node = IndexScanNode::new(
231            1,
232            1,
233            1,
234            "test_index".to_string(),
235            "test_schema".to_string(),
236            ScanType::Unique,
237        );
238        node.set_scan_limits(vec![IndexLimit::equal(
239            "Person.name",
240            Value::string("Alice"),
241        )]);
242        let plan_node = PlanNodeEnum::IndexScan(node);
243
244        let child_estimates = vec![];
245        let result = estimator.estimate(
246            &calculator.stats_view(Some("test")),
247            &plan_node,
248            &child_estimates,
249        );
250
251        assert!(result.is_ok());
252        let (cost, output_rows) = result.expect("Estimates should be successful");
253        assert!(cost > 0.0);
254        assert!(output_rows >= 1);
255    }
256
257    #[test]
258    fn test_unsupported_node_type() {
259        let calculator = create_test_calculator();
260        let estimator = ScanEstimator::new(&calculator);
261
262        let node = PlanNodeEnum::Start(
263            crate::planning::plan::core::nodes::control_flow::start_node::StartNode::new(),
264        );
265        let child_estimates = vec![];
266        let result = estimator.estimate(
267            &calculator.stats_view(Some("test")),
268            &node,
269            &child_estimates,
270        );
271
272        assert!(result.is_err());
273    }
274
275    #[test]
276    fn test_estimate_index_scan_selectivity_empty() {
277        let calculator = create_test_calculator();
278        let estimator = ScanEstimator::new(&calculator);
279
280        let node = IndexScanNode::new(
281            1,
282            1,
283            1,
284            "test_index".to_string(),
285            "test_schema".to_string(),
286            ScanType::Unique,
287        );
288        let selectivity = estimator.estimate_index_scan_selectivity(&node);
289        assert_eq!(selectivity, 0.1);
290    }
291
292    #[test]
293    fn test_estimate_index_scan_selectivity_unique() {
294        let calculator = create_test_calculator();
295        let estimator = ScanEstimator::new(&calculator);
296
297        let mut node = IndexScanNode::new(
298            1,
299            1,
300            1,
301            "test_index".to_string(),
302            "test_schema".to_string(),
303            ScanType::Unique,
304        );
305        node.set_scan_limits(vec![IndexLimit::equal(
306            "Person.name",
307            Value::string("Alice"),
308        )]);
309        let selectivity = estimator.estimate_index_scan_selectivity(&node);
310        assert_eq!(selectivity, 0.01);
311    }
312
313    #[test]
314    fn test_estimate_index_scan_selectivity_prefix() {
315        let calculator = create_test_calculator();
316        let estimator = ScanEstimator::new(&calculator);
317
318        let mut node = IndexScanNode::new(
319            1,
320            1,
321            1,
322            "test_index".to_string(),
323            "test_schema".to_string(),
324            ScanType::Prefix,
325        );
326        node.set_scan_limits(vec![IndexLimit::prefix("Person.name", Value::string("A"))]);
327        let selectivity = estimator.estimate_index_scan_selectivity(&node);
328        assert_eq!(selectivity, 0.05);
329    }
330
331    #[test]
332    fn test_estimate_index_scan_selectivity_range() {
333        let calculator = create_test_calculator();
334        let estimator = ScanEstimator::new(&calculator);
335
336        let mut node = IndexScanNode::new(
337            1,
338            1,
339            1,
340            "test_index".to_string(),
341            "test_schema".to_string(),
342            ScanType::Range,
343        );
344        node.set_scan_limits(vec![IndexLimit::range(
345            "Person.age",
346            Some(Value::string("20")),
347            Some(Value::string("30")),
348            true,
349            true,
350        )]);
351        let selectivity = estimator.estimate_index_scan_selectivity(&node);
352        assert_eq!(selectivity, 0.1);
353    }
354
355    #[test]
356    fn test_estimate_index_scan_selectivity_multiple() {
357        let calculator = create_test_calculator();
358        let estimator = ScanEstimator::new(&calculator);
359
360        let mut node = IndexScanNode::new(
361            1,
362            1,
363            1,
364            "test_index".to_string(),
365            "test_schema".to_string(),
366            ScanType::Unique,
367        );
368        node.set_scan_limits(vec![
369            IndexLimit::equal("Person.name", Value::string("Alice")),
370            IndexLimit::equal("Person.age", Value::string("25")),
371        ]);
372        let selectivity = estimator.estimate_index_scan_selectivity(&node);
373        assert_eq!(selectivity, 0.0001);
374    }
375
376    #[test]
377    fn test_scan_vertices_no_tag() {
378        let calculator = create_test_calculator();
379        let estimator = ScanEstimator::new(&calculator);
380
381        let node = ScanVerticesNode::new(1, "test_space");
382        let plan_node = PlanNodeEnum::ScanVertices(node);
383
384        let child_estimates = vec![];
385        let result = estimator.estimate(
386            &calculator.stats_view(Some("test")),
387            &plan_node,
388            &child_estimates,
389        );
390
391        assert!(result.is_ok());
392        let (cost, output_rows) = result.expect("Estimation should succeed");
393        assert_eq!(cost, 0.0);
394        assert_eq!(output_rows, 1);
395    }
396
397    #[test]
398    fn test_scan_edges_no_edge_type() {
399        let calculator = create_test_calculator();
400        let estimator = ScanEstimator::new(&calculator);
401
402        let node = ScanEdgesNode::new(1, "");
403        let plan_node = PlanNodeEnum::ScanEdges(node);
404
405        let child_estimates = vec![];
406        let result = estimator.estimate(
407            &calculator.stats_view(Some("test")),
408            &plan_node,
409            &child_estimates,
410        );
411
412        assert!(result.is_ok());
413        let (cost, output_rows) = result.expect("Estimation should succeed");
414        assert_eq!(cost, 0.0);
415        assert_eq!(output_rows, 1);
416    }
417
418    #[test]
419    fn test_get_tag_name_from_index_scan_with_id() {
420        let calculator = create_test_calculator_with_stats();
421        let estimator = ScanEstimator::new(&calculator);
422
423        let node = IndexScanNode::new(
424            1,
425            1,
426            1,
427            "test_index".to_string(),
428            "test_schema".to_string(),
429            ScanType::Unique,
430        );
431        let tag_name = estimator.get_tag_name_from_index_scan("test", &node);
432        assert_eq!(tag_name, "default");
433    }
434
435    #[test]
436    fn test_get_property_name_from_index_scan() {
437        let calculator = create_test_calculator();
438        let estimator = ScanEstimator::new(&calculator);
439
440        let mut node = IndexScanNode::new(
441            1,
442            1,
443            1,
444            "test_index".to_string(),
445            "test_schema".to_string(),
446            ScanType::Unique,
447        );
448        node.set_scan_limits(vec![IndexLimit::equal(
449            "Person.name",
450            Value::string("Alice"),
451        )]);
452        let property_name = estimator.get_property_name_from_index_scan(&node);
453        assert_eq!(property_name, "Person.name");
454    }
455
456    #[test]
457    fn test_get_property_name_from_index_scan_empty() {
458        let calculator = create_test_calculator();
459        let estimator = ScanEstimator::new(&calculator);
460
461        let node = IndexScanNode::new(
462            1,
463            1,
464            1,
465            "test_index".to_string(),
466            "test_schema".to_string(),
467            ScanType::Unique,
468        );
469        let property_name = estimator.get_property_name_from_index_scan(&node);
470        assert_eq!(property_name, "default");
471    }
472}