Skip to main content

oxirs_arq/
adaptive_execution.rs

1//! Adaptive Query Execution
2//!
3//! This module implements adaptive query execution that monitors runtime statistics
4//! and dynamically re-optimizes query plans based on actual execution characteristics.
5
6use 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
16/// Adaptive query executor that re-optimizes plans based on runtime feedback
17pub struct AdaptiveQueryExecutor {
18    /// Runtime statistics collector
19    runtime_stats: Arc<RwLock<RuntimeStatistics>>,
20    /// Cardinality estimator (for future use in learning)
21    #[allow(dead_code)]
22    cardinality_estimator: Arc<RwLock<CardinalityEstimator>>,
23    /// Cost model
24    cost_model: Arc<RwLock<CostModel>>,
25    /// Configuration
26    config: AdaptiveConfig,
27    /// Re-optimization decisions
28    reopt_history: Arc<RwLock<Vec<ReoptimizationDecision>>>,
29}
30
31/// Configuration for adaptive execution
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AdaptiveConfig {
34    /// Enable adaptive execution
35    pub enabled: bool,
36    /// Minimum error threshold to trigger re-optimization (0.0 to 1.0)
37    pub error_threshold: f64,
38    /// Minimum rows processed before re-optimization
39    pub min_rows_threshold: u64,
40    /// Maximum re-optimizations per query
41    pub max_reoptimizations: usize,
42    /// Enable runtime statistics collection
43    pub collect_statistics: bool,
44    /// Re-optimization check interval (number of rows)
45    pub check_interval: u64,
46    /// Enable plan caching after re-optimization
47    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, // 30% estimation error
55            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/// Runtime execution statistics
65#[derive(Debug, Clone, Default)]
66pub struct RuntimeStatistics {
67    /// Operator statistics by operator ID
68    pub operator_stats: HashMap<String, OperatorStats>,
69    /// Global query statistics
70    pub global_stats: GlobalStats,
71    /// Estimation errors
72    pub estimation_errors: Vec<EstimationError>,
73}
74
75/// Statistics for a single operator
76#[derive(Debug, Clone)]
77pub struct OperatorStats {
78    /// Operator identifier
79    pub operator_id: String,
80    /// Estimated cardinality
81    pub estimated_cardinality: u64,
82    /// Actual cardinality observed
83    pub actual_cardinality: u64,
84    /// Estimated cost
85    pub estimated_cost: f64,
86    /// Actual execution time
87    pub actual_time: Duration,
88    /// Number of rows processed
89    pub rows_processed: u64,
90    /// Selectivity observed
91    pub selectivity: f64,
92    /// Start time
93    pub start_time: Instant,
94    /// End time
95    pub end_time: Option<Instant>,
96}
97
98impl OperatorStats {
99    /// Create new operator statistics
100    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    /// Update with actual results
115    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    /// Calculate estimation error
129    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    /// Check if re-optimization is needed
141    pub fn needs_reoptimization(&self, threshold: f64) -> bool {
142        self.estimation_error() > threshold
143    }
144}
145
146/// Global query execution statistics
147#[derive(Debug, Clone, Default)]
148pub struct GlobalStats {
149    /// Total query execution time
150    pub total_time: Duration,
151    /// Total rows produced
152    pub total_rows: u64,
153    /// Number of re-optimizations
154    pub reoptimization_count: usize,
155    /// Average estimation error
156    pub avg_estimation_error: f64,
157    /// Plan cache hits
158    pub plan_cache_hits: u64,
159    /// Plan cache misses
160    pub plan_cache_misses: u64,
161}
162
163/// Record of an estimation error
164#[derive(Debug, Clone)]
165pub struct EstimationError {
166    /// Operator ID
167    pub operator_id: String,
168    /// Estimated value
169    pub estimated: u64,
170    /// Actual value
171    pub actual: u64,
172    /// Relative error
173    pub error: f64,
174    /// Timestamp
175    pub timestamp: Instant,
176}
177
178/// Re-optimization decision record
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ReoptimizationDecision {
181    /// When the decision was made
182    pub timestamp_ms: u128,
183    /// Operator that triggered re-optimization
184    pub trigger_operator: String,
185    /// Estimation error that triggered
186    pub trigger_error: f64,
187    /// Old plan cost
188    pub old_cost: f64,
189    /// New plan cost
190    pub new_cost: f64,
191    /// Was re-optimization beneficial?
192    pub beneficial: bool,
193    /// Cost improvement percentage
194    pub improvement_pct: f64,
195}
196
197impl AdaptiveQueryExecutor {
198    /// Create a new adaptive query executor
199    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    /// Start monitoring an operator
214    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    /// Update operator with actual results
237    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        // Record estimation error if needed
256        if needs_error_recording {
257            // Extract data before pushing to avoid borrow conflict
258            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        // Update cardinality estimator with actual results
278        // (This would call the cardinality estimator's learning methods)
279
280        Ok(())
281    }
282
283    /// Check if re-optimization should be triggered
284    pub fn should_reoptimize(&self, rows_processed: u64) -> Result<bool> {
285        if !self.config.enabled {
286            return Ok(false);
287        }
288
289        // Check minimum threshold
290        if rows_processed < self.config.min_rows_threshold {
291            return Ok(false);
292        }
293
294        // Check max re-optimizations
295        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        // Check for significant estimation errors
308        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    /// Re-optimize query plan based on runtime statistics
322    pub fn reoptimize_plan(
323        &self,
324        current_plan: &Algebra,
325        _statistics: &Statistics,
326    ) -> Result<(Algebra, ReoptimizationDecision)> {
327        // Get runtime statistics
328        let runtime_stats = self
329            .runtime_stats
330            .read()
331            .map_err(|e| anyhow!("Failed to acquire runtime stats lock: {}", e))?;
332
333        // Find operator with largest estimation error
334        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        // Calculate current plan cost
348        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        // Generate new plan (simplified - would use full optimizer)
358        let new_plan = current_plan.clone(); // Placeholder: real implementation would re-optimize
359        let new_cost_f64 = old_cost_f64 * 0.9; // Placeholder: assume 10% improvement
360
361        // Create re-optimization decision
362        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        // Record decision
379        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        // Update global stats
386        // Update global stats in a new scope
387        {
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    /// Get runtime statistics
399    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    /// Get re-optimization history
408    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    /// Reset statistics
417    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    /// Get configuration
434    pub fn get_config(&self) -> &AdaptiveConfig {
435        &self.config
436    }
437
438    /// Update configuration
439    pub fn update_config(&mut self, config: AdaptiveConfig) {
440        self.config = config;
441    }
442}
443
444/// Adaptive execution context for a single query
445pub struct AdaptiveExecutionContext {
446    /// Parent executor
447    executor: Arc<AdaptiveQueryExecutor>,
448    /// Query start time
449    start_time: Instant,
450    /// Rows processed so far
451    rows_processed: u64,
452    /// Last re-optimization check
453    last_check: u64,
454    /// Current plan
455    current_plan: Algebra,
456}
457
458impl AdaptiveExecutionContext {
459    /// Create a new adaptive execution context
460    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    /// Process a batch of rows and check for re-optimization
471    pub fn process_batch(&mut self, batch_size: u64, statistics: &Statistics) -> Result<bool> {
472        self.rows_processed += batch_size;
473
474        // Check if we should re-optimize
475        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); // Indicate that re-optimization occurred
489                }
490            }
491        }
492
493        Ok(false)
494    }
495
496    /// Get current plan
497    pub fn get_current_plan(&self) -> &Algebra {
498        &self.current_plan
499    }
500
501    /// Get rows processed
502    pub fn get_rows_processed(&self) -> u64 {
503        self.rows_processed
504    }
505
506    /// Get elapsed time
507    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        // Simulate execution
523        std::thread::sleep(std::time::Duration::from_millis(10));
524        stats.update(1500);
525
526        // Check estimation error
527        let error = stats.estimation_error();
528        assert!(error > 0.0);
529
530        // Check if re-optimization is needed
531        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        // Start monitoring an operator
545        executor
546            .start_operator("scan_1".to_string(), 1000, 100.0)
547            .unwrap();
548
549        // Update with actual results
550        executor.update_operator("scan_1", 2000).unwrap();
551
552        // Get runtime stats
553        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        // Create dummy plan
590        let plan = Algebra::Bgp(vec![]);
591        let mut context = AdaptiveExecutionContext::new(executor.clone(), plan);
592
593        // Process batches
594        let stats = Statistics::new();
595        let _reopt = context.process_batch(50, &stats).unwrap();
596        // Not enough rows yet
597
598        let _reopt = context.process_batch(100, &stats).unwrap();
599        // May or may not re-optimize depending on stats
600
601        assert_eq!(context.get_rows_processed(), 150);
602    }
603}