1use 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
16pub struct ScanEstimator<'a> {
18 cost_calculator: &'a CostCalculator,
19}
20
21impl<'a> ScanEstimator<'a> {
22 pub fn new(cost_calculator: &'a CostCalculator) -> Self {
24 Self { cost_calculator }
25 }
26
27 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 fn get_tag_name_from_index_scan(&self, space: &str, node: &IndexScanNode) -> String {
48 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 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 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}