use std::time::Instant;
#[derive(Debug, Clone)]
pub struct OperatorFeedback {
pub operator_id: String,
pub operator_type: String,
pub estimated_rows: u64,
pub actual_rows: u64,
pub actual_input_rows: Option<u64>,
pub partition_count: u32,
pub exhausted: bool,
pub estimated_time_us: u64,
pub actual_time_us: u64,
pub execution_loops: u64,
pub condition_key: Option<String>,
pub shape_key: Option<String>,
}
impl OperatorFeedback {
pub fn new(
operator_id: String,
operator_type: String,
estimated_rows: u64,
actual_rows: u64,
) -> Self {
Self {
operator_id,
operator_type,
estimated_rows,
actual_rows,
estimated_time_us: 0,
actual_time_us: 0,
execution_loops: 1,
actual_input_rows: None,
partition_count: 1,
exhausted: true,
condition_key: None,
shape_key: None,
}
}
pub fn row_estimation_error(&self) -> f64 {
if self.estimated_rows == 0 {
return 1.0;
}
let estimated = self.estimated_rows as f64;
let actual = self.actual_rows as f64;
((actual - estimated).abs() / estimated).min(10.0)
}
pub fn time_estimation_error(&self) -> f64 {
if self.estimated_time_us == 0 {
return 1.0;
}
let estimated = self.estimated_time_us as f64;
let actual = self.actual_time_us as f64;
((actual - estimated).abs() / estimated).min(10.0)
}
pub fn avg_rows_per_loop(&self) -> f64 {
if self.execution_loops == 0 {
return 0.0;
}
self.actual_rows as f64 / self.execution_loops as f64
}
pub fn avg_time_us_per_loop(&self) -> f64 {
if self.execution_loops == 0 {
return 0.0;
}
self.actual_time_us as f64 / self.execution_loops as f64
}
}
#[derive(Debug, Clone)]
pub struct QueryExecutionFeedback {
pub query_fingerprint: String,
pub sequence: u64,
pub space: Option<String>,
pub estimated_rows: u64,
pub actual_rows: u64,
pub estimated_time_us: u64,
pub actual_time_us: u64,
pub execution_timestamp: Instant,
pub apply_rows: u64,
pub join_rows: u64,
pub apply_time_us: u64,
pub join_time_us: u64,
pub operator_feedbacks: Vec<OperatorFeedback>,
}
impl QueryExecutionFeedback {
pub fn new(query_fingerprint: String) -> Self {
Self {
query_fingerprint,
sequence: 0,
space: None,
estimated_rows: 0,
actual_rows: 0,
estimated_time_us: 0,
actual_time_us: 0,
execution_timestamp: Instant::now(),
apply_rows: 0,
join_rows: 0,
apply_time_us: 0,
join_time_us: 0,
operator_feedbacks: Vec::new(),
}
}
pub fn row_estimation_error(&self) -> f64 {
if self.estimated_rows == 0 {
return 1.0;
}
let estimated = self.estimated_rows as f64;
let actual = self.actual_rows as f64;
((actual - estimated).abs() / estimated).min(10.0)
}
pub fn time_estimation_error(&self) -> f64 {
if self.estimated_time_us == 0 {
return 1.0;
}
let estimated = self.estimated_time_us as f64;
let actual = self.actual_time_us as f64;
((actual - estimated).abs() / estimated).min(10.0)
}
pub fn add_operator_feedback(&mut self, feedback: OperatorFeedback) {
self.operator_feedbacks.push(feedback);
}
pub fn operator_feedback_count(&self) -> usize {
self.operator_feedbacks.len()
}
pub fn get_operator_feedback(&self, operator_id: &str) -> Option<&OperatorFeedback> {
self.operator_feedbacks
.iter()
.find(|f| f.operator_id == operator_id)
}
pub fn avg_operator_row_error(&self) -> f64 {
if self.operator_feedbacks.is_empty() {
return 0.0;
}
let total_error: f64 = self
.operator_feedbacks
.iter()
.map(|f| f.row_estimation_error())
.sum();
total_error / self.operator_feedbacks.len() as f64
}
pub fn avg_operator_time_error(&self) -> f64 {
if self.operator_feedbacks.is_empty() {
return 0.0;
}
let total_error: f64 = self
.operator_feedbacks
.iter()
.map(|f| f.time_estimation_error())
.sum();
total_error / self.operator_feedbacks.len() as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_operator_feedback() {
let feedback = OperatorFeedback {
operator_id: "scan_1".to_string(),
operator_type: "IndexScan".to_string(),
estimated_rows: 100,
actual_rows: 150,
estimated_time_us: 1000,
actual_time_us: 1200,
execution_loops: 1,
actual_input_rows: None,
partition_count: 1,
exhausted: true,
condition_key: None,
shape_key: None,
};
assert_eq!(feedback.row_estimation_error(), 0.5); assert_eq!(feedback.time_estimation_error(), 0.2); }
#[test]
fn test_operator_feedback_loops() {
let feedback = OperatorFeedback {
operator_id: "nested_loop_inner".to_string(),
operator_type: "IndexScan".to_string(),
estimated_rows: 100,
actual_rows: 500, estimated_time_us: 1000,
actual_time_us: 5000,
execution_loops: 10,
actual_input_rows: None,
partition_count: 1,
exhausted: true,
condition_key: None,
shape_key: None,
};
assert_eq!(feedback.avg_rows_per_loop(), 50.0); assert_eq!(feedback.avg_time_us_per_loop(), 500.0); }
#[test]
fn test_query_execution_feedback() {
let mut feedback = QueryExecutionFeedback::new("fp_123".to_string());
feedback.estimated_rows = 1000;
feedback.actual_rows = 1200;
feedback.estimated_time_us = 5000;
feedback.actual_time_us = 6000;
let op_feedback = OperatorFeedback::new("scan_1".into(), "SeqScan".to_string(), 1000, 1200);
feedback.add_operator_feedback(op_feedback);
assert_eq!(feedback.operator_feedback_count(), 1);
assert!(feedback.row_estimation_error() > 0.0);
assert!(feedback.time_estimation_error() > 0.0);
}
#[test]
fn test_avg_operator_errors() {
let mut feedback = QueryExecutionFeedback::new("fp_123".to_string());
feedback.add_operator_feedback(OperatorFeedback {
operator_id: "op1".to_string(),
operator_type: "Scan".to_string(),
estimated_rows: 100,
actual_rows: 110,
estimated_time_us: 1000,
actual_time_us: 1100,
execution_loops: 1,
actual_input_rows: None,
partition_count: 1,
exhausted: true,
condition_key: None,
shape_key: None,
});
feedback.add_operator_feedback(OperatorFeedback {
operator_id: "op2".to_string(),
operator_type: "Filter".to_string(),
estimated_rows: 100,
actual_rows: 90,
estimated_time_us: 500,
actual_time_us: 450,
execution_loops: 1,
actual_input_rows: None,
partition_count: 1,
exhausted: true,
condition_key: None,
shape_key: None,
});
let avg_row_error = feedback.avg_operator_row_error();
let avg_time_error = feedback.avg_operator_time_error();
assert!(avg_row_error > 0.0);
assert!(avg_time_error > 0.0);
}
}