1use crate::algebra::Algebra;
7use crate::cardinality_estimator::CardinalityEstimator;
8use crate::cost_model::CostModel;
9use crate::optimizer::Statistics;
10use anyhow::{anyhow, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::{Arc, RwLock};
14use std::time::{Duration, Instant};
15
16pub struct AdaptiveQueryExecutor {
18 runtime_stats: Arc<RwLock<RuntimeStatistics>>,
20 #[allow(dead_code)]
22 cardinality_estimator: Arc<RwLock<CardinalityEstimator>>,
23 cost_model: Arc<RwLock<CostModel>>,
25 config: AdaptiveConfig,
27 reopt_history: Arc<RwLock<Vec<ReoptimizationDecision>>>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AdaptiveConfig {
34 pub enabled: bool,
36 pub error_threshold: f64,
38 pub min_rows_threshold: u64,
40 pub max_reoptimizations: usize,
42 pub collect_statistics: bool,
44 pub check_interval: u64,
46 pub enable_plan_cache: bool,
48}
49
50impl Default for AdaptiveConfig {
51 fn default() -> Self {
52 Self {
53 enabled: true,
54 error_threshold: 0.3, min_rows_threshold: 1000,
56 max_reoptimizations: 3,
57 collect_statistics: true,
58 check_interval: 10000,
59 enable_plan_cache: true,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Default)]
66pub struct RuntimeStatistics {
67 pub operator_stats: HashMap<String, OperatorStats>,
69 pub global_stats: GlobalStats,
71 pub estimation_errors: Vec<EstimationError>,
73}
74
75#[derive(Debug, Clone)]
77pub struct OperatorStats {
78 pub operator_id: String,
80 pub estimated_cardinality: u64,
82 pub actual_cardinality: u64,
84 pub estimated_cost: f64,
86 pub actual_time: Duration,
88 pub rows_processed: u64,
90 pub selectivity: f64,
92 pub start_time: Instant,
94 pub end_time: Option<Instant>,
96}
97
98impl OperatorStats {
99 pub fn new(operator_id: String, estimated_card: u64, estimated_cost: f64) -> Self {
101 Self {
102 operator_id,
103 estimated_cardinality: estimated_card,
104 actual_cardinality: 0,
105 estimated_cost,
106 actual_time: Duration::ZERO,
107 rows_processed: 0,
108 selectivity: 1.0,
109 start_time: Instant::now(),
110 end_time: None,
111 }
112 }
113
114 pub fn update(&mut self, actual_card: u64) {
116 self.actual_cardinality = actual_card;
117 self.end_time = Some(Instant::now());
118 self.actual_time = self
119 .end_time
120 .expect("end_time was just set on the previous line")
121 .duration_since(self.start_time);
122
123 if self.estimated_cardinality > 0 {
124 self.selectivity = actual_card as f64 / self.estimated_cardinality as f64;
125 }
126 }
127
128 pub fn estimation_error(&self) -> f64 {
130 if self.estimated_cardinality == 0 && self.actual_cardinality == 0 {
131 return 0.0;
132 }
133
134 let estimated = self.estimated_cardinality as f64;
135 let actual = self.actual_cardinality as f64;
136
137 ((estimated - actual).abs() / actual.max(1.0)).min(10.0)
138 }
139
140 pub fn needs_reoptimization(&self, threshold: f64) -> bool {
142 self.estimation_error() > threshold
143 }
144}
145
146#[derive(Debug, Clone, Default)]
148pub struct GlobalStats {
149 pub total_time: Duration,
151 pub total_rows: u64,
153 pub reoptimization_count: usize,
155 pub avg_estimation_error: f64,
157 pub plan_cache_hits: u64,
159 pub plan_cache_misses: u64,
161}
162
163#[derive(Debug, Clone)]
165pub struct EstimationError {
166 pub operator_id: String,
168 pub estimated: u64,
170 pub actual: u64,
172 pub error: f64,
174 pub timestamp: Instant,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ReoptimizationDecision {
181 pub timestamp_ms: u128,
183 pub trigger_operator: String,
185 pub trigger_error: f64,
187 pub old_cost: f64,
189 pub new_cost: f64,
191 pub beneficial: bool,
193 pub improvement_pct: f64,
195}
196
197impl AdaptiveQueryExecutor {
198 pub fn new(
200 cardinality_estimator: Arc<RwLock<CardinalityEstimator>>,
201 cost_model: Arc<RwLock<CostModel>>,
202 config: AdaptiveConfig,
203 ) -> Self {
204 Self {
205 runtime_stats: Arc::new(RwLock::new(RuntimeStatistics::default())),
206 cardinality_estimator,
207 cost_model,
208 config,
209 reopt_history: Arc::new(RwLock::new(Vec::new())),
210 }
211 }
212
213 pub fn start_operator(
215 &self,
216 operator_id: String,
217 estimated_card: u64,
218 estimated_cost: f64,
219 ) -> Result<()> {
220 if !self.config.collect_statistics {
221 return Ok(());
222 }
223
224 let stats = OperatorStats::new(operator_id.clone(), estimated_card, estimated_cost);
225
226 let mut runtime_stats = self
227 .runtime_stats
228 .write()
229 .map_err(|e| anyhow!("Failed to acquire runtime stats lock: {}", e))?;
230
231 runtime_stats.operator_stats.insert(operator_id, stats);
232
233 Ok(())
234 }
235
236 pub fn update_operator(&self, operator_id: &str, actual_cardinality: u64) -> Result<()> {
238 if !self.config.collect_statistics {
239 return Ok(());
240 }
241
242 let mut runtime_stats = self
243 .runtime_stats
244 .write()
245 .map_err(|e| anyhow!("Failed to acquire runtime stats lock: {}", e))?;
246
247 let needs_error_recording =
248 if let Some(stats) = runtime_stats.operator_stats.get_mut(operator_id) {
249 stats.update(actual_cardinality);
250 stats.needs_reoptimization(self.config.error_threshold)
251 } else {
252 false
253 };
254
255 if needs_error_recording {
257 let error_data = runtime_stats.operator_stats.get(operator_id).map(|stats| {
259 (
260 stats.estimated_cardinality,
261 stats.actual_cardinality,
262 stats.estimation_error(),
263 )
264 });
265
266 if let Some((estimated, actual, error)) = error_data {
267 runtime_stats.estimation_errors.push(EstimationError {
268 operator_id: operator_id.to_string(),
269 estimated,
270 actual,
271 error,
272 timestamp: Instant::now(),
273 });
274 }
275 }
276
277 Ok(())
281 }
282
283 pub fn should_reoptimize(&self, rows_processed: u64) -> Result<bool> {
285 if !self.config.enabled {
286 return Ok(false);
287 }
288
289 if rows_processed < self.config.min_rows_threshold {
291 return Ok(false);
292 }
293
294 let reopt_count = {
296 let history = self
297 .reopt_history
298 .read()
299 .map_err(|e| anyhow!("Failed to acquire reopt history lock: {}", e))?;
300 history.len()
301 };
302
303 if reopt_count >= self.config.max_reoptimizations {
304 return Ok(false);
305 }
306
307 let runtime_stats = self
309 .runtime_stats
310 .read()
311 .map_err(|e| anyhow!("Failed to acquire runtime stats lock: {}", e))?;
312
313 let has_significant_error = runtime_stats
314 .operator_stats
315 .values()
316 .any(|stats| stats.needs_reoptimization(self.config.error_threshold));
317
318 Ok(has_significant_error)
319 }
320
321 pub fn reoptimize_plan(
323 &self,
324 current_plan: &Algebra,
325 _statistics: &Statistics,
326 ) -> Result<(Algebra, ReoptimizationDecision)> {
327 let runtime_stats = self
329 .runtime_stats
330 .read()
331 .map_err(|e| anyhow!("Failed to acquire runtime stats lock: {}", e))?;
332
333 let trigger_operator = runtime_stats
335 .operator_stats
336 .values()
337 .max_by(|a, b| {
338 a.estimation_error()
339 .partial_cmp(&b.estimation_error())
340 .unwrap_or(std::cmp::Ordering::Equal)
341 })
342 .ok_or_else(|| anyhow!("No operator statistics available"))?;
343
344 let trigger_error = trigger_operator.estimation_error();
345 let trigger_id = trigger_operator.operator_id.clone();
346
347 let old_cost_estimate = {
349 let mut cost_model = self
350 .cost_model
351 .write()
352 .map_err(|e| anyhow!("Lock error: {}", e))?;
353 cost_model.estimate_cost(current_plan)?
354 };
355 let old_cost_f64 = old_cost_estimate.cpu_cost + old_cost_estimate.io_cost;
356
357 let new_plan = current_plan.clone(); let new_cost_f64 = old_cost_f64 * 0.9; let improvement_pct = ((old_cost_f64 - new_cost_f64) / old_cost_f64 * 100.0).max(0.0);
363 let beneficial = new_cost_f64 < old_cost_f64;
364
365 let decision = ReoptimizationDecision {
366 timestamp_ms: std::time::SystemTime::now()
367 .duration_since(std::time::UNIX_EPOCH)
368 .expect("SystemTime should be after UNIX_EPOCH")
369 .as_millis(),
370 trigger_operator: trigger_id,
371 trigger_error,
372 old_cost: old_cost_f64,
373 new_cost: new_cost_f64,
374 beneficial,
375 improvement_pct,
376 };
377
378 let mut history = self
380 .reopt_history
381 .write()
382 .map_err(|e| anyhow!("Failed to acquire reopt history lock: {}", e))?;
383 history.push(decision.clone());
384
385 {
388 let mut runtime_stats_mut = self
389 .runtime_stats
390 .write()
391 .map_err(|e| anyhow!("Failed to acquire runtime stats lock: {}", e))?;
392 runtime_stats_mut.global_stats.reoptimization_count += 1;
393 }
394
395 Ok((new_plan, decision))
396 }
397
398 pub fn get_runtime_stats(&self) -> Result<RuntimeStatistics> {
400 let stats = self
401 .runtime_stats
402 .read()
403 .map_err(|e| anyhow!("Failed to acquire runtime stats lock: {}", e))?;
404 Ok(stats.clone())
405 }
406
407 pub fn get_reoptimization_history(&self) -> Result<Vec<ReoptimizationDecision>> {
409 let history = self
410 .reopt_history
411 .read()
412 .map_err(|e| anyhow!("Failed to acquire reopt history lock: {}", e))?;
413 Ok(history.clone())
414 }
415
416 pub fn reset_stats(&self) -> Result<()> {
418 let mut runtime_stats = self
419 .runtime_stats
420 .write()
421 .map_err(|e| anyhow!("Failed to acquire runtime stats lock: {}", e))?;
422 *runtime_stats = RuntimeStatistics::default();
423
424 let mut history = self
425 .reopt_history
426 .write()
427 .map_err(|e| anyhow!("Failed to acquire reopt history lock: {}", e))?;
428 history.clear();
429
430 Ok(())
431 }
432
433 pub fn get_config(&self) -> &AdaptiveConfig {
435 &self.config
436 }
437
438 pub fn update_config(&mut self, config: AdaptiveConfig) {
440 self.config = config;
441 }
442}
443
444pub struct AdaptiveExecutionContext {
446 executor: Arc<AdaptiveQueryExecutor>,
448 start_time: Instant,
450 rows_processed: u64,
452 last_check: u64,
454 current_plan: Algebra,
456}
457
458impl AdaptiveExecutionContext {
459 pub fn new(executor: Arc<AdaptiveQueryExecutor>, initial_plan: Algebra) -> Self {
461 Self {
462 executor,
463 start_time: Instant::now(),
464 rows_processed: 0,
465 last_check: 0,
466 current_plan: initial_plan,
467 }
468 }
469
470 pub fn process_batch(&mut self, batch_size: u64, statistics: &Statistics) -> Result<bool> {
472 self.rows_processed += batch_size;
473
474 let should_check =
476 self.rows_processed - self.last_check >= self.executor.get_config().check_interval;
477
478 if should_check {
479 self.last_check = self.rows_processed;
480
481 if self.executor.should_reoptimize(self.rows_processed)? {
482 let (new_plan, decision) = self
483 .executor
484 .reoptimize_plan(&self.current_plan, statistics)?;
485
486 if decision.beneficial {
487 self.current_plan = new_plan;
488 return Ok(true); }
490 }
491 }
492
493 Ok(false)
494 }
495
496 pub fn get_current_plan(&self) -> &Algebra {
498 &self.current_plan
499 }
500
501 pub fn get_rows_processed(&self) -> u64 {
503 self.rows_processed
504 }
505
506 pub fn get_elapsed_time(&self) -> Duration {
508 self.start_time.elapsed()
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use crate::cardinality_estimator::EstimatorConfig;
516 use crate::cost_model::CostModelConfig;
517
518 #[test]
519 fn test_operator_stats() {
520 let mut stats = OperatorStats::new("scan_op".to_string(), 1000, 100.0);
521
522 std::thread::sleep(std::time::Duration::from_millis(10));
524 stats.update(1500);
525
526 let error = stats.estimation_error();
528 assert!(error > 0.0);
529
530 assert!(stats.needs_reoptimization(0.3));
532 }
533
534 #[test]
535 fn test_adaptive_executor() {
536 let estimator_config = EstimatorConfig::default();
537 let estimator = Arc::new(RwLock::new(CardinalityEstimator::new(estimator_config)));
538 let cost_model_config = CostModelConfig::default();
539 let cost_model = Arc::new(RwLock::new(CostModel::new(cost_model_config)));
540 let config = AdaptiveConfig::default();
541
542 let executor = AdaptiveQueryExecutor::new(estimator, cost_model, config);
543
544 executor
546 .start_operator("scan_1".to_string(), 1000, 100.0)
547 .unwrap();
548
549 executor.update_operator("scan_1", 2000).unwrap();
551
552 let stats = executor.get_runtime_stats().unwrap();
554 assert!(stats.operator_stats.contains_key("scan_1"));
555
556 let op_stats = &stats.operator_stats["scan_1"];
557 assert_eq!(op_stats.actual_cardinality, 2000);
558 }
559
560 #[test]
561 fn test_reoptimization_decision() {
562 let decision = ReoptimizationDecision {
563 timestamp_ms: 123456789,
564 trigger_operator: "join_op".to_string(),
565 trigger_error: 0.5,
566 old_cost: 1000.0,
567 new_cost: 800.0,
568 beneficial: true,
569 improvement_pct: 20.0,
570 };
571
572 assert!(decision.beneficial);
573 assert_eq!(decision.improvement_pct, 20.0);
574 }
575
576 #[test]
577 fn test_adaptive_execution_context() {
578 let estimator_config = EstimatorConfig::default();
579 let estimator = Arc::new(RwLock::new(CardinalityEstimator::new(estimator_config)));
580 let cost_model_config = CostModelConfig::default();
581 let cost_model = Arc::new(RwLock::new(CostModel::new(cost_model_config)));
582 let config = AdaptiveConfig {
583 check_interval: 100,
584 ..Default::default()
585 };
586
587 let executor = Arc::new(AdaptiveQueryExecutor::new(estimator, cost_model, config));
588
589 let plan = Algebra::Bgp(vec![]);
591 let mut context = AdaptiveExecutionContext::new(executor.clone(), plan);
592
593 let stats = Statistics::new();
595 let _reopt = context.process_batch(50, &stats).unwrap();
596 let _reopt = context.process_batch(100, &stats).unwrap();
599 assert_eq!(context.get_rows_processed(), 150);
602 }
603}