1use crate::algebra::{Algebra, Expression, TriplePattern};
7use crate::statistics_collector::StatisticsCollector;
8use anyhow::Result;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
13pub struct CostModelConfig {
14 pub cpu_cost_per_op: f64,
16 pub io_cost_per_page: f64,
18 pub memory_cost_per_byte: f64,
20 pub network_cost_per_byte: f64,
22 pub page_size: usize,
24 pub available_memory: usize,
26 pub calibration: CostCalibration,
28}
29
30impl Default for CostModelConfig {
31 fn default() -> Self {
32 Self {
33 cpu_cost_per_op: 1.0,
34 io_cost_per_page: 10.0,
35 memory_cost_per_byte: 0.001,
36 network_cost_per_byte: 0.1,
37 page_size: 4096,
38 available_memory: 1024 * 1024 * 1024, calibration: CostCalibration::default(),
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct CostCalibration {
47 pub cpu_scale: f64,
49 pub io_scale: f64,
51 pub memory_scale: f64,
53 pub network_scale: f64,
55 pub join_factors: JoinCostFactors,
57}
58
59impl Default for CostCalibration {
60 fn default() -> Self {
61 Self {
62 cpu_scale: 1.0,
63 io_scale: 1.0,
64 memory_scale: 1.0,
65 network_scale: 1.0,
66 join_factors: JoinCostFactors::default(),
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct JoinCostFactors {
74 pub hash_join_factor: f64,
76 pub sort_merge_join_factor: f64,
78 pub nested_loop_join_factor: f64,
80 pub index_join_factor: f64,
82}
83
84impl Default for JoinCostFactors {
85 fn default() -> Self {
86 Self {
87 hash_join_factor: 1.0,
88 sort_merge_join_factor: 1.2,
89 nested_loop_join_factor: 2.0,
90 index_join_factor: 0.8,
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct CostEstimate {
98 pub cpu_cost: f64,
100 pub io_cost: f64,
102 pub memory_cost: f64,
104 pub network_cost: f64,
106 pub total_cost: f64,
108 pub cardinality: usize,
110 pub selectivity: f64,
112 pub operation_costs: HashMap<String, f64>,
114}
115
116impl CostEstimate {
117 pub fn new(cpu: f64, io: f64, memory: f64, network: f64, cardinality: usize) -> Self {
118 let total = cpu + io + memory + network;
119 Self {
120 cpu_cost: cpu,
121 io_cost: io,
122 memory_cost: memory,
123 network_cost: network,
124 total_cost: total,
125 cardinality,
126 selectivity: 1.0,
127 operation_costs: HashMap::new(),
128 }
129 }
130
131 pub fn with_selectivity(mut self, selectivity: f64) -> Self {
132 self.selectivity = selectivity;
133 self
134 }
135
136 pub fn add_operation_cost(&mut self, operation: &str, cost: f64) {
137 self.operation_costs.insert(operation.to_string(), cost);
138 self.total_cost += cost;
139 }
140
141 pub fn zero() -> Self {
142 Self::new(0.0, 0.0, 0.0, 0.0, 0)
143 }
144
145 pub fn infinite() -> Self {
146 Self::new(
147 f64::INFINITY,
148 f64::INFINITY,
149 f64::INFINITY,
150 f64::INFINITY,
151 usize::MAX,
152 )
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
158pub enum IOPattern {
159 Sequential,
161 Random,
163 IndexScan,
165 FullScan,
167}
168
169#[derive(Debug, Clone)]
171pub struct MemoryUsage {
172 pub peak_usage: usize,
174 pub average_usage: usize,
176 pub access_pattern: MemoryAccessPattern,
178 pub duration_estimate: f64,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum MemoryAccessPattern {
185 Sequential,
187 Random,
189 Locality,
191 CacheFriendly,
193}
194
195#[derive(Debug, Clone)]
197pub struct CostModel {
198 config: CostModelConfig,
199 #[allow(dead_code)]
200 statistics: Option<StatisticsCollector>,
201 cached_estimates: HashMap<String, CostEstimate>,
202}
203
204impl CostModel {
205 pub fn new(config: CostModelConfig) -> Self {
207 Self {
208 config,
209 statistics: None,
210 cached_estimates: HashMap::new(),
211 }
212 }
213
214 pub fn with_statistics(config: CostModelConfig, statistics: StatisticsCollector) -> Self {
216 Self {
217 config,
218 statistics: Some(statistics),
219 cached_estimates: HashMap::new(),
220 }
221 }
222
223 pub fn estimate_cost(&mut self, algebra: &Algebra) -> Result<CostEstimate> {
225 let algebra_key = self.algebra_to_key(algebra);
227 if let Some(cached) = self.cached_estimates.get(&algebra_key) {
228 return Ok(cached.clone());
229 }
230
231 let estimate = self.estimate_cost_recursive(algebra)?;
232
233 self.cached_estimates.insert(algebra_key, estimate.clone());
235
236 Ok(estimate)
237 }
238
239 fn estimate_cost_recursive(&self, algebra: &Algebra) -> Result<CostEstimate> {
240 match algebra {
241 Algebra::Bgp(patterns) if patterns.len() == 1 => {
242 self.estimate_triple_pattern_cost(&patterns[0])
243 }
244 Algebra::Bgp(patterns) => self.estimate_bgp_cost(patterns),
245 Algebra::Join { left, right } => self.estimate_join_cost(left, right),
246 Algebra::LeftJoin { left, right, .. } => self.estimate_left_join_cost(left, right),
247 Algebra::Union { left, right } => self.estimate_union_cost(left, right),
248 Algebra::Filter { condition, pattern } => self.estimate_filter_cost(condition, pattern),
249 Algebra::Project { pattern, .. } => self.estimate_project_cost(pattern),
250 Algebra::Extend { expr, pattern, .. } => self.estimate_extend_cost(expr, pattern),
251 Algebra::Distinct { pattern } => self.estimate_distinct_cost(pattern),
252 Algebra::Reduced { pattern } => self.estimate_reduced_cost(pattern),
253 Algebra::OrderBy { pattern, .. } => self.estimate_order_by_cost(pattern),
254 Algebra::Slice {
255 pattern,
256 offset,
257 limit,
258 } => self.estimate_slice_cost(pattern, *offset, *limit),
259 Algebra::Group { pattern, .. } => self.estimate_group_cost(pattern),
260 Algebra::PropertyPath { .. } => {
261 Ok(CostEstimate::new(5000.0, 1000.0, 500.0, 0.0, 2000))
263 }
264 Algebra::Minus { left, right } => {
265 let left_cost = self.estimate_cost_recursive(left)?;
266 let right_cost = self.estimate_cost_recursive(right)?;
267 Ok(CostEstimate::new(
268 left_cost.total_cost + right_cost.total_cost * 2.0,
269 0.0,
270 0.0,
271 0.0,
272 left_cost.cardinality,
273 ))
274 }
275 Algebra::Service { pattern, .. } => {
276 let pattern_cost = self.estimate_cost_recursive(pattern)?;
278 Ok(CostEstimate::new(
279 pattern_cost.total_cost,
280 0.0,
281 0.0,
282 pattern_cost.total_cost * 10.0 + 1000.0, pattern_cost.cardinality,
284 ))
285 }
286 Algebra::Graph { pattern, .. } => {
287 self.estimate_cost_recursive(pattern)
289 }
290 Algebra::Having { pattern, condition } => {
291 let pattern_cost = self.estimate_cost_recursive(pattern)?;
292 let filter_selectivity = self.estimate_expression_selectivity(condition);
293 Ok(CostEstimate::new(
294 pattern_cost.total_cost * 1.1, 0.0,
296 0.0,
297 0.0,
298 (pattern_cost.cardinality as f64 * filter_selectivity) as usize,
299 ))
300 }
301 Algebra::Values { bindings, .. } => {
302 Ok(CostEstimate::new(
304 bindings.len() as f64 * 0.1,
305 0.0,
306 0.0,
307 0.0,
308 bindings.len(),
309 ))
310 }
311 Algebra::Table => {
312 Ok(CostEstimate::new(0.1, 0.0, 0.0, 0.0, 1))
314 }
315 Algebra::Zero => {
316 Ok(CostEstimate::new(0.1, 0.0, 0.0, 0.0, 0))
318 }
319 Algebra::Empty => {
320 Ok(CostEstimate::new(0.1, 0.0, 0.0, 0.0, 0))
322 }
323 }
324 }
325
326 fn estimate_triple_pattern_cost(&self, pattern: &TriplePattern) -> Result<CostEstimate> {
328 let selectivity = self.estimate_pattern_selectivity(pattern);
330
331 let base_cardinality = 100000; let cardinality = (base_cardinality as f64 * selectivity) as usize;
334
335 let io_pattern = self.determine_io_pattern(pattern);
337 let pages_accessed = self.estimate_pages_accessed(cardinality, io_pattern);
338 let io_cost =
339 pages_accessed as f64 * self.config.io_cost_per_page * self.config.calibration.io_scale;
340
341 let cpu_cost =
343 cardinality as f64 * self.config.cpu_cost_per_op * self.config.calibration.cpu_scale;
344
345 let memory_usage = cardinality * 100; let memory_cost = memory_usage as f64
348 * self.config.memory_cost_per_byte
349 * self.config.calibration.memory_scale;
350
351 let mut estimate = CostEstimate::new(cpu_cost, io_cost, memory_cost, 0.0, cardinality)
352 .with_selectivity(selectivity);
353
354 estimate.add_operation_cost("pattern_scan", cpu_cost + io_cost);
355
356 Ok(estimate)
357 }
358
359 fn estimate_bgp_cost(&self, patterns: &[TriplePattern]) -> Result<CostEstimate> {
361 if patterns.is_empty() {
362 return Ok(CostEstimate::zero());
363 }
364
365 if patterns.len() == 1 {
366 return self.estimate_triple_pattern_cost(&patterns[0]);
367 }
368
369 let mut total_cost = CostEstimate::zero();
371 let mut current_cardinality = 1;
372
373 for pattern in patterns {
374 let pattern_cost = self.estimate_triple_pattern_cost(pattern)?;
375
376 let join_cost = self.estimate_join_cost_detailed(
378 current_cardinality,
379 pattern_cost.cardinality,
380 0.1, JoinAlgorithm::HashJoin,
382 );
383
384 total_cost.cpu_cost += pattern_cost.cpu_cost + join_cost.cpu_cost;
385 total_cost.io_cost += pattern_cost.io_cost + join_cost.io_cost;
386 total_cost.memory_cost += pattern_cost.memory_cost + join_cost.memory_cost;
387
388 current_cardinality = join_cost.cardinality;
389 }
390
391 total_cost.cardinality = current_cardinality;
392 total_cost.total_cost = total_cost.cpu_cost + total_cost.io_cost + total_cost.memory_cost;
393
394 Ok(total_cost)
395 }
396
397 fn estimate_join_cost(&self, left: &Algebra, right: &Algebra) -> Result<CostEstimate> {
399 let left_cost = self.estimate_cost_recursive(left)?;
400 let right_cost = self.estimate_cost_recursive(right)?;
401
402 let algorithm = self.choose_join_algorithm(left_cost.cardinality, right_cost.cardinality);
404
405 let join_selectivity = 0.1; let join_cost = self.estimate_join_cost_detailed(
409 left_cost.cardinality,
410 right_cost.cardinality,
411 join_selectivity,
412 algorithm,
413 );
414
415 let total_cpu = left_cost.cpu_cost + right_cost.cpu_cost + join_cost.cpu_cost;
416 let total_io = left_cost.io_cost + right_cost.io_cost + join_cost.io_cost;
417 let total_memory =
418 left_cost.memory_cost.max(right_cost.memory_cost) + join_cost.memory_cost;
419
420 let mut estimate = CostEstimate::new(
421 total_cpu,
422 total_io,
423 total_memory,
424 0.0,
425 join_cost.cardinality,
426 );
427 estimate.add_operation_cost("join", join_cost.total_cost);
428
429 Ok(estimate)
430 }
431
432 fn estimate_left_join_cost(&self, left: &Algebra, right: &Algebra) -> Result<CostEstimate> {
434 let left_cost = self.estimate_cost_recursive(left)?;
435 let right_cost = self.estimate_cost_recursive(right)?;
436
437 let result_cardinality =
439 left_cost.cardinality + (right_cost.cardinality as f64 * 0.5) as usize;
440
441 let base_join_cost = self.estimate_join_cost_detailed(
443 left_cost.cardinality,
444 right_cost.cardinality,
445 0.5, self.choose_join_algorithm(left_cost.cardinality, right_cost.cardinality),
447 );
448
449 let total_cpu = left_cost.cpu_cost + right_cost.cpu_cost + base_join_cost.cpu_cost * 1.2;
450 let total_io = left_cost.io_cost + right_cost.io_cost + base_join_cost.io_cost;
451 let total_memory =
452 left_cost.memory_cost.max(right_cost.memory_cost) + base_join_cost.memory_cost;
453
454 Ok(CostEstimate::new(
455 total_cpu,
456 total_io,
457 total_memory,
458 0.0,
459 result_cardinality,
460 ))
461 }
462
463 fn estimate_union_cost(&self, left: &Algebra, right: &Algebra) -> Result<CostEstimate> {
465 let left_cost = self.estimate_cost_recursive(left)?;
466 let right_cost = self.estimate_cost_recursive(right)?;
467
468 let result_cardinality = left_cost.cardinality + right_cost.cardinality;
470
471 let union_overhead = (result_cardinality as f64 * 0.1) * self.config.cpu_cost_per_op;
473
474 let total_cpu = left_cost.cpu_cost + right_cost.cpu_cost + union_overhead;
475 let total_io = left_cost.io_cost + right_cost.io_cost;
476 let total_memory = left_cost.memory_cost + right_cost.memory_cost;
477
478 Ok(CostEstimate::new(
479 total_cpu,
480 total_io,
481 total_memory,
482 0.0,
483 result_cardinality,
484 ))
485 }
486
487 fn estimate_filter_cost(
489 &self,
490 expression: &Expression,
491 input: &Algebra,
492 ) -> Result<CostEstimate> {
493 let input_cost = self.estimate_cost_recursive(input)?;
494
495 let filter_selectivity = self.estimate_expression_selectivity(expression);
497 let result_cardinality = (input_cost.cardinality as f64 * filter_selectivity) as usize;
498
499 let filter_cpu_cost = input_cost.cardinality as f64
501 * self.config.cpu_cost_per_op
502 * self.estimate_expression_complexity(expression);
503
504 let total_cpu = input_cost.cpu_cost + filter_cpu_cost;
505 let total_io = input_cost.io_cost;
506 let total_memory = input_cost.memory_cost;
507
508 Ok(
509 CostEstimate::new(total_cpu, total_io, total_memory, 0.0, result_cardinality)
510 .with_selectivity(filter_selectivity),
511 )
512 }
513
514 fn estimate_project_cost(&self, input: &Algebra) -> Result<CostEstimate> {
516 let input_cost = self.estimate_cost_recursive(input)?;
517
518 let memory_reduction = 0.8; let projection_cpu = input_cost.cardinality as f64 * self.config.cpu_cost_per_op * 0.1;
521
522 let total_cpu = input_cost.cpu_cost + projection_cpu;
523 let total_memory = input_cost.memory_cost * memory_reduction;
524
525 Ok(CostEstimate::new(
526 total_cpu,
527 input_cost.io_cost,
528 total_memory,
529 0.0,
530 input_cost.cardinality,
531 ))
532 }
533
534 fn estimate_extend_cost(
536 &self,
537 expression: &Expression,
538 input: &Algebra,
539 ) -> Result<CostEstimate> {
540 let input_cost = self.estimate_cost_recursive(input)?;
541
542 let expression_cost = input_cost.cardinality as f64
544 * self.config.cpu_cost_per_op
545 * self.estimate_expression_complexity(expression);
546
547 let total_cpu = input_cost.cpu_cost + expression_cost;
548
549 Ok(CostEstimate::new(
550 total_cpu,
551 input_cost.io_cost,
552 input_cost.memory_cost,
553 0.0,
554 input_cost.cardinality,
555 ))
556 }
557
558 fn estimate_distinct_cost(&self, input: &Algebra) -> Result<CostEstimate> {
560 let input_cost = self.estimate_cost_recursive(input)?;
561
562 let distinct_cardinality = (input_cost.cardinality as f64 * 0.8) as usize; let sort_cost = input_cost.cardinality as f64
565 * (input_cost.cardinality as f64).log2()
566 * self.config.cpu_cost_per_op;
567
568 let total_cpu = input_cost.cpu_cost + sort_cost;
569 let total_memory = input_cost.memory_cost * 1.5; Ok(CostEstimate::new(
572 total_cpu,
573 input_cost.io_cost,
574 total_memory,
575 0.0,
576 distinct_cardinality,
577 ))
578 }
579
580 fn estimate_reduced_cost(&self, input: &Algebra) -> Result<CostEstimate> {
582 let input_cost = self.estimate_cost_recursive(input)?;
584 let reduced_cpu = input_cost.cardinality as f64 * self.config.cpu_cost_per_op * 0.1;
585
586 let total_cpu = input_cost.cpu_cost + reduced_cpu;
587
588 Ok(CostEstimate::new(
589 total_cpu,
590 input_cost.io_cost,
591 input_cost.memory_cost,
592 0.0,
593 input_cost.cardinality,
594 ))
595 }
596
597 fn estimate_order_by_cost(&self, input: &Algebra) -> Result<CostEstimate> {
599 let input_cost = self.estimate_cost_recursive(input)?;
600
601 let sort_cost = input_cost.cardinality as f64
603 * (input_cost.cardinality as f64).log2()
604 * self.config.cpu_cost_per_op;
605
606 let total_cpu = input_cost.cpu_cost + sort_cost;
607 let total_memory = input_cost.memory_cost * 2.0; Ok(CostEstimate::new(
610 total_cpu,
611 input_cost.io_cost,
612 total_memory,
613 0.0,
614 input_cost.cardinality,
615 ))
616 }
617
618 fn estimate_slice_cost(
620 &self,
621 input: &Algebra,
622 offset: Option<usize>,
623 limit: Option<usize>,
624 ) -> Result<CostEstimate> {
625 let input_cost = self.estimate_cost_recursive(input)?;
626
627 let offset_val = offset.unwrap_or(0);
628 let limit_val = limit.unwrap_or(input_cost.cardinality);
629 let result_cardinality = limit_val.min(input_cost.cardinality.saturating_sub(offset_val));
630
631 let slice_cpu = result_cardinality as f64 * self.config.cpu_cost_per_op * 0.01;
633
634 Ok(CostEstimate::new(
635 input_cost.cpu_cost + slice_cpu,
636 input_cost.io_cost,
637 input_cost.memory_cost,
638 0.0,
639 result_cardinality,
640 ))
641 }
642
643 fn estimate_group_cost(&self, input: &Algebra) -> Result<CostEstimate> {
645 let input_cost = self.estimate_cost_recursive(input)?;
646
647 let group_cardinality = (input_cost.cardinality as f64 * 0.1) as usize; let group_cost = input_cost.cardinality as f64
650 * (input_cost.cardinality as f64).log2()
651 * self.config.cpu_cost_per_op;
652
653 let total_cpu = input_cost.cpu_cost + group_cost;
654 let total_memory = input_cost.memory_cost * 1.5;
655
656 Ok(CostEstimate::new(
657 total_cpu,
658 input_cost.io_cost,
659 total_memory,
660 0.0,
661 group_cardinality,
662 ))
663 }
664
665 fn estimate_join_cost_detailed(
667 &self,
668 left_cardinality: usize,
669 right_cardinality: usize,
670 selectivity: f64,
671 algorithm: JoinAlgorithm,
672 ) -> CostEstimate {
673 let result_cardinality =
674 (left_cardinality as f64 * right_cardinality as f64 * selectivity) as usize;
675
676 match algorithm {
677 JoinAlgorithm::HashJoin => {
678 let build_cost =
679 left_cardinality.min(right_cardinality) as f64 * self.config.cpu_cost_per_op;
680 let probe_cost =
681 left_cardinality.max(right_cardinality) as f64 * self.config.cpu_cost_per_op;
682 let cpu_cost = (build_cost + probe_cost)
683 * self.config.calibration.join_factors.hash_join_factor;
684
685 let memory_cost = (left_cardinality.min(right_cardinality) * 50) as f64
686 * self.config.memory_cost_per_byte;
687 let io_cost = 0.0; CostEstimate::new(cpu_cost, io_cost, memory_cost, 0.0, result_cardinality)
690 }
691 JoinAlgorithm::SortMergeJoin => {
692 let sort_cost_left = left_cardinality as f64
693 * (left_cardinality as f64).log2()
694 * self.config.cpu_cost_per_op;
695 let sort_cost_right = right_cardinality as f64
696 * (right_cardinality as f64).log2()
697 * self.config.cpu_cost_per_op;
698 let merge_cost =
699 (left_cardinality + right_cardinality) as f64 * self.config.cpu_cost_per_op;
700 let cpu_cost = (sort_cost_left + sort_cost_right + merge_cost)
701 * self.config.calibration.join_factors.sort_merge_join_factor;
702
703 let memory_cost = (left_cardinality + right_cardinality) as f64
704 * 50.0
705 * self.config.memory_cost_per_byte;
706 let io_cost = 0.0;
707
708 CostEstimate::new(cpu_cost, io_cost, memory_cost, 0.0, result_cardinality)
709 }
710 JoinAlgorithm::NestedLoopJoin => {
711 let cpu_cost = (left_cardinality as f64 * right_cardinality as f64)
712 * self.config.cpu_cost_per_op
713 * self.config.calibration.join_factors.nested_loop_join_factor;
714 let memory_cost = 1000.0 * self.config.memory_cost_per_byte; let io_cost = 0.0;
716
717 CostEstimate::new(cpu_cost, io_cost, memory_cost, 0.0, result_cardinality)
718 }
719 JoinAlgorithm::IndexJoin => {
720 let cpu_cost = (left_cardinality as f64 * (right_cardinality as f64).log2())
721 * self.config.cpu_cost_per_op
722 * self.config.calibration.join_factors.index_join_factor;
723 let io_cost = left_cardinality as f64 * self.config.io_cost_per_page * 0.1; let memory_cost = 5000.0 * self.config.memory_cost_per_byte;
725
726 CostEstimate::new(cpu_cost, io_cost, memory_cost, 0.0, result_cardinality)
727 }
728 }
729 }
730
731 fn choose_join_algorithm(&self, left_size: usize, right_size: usize) -> JoinAlgorithm {
733 let smaller = left_size.min(right_size);
734 let larger = left_size.max(right_size);
735
736 if smaller < 1000 {
737 JoinAlgorithm::NestedLoopJoin
738 } else if smaller < 10000 && larger > smaller * 10 {
739 JoinAlgorithm::IndexJoin
740 } else if smaller * 8 < self.config.available_memory / 100 {
741 JoinAlgorithm::HashJoin
742 } else {
743 JoinAlgorithm::SortMergeJoin
744 }
745 }
746
747 fn estimate_pattern_selectivity(&self, pattern: &TriplePattern) -> f64 {
749 let mut specificity = 0;
750
751 if !matches!(pattern.subject, crate::algebra::Term::Variable(_)) {
752 specificity += 1;
753 }
754 if !matches!(pattern.predicate, crate::algebra::Term::Variable(_)) {
755 specificity += 1;
756 }
757 if !matches!(pattern.object, crate::algebra::Term::Variable(_)) {
758 specificity += 1;
759 }
760
761 match specificity {
762 0 => 1.0, 1 => 0.1, 2 => 0.01, 3 => 0.001, _ => 0.0001,
767 }
768 }
769
770 fn determine_io_pattern(&self, pattern: &TriplePattern) -> IOPattern {
772 if matches!(pattern.subject, crate::algebra::Term::Variable(_))
774 && matches!(pattern.predicate, crate::algebra::Term::Variable(_))
775 && matches!(pattern.object, crate::algebra::Term::Variable(_))
776 {
777 IOPattern::FullScan
778 } else if !matches!(pattern.predicate, crate::algebra::Term::Variable(_)) {
779 IOPattern::IndexScan
780 } else {
781 IOPattern::Random
782 }
783 }
784
785 fn estimate_pages_accessed(&self, cardinality: usize, pattern: IOPattern) -> usize {
787 let bytes_per_triple = 100; let total_bytes = cardinality * bytes_per_triple;
789 let pages = (total_bytes + self.config.page_size - 1) / self.config.page_size;
790
791 match pattern {
792 IOPattern::Sequential => pages,
793 IOPattern::Random => pages * 2, IOPattern::IndexScan => pages / 2, IOPattern::FullScan => pages,
796 }
797 }
798
799 fn estimate_expression_selectivity(&self, expression: &Expression) -> f64 {
801 match expression {
802 Expression::Binary { op: operator, .. } => match operator {
803 crate::algebra::BinaryOperator::Equal => 0.1,
804 crate::algebra::BinaryOperator::NotEqual => 0.9,
805 crate::algebra::BinaryOperator::Less => 0.3,
806 crate::algebra::BinaryOperator::LessEqual => 0.4,
807 crate::algebra::BinaryOperator::Greater => 0.3,
808 crate::algebra::BinaryOperator::GreaterEqual => 0.4,
809 _ => 0.5,
810 },
811 Expression::Function { name, .. } => match name.as_str() {
812 "contains" => 0.2,
813 "startsWith" => 0.1,
814 "endsWith" => 0.1,
815 "regex" => 0.05,
816 _ => 0.5,
817 },
818 _ => 0.5, }
820 }
821
822 fn estimate_expression_complexity(&self, expression: &Expression) -> f64 {
824 match expression {
825 Expression::Variable(_) | Expression::Literal(_) => 1.0,
826 Expression::Binary { .. } => 2.0,
827 Expression::Unary { .. } => 1.5,
828 Expression::Function { name, args } => {
829 let base_cost = match name.as_str() {
830 "regex" => 10.0,
831 "contains" => 3.0,
832 "startsWith" | "endsWith" => 2.0,
833 _ => 5.0,
834 };
835 base_cost + args.len() as f64
836 }
837 Expression::Conditional { .. } => 3.0,
838 _ => 2.0,
839 }
840 }
841
842 fn algebra_to_key(&self, algebra: &Algebra) -> String {
844 format!("{algebra:?}")
846 }
847
848 pub fn clear_cache(&mut self) {
850 self.cached_estimates.clear();
851 }
852
853 pub fn update_with_feedback(
855 &mut self,
856 algebra: &Algebra,
857 actual_cost: f64,
858 actual_cardinality: usize,
859 ) {
860 let predicted = self.estimate_cost(algebra).unwrap_or(CostEstimate::zero());
862
863 if predicted.total_cost > 0.0 {
864 let cost_ratio = actual_cost / predicted.total_cost;
865 let _cardinality_ratio = actual_cardinality as f64 / predicted.cardinality as f64;
866
867 self.config.calibration.cpu_scale =
869 (self.config.calibration.cpu_scale + cost_ratio) / 2.0;
870
871 self.clear_cache();
873 }
874 }
875}
876
877#[derive(Debug, Clone, PartialEq, Eq)]
879pub enum JoinAlgorithm {
880 HashJoin,
881 SortMergeJoin,
882 NestedLoopJoin,
883 IndexJoin,
884}
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889 use crate::algebra::{Term, Variable};
890 use oxirs_core::model::NamedNode;
891
892 #[test]
893 fn test_triple_pattern_cost_estimation() {
894 let cost_model = CostModel::new(CostModelConfig::default());
895
896 let pattern = TriplePattern {
897 subject: Term::Variable(Variable::new("s").unwrap()),
898 predicate: Term::Iri(NamedNode::new_unchecked("http://example.org/predicate")),
899 object: Term::Variable(Variable::new("o").unwrap()),
900 };
901
902 let cost = cost_model.estimate_triple_pattern_cost(&pattern).unwrap();
903
904 assert!(cost.total_cost > 0.0);
905 assert!(cost.cardinality > 0);
906 assert!(cost.selectivity > 0.0 && cost.selectivity <= 1.0);
907 }
908
909 #[test]
910 fn test_join_algorithm_selection() {
911 let cost_model = CostModel::new(CostModelConfig::default());
912
913 assert_eq!(
914 cost_model.choose_join_algorithm(100, 1000),
915 JoinAlgorithm::NestedLoopJoin
916 );
917 assert_eq!(
918 cost_model.choose_join_algorithm(10000, 100000),
919 JoinAlgorithm::HashJoin
920 );
921 }
922
923 #[test]
924 fn test_cost_estimate_operations() {
925 let mut estimate = CostEstimate::new(10.0, 5.0, 2.0, 1.0, 1000);
926 estimate.add_operation_cost("test_op", 3.0);
927
928 assert_eq!(estimate.total_cost, 21.0); assert!(estimate.operation_costs.contains_key("test_op"));
930 }
931
932 #[test]
933 fn test_pattern_selectivity_estimation() {
934 let cost_model = CostModel::new(CostModelConfig::default());
935
936 let pattern1 = TriplePattern {
938 subject: Term::Variable(Variable::new("s").unwrap()),
939 predicate: Term::Variable(Variable::new("p").unwrap()),
940 object: Term::Variable(Variable::new("o").unwrap()),
941 };
942
943 let selectivity1 = cost_model.estimate_pattern_selectivity(&pattern1);
944 assert_eq!(selectivity1, 1.0);
945
946 let pattern2 = TriplePattern {
948 subject: Term::Variable(Variable::new("s").unwrap()),
949 predicate: Term::Iri(NamedNode::new_unchecked("http://example.org/predicate")),
950 object: Term::Variable(Variable::new("o").unwrap()),
951 };
952
953 let selectivity2 = cost_model.estimate_pattern_selectivity(&pattern2);
954 assert_eq!(selectivity2, 0.1);
955 }
956}