use std::collections::HashMap;
use parking_lot::RwLock;
use crate::optimizer::cost_based::subquery_unnesting::MAX_BOUNDED_SUBQUERY_ROWS;
const MIN_DECISION_RUNS: u64 = 8;
const APPLY_ROWS_UNNEST_HINT: f64 = MAX_BOUNDED_SUBQUERY_ROWS as f64;
#[derive(Debug, Clone, Default)]
pub struct DecorrelationAdvice {
pub prefer_unnest: bool,
pub prefer_keep: bool,
pub confidence: f64,
pub apply_cost_per_row: Option<f64>,
}
#[derive(Debug, Clone, Default)]
struct SpaceDecisionStats {
apply_runs: u64,
apply_rows: f64,
apply_time_us: f64,
join_runs: u64,
join_rows: f64,
join_time_us: f64,
}
impl SpaceDecisionStats {
fn total_runs(&self) -> u64 {
self.apply_runs + self.join_runs
}
fn apply_cost_per_row(&self) -> Option<f64> {
if self.apply_runs > 0 && self.apply_rows > 0.0 {
Some(self.apply_time_us / self.apply_rows)
} else {
None
}
}
fn join_cost_per_row(&self) -> Option<f64> {
if self.join_runs > 0 && self.join_rows > 0.0 {
Some(self.join_time_us / self.join_rows)
} else {
None
}
}
}
#[derive(Debug, Default)]
pub struct DecisionFeedbackStore {
stats: RwLock<HashMap<String, SpaceDecisionStats>>,
}
impl DecisionFeedbackStore {
pub fn new() -> Self {
Self::default()
}
fn key(space: &str) -> String {
format!("{space}:decorrelation")
}
pub fn record_apply_run(&self, space: &str, rows: u64, time_us: u64) {
let mut stats = self.stats.write();
let entry = stats.entry(Self::key(space)).or_default();
entry.apply_runs += 1;
entry.apply_rows += rows as f64;
entry.apply_time_us += time_us as f64;
}
pub fn record_join_run(&self, space: &str, rows: u64, time_us: u64) {
let mut stats = self.stats.write();
let entry = stats.entry(Self::key(space)).or_default();
entry.join_runs += 1;
entry.join_rows += rows as f64;
entry.join_time_us += time_us as f64;
}
pub fn advice(&self, space: &str) -> DecorrelationAdvice {
let stats = self.stats.read();
let Some(entry) = stats.get(&Self::key(space)) else {
return DecorrelationAdvice::default();
};
let runs = entry.total_runs();
let confidence = {
let x = runs as f64 * 0.1;
1.0 / (1.0 + (-x).exp())
};
let mut advice = DecorrelationAdvice {
confidence,
apply_cost_per_row: entry.apply_cost_per_row(),
..Default::default()
};
if runs >= MIN_DECISION_RUNS {
match (entry.apply_cost_per_row(), entry.join_cost_per_row()) {
(Some(apply_cost), Some(join_cost)) => {
advice.prefer_unnest = join_cost < apply_cost;
advice.prefer_keep = apply_cost <= join_cost;
}
(Some(_), None) => {
let avg_apply_rows = entry.apply_rows / entry.apply_runs as f64;
if avg_apply_rows < APPLY_ROWS_UNNEST_HINT {
advice.prefer_unnest = true;
} else {
advice.prefer_keep = true;
}
}
_ => {}
}
}
advice
}
pub fn invalidate_space(&self, space: Option<&str>) -> usize {
let mut stats = self.stats.write();
match space {
Some(space) => stats.remove(&Self::key(space)).is_some() as usize,
None => {
let removed = stats.len();
stats.clear();
removed
}
}
}
}
impl Clone for DecisionFeedbackStore {
fn clone(&self) -> Self {
Self {
stats: RwLock::new(self.stats.read().clone()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_advice_neutral_without_evidence() {
let store = DecisionFeedbackStore::new();
let advice = store.advice("s");
assert!(!advice.prefer_unnest && !advice.prefer_keep);
assert!(advice.apply_cost_per_row.is_none());
}
#[test]
fn test_advice_prefers_faster_path() {
let store = DecisionFeedbackStore::new();
for _ in 0..10 {
store.record_apply_run("s", 100, 10_000);
}
for _ in 0..10 {
store.record_join_run("s", 100, 1_000);
}
let advice = store.advice("s");
assert!(advice.prefer_unnest);
assert!(!advice.prefer_keep);
assert!(advice.confidence > 0.7);
assert!(advice.apply_cost_per_row.is_some());
}
#[test]
fn test_advice_prefers_keep_when_apply_is_faster() {
let store = DecisionFeedbackStore::new();
for _ in 0..10 {
store.record_apply_run("s", 100, 1_000);
store.record_join_run("s", 100, 10_000);
}
let advice = store.advice("s");
assert!(!advice.prefer_unnest);
assert!(advice.prefer_keep);
}
#[test]
fn test_advice_requires_minimum_runs() {
let store = DecisionFeedbackStore::new();
for _ in 0..3 {
store.record_apply_run("s", 100, 100_000);
store.record_join_run("s", 100, 1_000);
}
let advice = store.advice("s");
assert!(!advice.prefer_unnest && !advice.prefer_keep);
}
#[test]
fn test_apply_only_low_rows_marks_decorrelation_hint() {
let store = DecisionFeedbackStore::new();
for _ in 0..10 {
store.record_apply_run("s", 50, 5_000);
}
let advice = store.advice("s");
assert!(advice.prefer_unnest);
assert!(!advice.prefer_keep);
}
#[test]
fn test_apply_only_large_rows_keeps_apply() {
let store = DecisionFeedbackStore::new();
for _ in 0..10 {
store.record_apply_run("s", 20_000, 5_000);
}
let advice = store.advice("s");
assert!(!advice.prefer_unnest);
assert!(advice.prefer_keep);
}
#[test]
fn test_invalidate_space() {
let store = DecisionFeedbackStore::new();
store.record_apply_run("s", 10, 100);
assert_eq!(store.invalidate_space(Some("s")), 1);
assert!(!store.advice("s").prefer_keep && !store.advice("s").prefer_unnest);
}
}