linkrs_query/optimizer/stats/feedback/
selectivity.rs1use parking_lot::RwLock;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
30pub struct FeedbackDrivenSelectivity {
31 estimated_selectivity: f64,
33 actual_selectivity_ewma: f64,
35 correction_factor: f64,
37 feedback_count: u64,
39 alpha: f64,
41 min_correction: f64,
43 max_correction: f64,
45 selectivity_cap: f64,
47 cumulative_estimation_error: f64,
49 error_sum_squares: f64,
51 total_input_rows: u64,
54 pending_outlier: Option<MeasuredSample>,
57}
58
59#[derive(Debug, Clone, Copy)]
62struct MeasuredSample {
63 ratio: f64,
64 input_rows: u64,
65}
66
67const OUTLIER_DEVIATION: f64 = 4.0;
71const CORROBORATION_AGREEMENT: f64 = 2.0;
74const FULL_WEIGHT_INPUT_ROWS: f64 = 1000.0;
78
79impl FeedbackDrivenSelectivity {
80 pub fn new(estimated_selectivity: f64) -> Self {
82 Self {
83 estimated_selectivity,
84 actual_selectivity_ewma: estimated_selectivity,
85 correction_factor: 1.0,
86 feedback_count: 0,
87 alpha: 0.3,
88 min_correction: 0.1,
89 max_correction: 10.0,
90 selectivity_cap: 0.99,
91 cumulative_estimation_error: 0.0,
92 error_sum_squares: 0.0,
93 total_input_rows: 0,
94 pending_outlier: None,
95 }
96 }
97
98 pub fn with_params(
100 estimated_selectivity: f64,
101 alpha: f64,
102 min_correction: f64,
103 max_correction: f64,
104 ) -> Self {
105 Self {
106 estimated_selectivity,
107 actual_selectivity_ewma: estimated_selectivity,
108 correction_factor: 1.0,
109 feedback_count: 0,
110 alpha,
111 min_correction,
112 max_correction,
113 selectivity_cap: 0.99,
114 cumulative_estimation_error: 0.0,
115 error_sum_squares: 0.0,
116 total_input_rows: 0,
117 pending_outlier: None,
118 }
119 }
120
121 pub fn estimated_selectivity(&self) -> f64 {
123 self.estimated_selectivity
124 }
125
126 pub fn set_estimated_selectivity(&mut self, estimated: f64) {
129 if estimated > 0.0 && estimated.is_finite() {
130 self.estimated_selectivity = estimated;
131 self.actual_selectivity_ewma = estimated * self.correction_factor;
132 }
133 }
134
135 pub fn corrected_selectivity(&self) -> f64 {
137 self.corrected_for_estimate(self.estimated_selectivity)
138 }
139
140 pub fn corrected_for_estimate(&self, estimated: f64) -> f64 {
143 (estimated * self.correction_factor)
144 .clamp(self.min_correction * estimated, self.selectivity_cap)
145 }
146
147 pub fn correction_factor(&self) -> f64 {
149 self.correction_factor
150 }
151
152 pub fn feedback_count(&self) -> u64 {
154 self.feedback_count
155 }
156
157 pub fn total_input_rows(&self) -> u64 {
159 self.total_input_rows
160 }
161
162 pub fn estimation_confidence(&self) -> f64 {
167 let x = self.feedback_count as f64 * 0.1;
170 1.0 / (1.0 + (-x).exp())
171 }
172
173 pub fn avg_estimation_error(&self) -> f64 {
175 if self.feedback_count == 0 {
176 return 1.0;
177 }
178 self.cumulative_estimation_error / self.feedback_count as f64
179 }
180
181 pub fn error_std_dev(&self) -> f64 {
183 if self.feedback_count < 2 {
184 return 0.0;
185 }
186 let n = self.feedback_count as f64;
187 let mean = self.cumulative_estimation_error / n;
188 let variance = (self.error_sum_squares / n) - (mean * mean);
189 variance.max(0.0).sqrt()
190 }
191
192 pub fn update_with_feedback(&mut self, actual_selectivity: f64) {
196 if self.estimated_selectivity <= 0.0 {
197 return;
198 }
199
200 let ratio = actual_selectivity / self.estimated_selectivity;
201
202 let estimated = self.corrected_selectivity();
204 let error = (actual_selectivity - estimated).abs();
205 self.cumulative_estimation_error += error;
206 self.error_sum_squares += error * error;
207
208 self.correction_factor = (1.0 - self.alpha) * self.correction_factor + self.alpha * ratio;
210
211 self.correction_factor = self
213 .correction_factor
214 .clamp(self.min_correction, self.max_correction);
215
216 self.actual_selectivity_ewma =
218 (1.0 - self.alpha) * self.actual_selectivity_ewma + self.alpha * actual_selectivity;
219
220 self.feedback_count += 1;
221 }
222
223 pub fn update_with_ratio(&mut self, ratio: f64) {
230 if self.estimated_selectivity <= 0.0 || !ratio.is_finite() {
231 return;
232 }
233
234 let error = (ratio - self.correction_factor).abs();
235 self.cumulative_estimation_error += error;
236 self.error_sum_squares += error * error;
237
238 self.correction_factor = (1.0 - self.alpha) * self.correction_factor + self.alpha * ratio;
240
241 self.correction_factor = self
243 .correction_factor
244 .clamp(self.min_correction, self.max_correction);
245
246 self.actual_selectivity_ewma = self.estimated_selectivity * self.correction_factor;
248
249 self.feedback_count += 1;
250 }
251
252 pub fn update_with_measured_ratio(&mut self, ratio: f64, input_rows: u64) {
267 if self.estimated_selectivity <= 0.0 || !ratio.is_finite() || ratio < 0.0 {
268 return;
269 }
270 let sample = MeasuredSample { ratio, input_rows };
271 if self.feedback_count > 0 && self.deviates(sample.ratio) {
272 if let Some(staged) = self.pending_outlier {
273 if agrees(staged.ratio, sample.ratio) {
274 self.pending_outlier = None;
275 self.apply_measured(staged);
276 self.apply_measured(sample);
277 } else {
278 self.pending_outlier = Some(sample);
279 }
280 } else {
281 self.pending_outlier = Some(sample);
282 }
283 return;
284 }
285 self.apply_measured(sample);
286 }
287
288 fn deviates(&self, ratio: f64) -> bool {
290 let factor = self.correction_factor;
291 if factor <= 0.0 || ratio <= 0.0 {
292 return ratio != factor;
293 }
294 ratio.max(factor) / ratio.min(factor) > OUTLIER_DEVIATION
295 }
296
297 fn apply_measured(&mut self, sample: MeasuredSample) {
300 let weight = (sample.input_rows as f64).max(1.0);
301 let step = self.alpha * weight / (weight + FULL_WEIGHT_INPUT_ROWS);
302 let error = (sample.ratio - self.correction_factor).abs();
303 self.cumulative_estimation_error += error;
304 self.error_sum_squares += error * error;
305 self.correction_factor = (1.0 - step) * self.correction_factor + step * sample.ratio;
306 self.correction_factor = self
307 .correction_factor
308 .clamp(self.min_correction, self.max_correction);
309 self.actual_selectivity_ewma = self.estimated_selectivity * self.correction_factor;
310 self.total_input_rows += sample.input_rows;
311 self.feedback_count += 1;
312 }
313
314 pub fn update_with_batch(&mut self, actual_selectivities: &[f64]) {
316 for &selectivity in actual_selectivities {
317 self.update_with_feedback(selectivity);
318 }
319 }
320
321 pub fn reset_correction(&mut self) {
323 self.correction_factor = 1.0;
324 self.actual_selectivity_ewma = self.estimated_selectivity;
325 self.feedback_count = 0;
326 self.cumulative_estimation_error = 0.0;
327 self.error_sum_squares = 0.0;
328 self.total_input_rows = 0;
329 self.pending_outlier = None;
330 }
331
332 pub fn set_alpha(&mut self, alpha: f64) {
334 self.alpha = alpha.clamp(0.0, 1.0);
335 }
336
337 pub fn set_correction_range(&mut self, min: f64, max: f64) {
339 self.min_correction = min.max(0.01);
340 self.max_correction = max.max(self.min_correction);
341 }
342
343 pub fn set_selectivity_cap(&mut self, cap: f64) {
345 self.selectivity_cap = cap.clamp(0.5, 1.0);
346 }
347}
348
349impl Default for FeedbackDrivenSelectivity {
350 fn default() -> Self {
351 Self::new(0.1)
352 }
353}
354
355#[derive(Debug)]
370pub struct SelectivityFeedbackManager {
371 feedbacks: RwLock<HashMap<String, FeedbackDrivenSelectivity>>,
373 default_alpha: f64,
375 default_min_correction: f64,
377 default_max_correction: f64,
379}
380
381impl SelectivityFeedbackManager {
382 pub fn new() -> Self {
384 Self {
385 feedbacks: RwLock::new(HashMap::new()),
386 default_alpha: 0.3,
387 default_min_correction: 0.1,
388 default_max_correction: 10.0,
389 }
390 }
391
392 pub fn with_params(alpha: f64, min_correction: f64, max_correction: f64) -> Self {
394 Self {
395 feedbacks: RwLock::new(HashMap::new()),
396 default_alpha: alpha,
397 default_min_correction: min_correction,
398 default_max_correction: max_correction,
399 }
400 }
401
402 pub fn register_condition(&self, key: String, estimated_selectivity: f64) {
408 let feedback = FeedbackDrivenSelectivity::with_params(
409 estimated_selectivity,
410 self.default_alpha,
411 self.default_min_correction,
412 self.default_max_correction,
413 );
414 self.feedbacks.write().entry(key).or_insert(feedback);
415 }
416
417 pub fn refresh_estimated(&self, key: &str, estimated_selectivity: f64) -> bool {
422 let mut feedbacks = self.feedbacks.write();
423 if let Some(feedback) = feedbacks.get_mut(key) {
424 feedback.set_estimated_selectivity(estimated_selectivity);
425 true
426 } else {
427 false
428 }
429 }
430
431 pub fn get_corrected_selectivity(&self, key: &str) -> Option<f64> {
433 self.feedbacks
434 .read()
435 .get(key)
436 .map(|f| f.corrected_selectivity())
437 }
438
439 pub fn snapshot(&self) -> SelectivitySnapshot {
442 SelectivitySnapshot {
443 corrections: self.feedbacks.read().clone(),
444 }
445 }
446
447 pub fn update_feedback(&self, key: &str, actual_selectivity: f64) -> bool {
449 let mut feedbacks = self.feedbacks.write();
450 if let Some(feedback) = feedbacks.get_mut(key) {
451 feedback.update_with_feedback(actual_selectivity);
452 true
453 } else {
454 false
455 }
456 }
457
458 pub fn update_feedback_ratio(&self, key: &str, ratio: f64) -> bool {
463 let mut feedbacks = self.feedbacks.write();
464 if let Some(feedback) = feedbacks.get_mut(key) {
465 feedback.update_with_ratio(ratio);
466 true
467 } else {
468 false
469 }
470 }
471
472 pub fn update_measured_ratio(&self, key: &str, ratio: f64, input_rows: u64) -> bool {
479 let mut feedbacks = self.feedbacks.write();
480 if let Some(feedback) = feedbacks.get_mut(key) {
481 feedback.update_with_measured_ratio(ratio, input_rows);
482 true
483 } else {
484 false
485 }
486 }
487
488 pub fn remove_feedback_by_space(&self, space_prefix: &str) -> usize {
491 let mut feedbacks = self.feedbacks.write();
492 let mut removed = 0;
493 feedbacks.retain(|key, _| {
494 let keep = !key.starts_with(space_prefix);
495 if !keep {
496 removed += 1;
497 }
498 keep
499 });
500 removed
501 }
502
503 pub fn update_feedback_batch(&self, updates: &[(String, f64)]) {
505 let mut feedbacks = self.feedbacks.write();
506 for (key, actual_selectivity) in updates {
507 if let Some(feedback) = feedbacks.get_mut(key) {
508 feedback.update_with_feedback(*actual_selectivity);
509 }
510 }
511 }
512
513 pub fn get_feedback(&self, key: &str) -> Option<FeedbackDrivenSelectivity> {
515 self.feedbacks.read().get(key).cloned()
516 }
517
518 pub fn get_all_keys(&self) -> Vec<String> {
520 self.feedbacks.read().keys().cloned().collect()
521 }
522
523 pub fn clear_all(&self) {
525 self.feedbacks.write().clear();
526 }
527
528 pub fn remove_feedback(&self, key: &str) -> Option<FeedbackDrivenSelectivity> {
530 self.feedbacks.write().remove(key)
531 }
532
533 pub fn feedback_count(&self) -> usize {
535 self.feedbacks.read().len()
536 }
537
538 pub fn set_default_params(&mut self, alpha: f64, min_correction: f64, max_correction: f64) {
540 self.default_alpha = alpha.clamp(0.0, 1.0);
541 self.default_min_correction = min_correction.max(0.01);
542 self.default_max_correction = max_correction.max(self.default_min_correction);
543 }
544}
545
546impl Default for SelectivityFeedbackManager {
547 fn default() -> Self {
548 Self::new()
549 }
550}
551
552fn agrees(first: f64, second: f64) -> bool {
555 if first <= 0.0 || second <= 0.0 {
556 return first == second;
557 }
558 first.max(second) / first.min(second) <= CORROBORATION_AGREEMENT
559}
560
561pub const MIN_STABLE_FEEDBACK_COUNT: u64 = 3;
566
567#[derive(Debug, Clone, Default)]
575pub struct SelectivitySnapshot {
576 corrections: HashMap<String, FeedbackDrivenSelectivity>,
577}
578
579impl SelectivitySnapshot {
580 pub fn corrected(&self, key: &str, fresh_estimate: f64) -> Option<f64> {
585 self.corrections
586 .get(key)
587 .filter(|feedback| feedback.feedback_count() >= MIN_STABLE_FEEDBACK_COUNT)
588 .map(|feedback| feedback.corrected_for_estimate(fresh_estimate))
589 }
590
591 pub fn len(&self) -> usize {
593 self.corrections.len()
594 }
595
596 pub fn is_empty(&self) -> bool {
598 self.corrections.is_empty()
599 }
600}
601
602impl Clone for SelectivityFeedbackManager {
603 fn clone(&self) -> Self {
604 Self {
605 feedbacks: RwLock::new(self.feedbacks.read().clone()),
606 default_alpha: self.default_alpha,
607 default_min_correction: self.default_min_correction,
608 default_max_correction: self.default_max_correction,
609 }
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616
617 #[test]
618 fn test_feedback_driven_selectivity() {
619 let mut feedback = FeedbackDrivenSelectivity::new(0.1);
620 assert_eq!(feedback.estimated_selectivity(), 0.1);
621
622 feedback.update_with_feedback(0.15);
624 feedback.update_with_feedback(0.12);
625
626 let corrected = feedback.corrected_selectivity();
628 assert!(corrected > 0.0 && corrected <= 0.99);
629
630 assert_eq!(feedback.feedback_count(), 2);
632 }
633
634 #[test]
635 fn test_feedback_correction_range() {
636 let mut feedback = FeedbackDrivenSelectivity::new(0.5);
637
638 for _ in 0..100 {
640 feedback.update_with_feedback(0.01); }
642
643 assert!(feedback.correction_factor() >= 0.1);
645 }
646
647 #[test]
648 fn test_estimation_confidence() {
649 let mut feedback = FeedbackDrivenSelectivity::new(0.1);
650 let initial_confidence = feedback.estimation_confidence();
652 assert!(initial_confidence < 0.55 && initial_confidence > 0.45);
653
654 for i in 0..100 {
656 feedback.update_with_feedback(0.1 + (i as f64 * 0.001));
657 }
658
659 assert!(feedback.estimation_confidence() > 0.9); }
661
662 #[test]
663 fn test_avg_estimation_error() {
664 let mut feedback = FeedbackDrivenSelectivity::new(0.5);
665
666 assert_eq!(feedback.avg_estimation_error(), 1.0);
668
669 feedback.update_with_feedback(0.5); feedback.update_with_feedback(0.6); assert!(feedback.avg_estimation_error() < 1.0);
674 }
675
676 #[test]
677 fn test_selectivity_feedback_manager() {
678 let manager = SelectivityFeedbackManager::new();
679 manager.register_condition("age > 25".to_string(), 0.3);
680 manager.register_condition("salary > 5000".to_string(), 0.2);
681
682 assert_eq!(manager.feedback_count(), 2);
683
684 manager.update_feedback("age > 25", 0.35);
686 manager.update_feedback("salary > 5000", 0.18);
687
688 let corrected_age = manager.get_corrected_selectivity("age > 25");
690 assert!(corrected_age.is_some());
691
692 assert!(manager.get_corrected_selectivity("unknown").is_none());
694 }
695
696 #[test]
697 fn test_selectivity_cap() {
698 let mut feedback = FeedbackDrivenSelectivity::new(0.9);
699 feedback.set_selectivity_cap(0.95);
700
701 for _ in 0..50 {
703 feedback.update_with_feedback(1.0);
704 }
705
706 assert!(feedback.corrected_selectivity() <= 0.95);
708 }
709
710 #[test]
711 fn test_measured_ratio_stages_outlier_until_corroborated() {
712 let mut feedback = FeedbackDrivenSelectivity::new(0.5);
713 for _ in 0..3 {
715 feedback.update_with_measured_ratio(1.0, 2000);
716 }
717 let baseline = feedback.correction_factor();
718 assert!((baseline - 1.0).abs() < 1e-9);
719 assert_eq!(feedback.feedback_count(), 3);
720
721 feedback.update_with_measured_ratio(0.0, 2000);
724 assert!((feedback.correction_factor() - baseline).abs() < 1e-9);
725 assert_eq!(feedback.feedback_count(), 3);
726
727 feedback.update_with_measured_ratio(0.0, 2000);
730 assert!(feedback.correction_factor() < baseline);
731 assert_eq!(feedback.feedback_count(), 5);
732 }
733
734 #[test]
735 fn test_measured_ratio_weights_step_by_input_rows() {
736 let mut tiny = FeedbackDrivenSelectivity::new(0.5);
737 tiny.update_with_measured_ratio(0.1, 10);
738 let mut large = FeedbackDrivenSelectivity::new(0.5);
739 large.update_with_measured_ratio(0.1, 100_000);
740 assert!(large.correction_factor() < tiny.correction_factor());
743 assert_eq!(tiny.total_input_rows(), 10);
744 assert_eq!(large.total_input_rows(), 100_000);
745 }
746
747 #[test]
748 fn test_measured_ratio_rejects_invalid_samples() {
749 let mut feedback = FeedbackDrivenSelectivity::new(0.5);
750 feedback.update_with_measured_ratio(f64::NAN, 100);
751 feedback.update_with_measured_ratio(-1.0, 100);
752 assert_eq!(feedback.feedback_count(), 0);
753 assert!((feedback.correction_factor() - 1.0).abs() < 1e-9);
754 }
755}