use std::sync::Arc;
use crate::optimizer::cost::CostCalculator;
use crate::planning::plan::core::nodes::{SortItem, SortNode};
#[derive(Debug, Clone, PartialEq)]
pub enum SortEliminationDecision {
KeepSort {
reason: SortKeepReason,
estimated_cost: f64,
},
ConvertToTopN {
reason: TopNConversionReason,
topn_cost: f64,
original_cost: f64,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum SortKeepReason {
NoLimitForTopN,
LimitTooSmall,
CostBasedDecision,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TopNConversionReason {
SortWithLimit,
SmallLimit,
CostBased,
MemoryConstrained,
}
#[derive(Debug, Clone)]
pub struct SortContext {
pub sort_node: SortNode,
pub input_rows: u64,
pub has_limit_child: bool,
pub limit_value: Option<i64>,
}
impl SortContext {
pub fn new(sort_node: SortNode, input_rows: u64) -> Self {
Self {
sort_node,
input_rows,
has_limit_child: false,
limit_value: None,
}
}
pub fn with_limit(mut self, limit: i64) -> Self {
self.has_limit_child = true;
self.limit_value = Some(limit);
self
}
}
#[derive(Debug)]
pub struct SortEliminationOptimizer {
cost_calculator: Arc<CostCalculator>,
topn_threshold: f64,
min_limit_for_topn: i64,
}
impl SortEliminationOptimizer {
pub fn new(cost_calculator: Arc<CostCalculator>) -> Self {
let thresholds = cost_calculator.config().strategy_thresholds;
Self {
cost_calculator,
topn_threshold: thresholds.topn_threshold,
min_limit_for_topn: thresholds.topn_default_limit as i64,
}
}
pub fn with_topn_threshold(mut self, threshold: f64) -> Self {
self.topn_threshold = threshold.clamp(0.001, 1.0);
self
}
pub fn with_min_limit(mut self, min_limit: i64) -> Self {
self.min_limit_for_topn = min_limit.max(1);
self
}
pub fn optimize(&self, context: &SortContext) -> SortEliminationDecision {
self.optimize_with_memory(context, None)
}
pub fn optimize_with_memory(
&self,
context: &SortContext,
available_memory: Option<usize>,
) -> SortEliminationDecision {
let sort_items = context.sort_node.sort_items();
if let Some(memory) = available_memory {
let sort_memory = self
.cost_calculator
.estimate_sort_memory(context.input_rows, sort_items.len());
if sort_memory > memory {
if let Some(limit) = context.limit_value {
if limit >= self.min_limit_for_topn {
let original_cost =
self.calculate_sort_cost(context.input_rows, sort_items.len());
let topn_cost = self
.cost_calculator
.calculate_topn_cost(context.input_rows, limit);
return SortEliminationDecision::ConvertToTopN {
reason: TopNConversionReason::MemoryConstrained,
topn_cost,
original_cost,
};
}
}
let sort_cost = self.calculate_sort_cost(context.input_rows, sort_items.len());
return SortEliminationDecision::KeepSort {
reason: SortKeepReason::CostBasedDecision,
estimated_cost: sort_cost,
};
}
}
if let Some(decision) = self.check_topn_conversion(context, sort_items) {
return decision;
}
let sort_cost = self.calculate_sort_cost(context.input_rows, sort_items.len());
SortEliminationDecision::KeepSort {
reason: SortKeepReason::NoLimitForTopN,
estimated_cost: sort_cost,
}
}
fn check_topn_conversion(
&self,
context: &SortContext,
sort_items: &[SortItem],
) -> Option<SortEliminationDecision> {
let limit = context.limit_value?;
if limit < self.min_limit_for_topn {
return None;
}
let limit_ratio = limit as f64 / context.input_rows as f64;
if limit_ratio < self.topn_threshold
|| context.input_rows > self.cost_calculator.config().memory_sort_threshold
{
let original_cost = self.calculate_sort_cost(context.input_rows, sort_items.len());
let topn_cost = self
.cost_calculator
.calculate_topn_cost(context.input_rows, limit);
if topn_cost < original_cost {
return Some(SortEliminationDecision::ConvertToTopN {
reason: if context.has_limit_child {
TopNConversionReason::SortWithLimit
} else {
TopNConversionReason::CostBased
},
topn_cost,
original_cost,
});
}
}
None
}
fn calculate_sort_cost(&self, input_rows: u64, sort_columns: usize) -> f64 {
self.cost_calculator
.calculate_sort_cost(input_rows, sort_columns, None)
}
pub fn check_topn_conversion_cost(
&self,
sort_items: &[SortItem],
limit: i64,
input_rows: u64,
) -> Option<(f64, f64)> {
if limit < self.min_limit_for_topn {
return None;
}
let limit_ratio = limit as f64 / input_rows as f64;
if limit_ratio < self.topn_threshold
|| input_rows > self.cost_calculator.config().memory_sort_threshold
{
let original_cost = self.calculate_sort_cost(input_rows, sort_items.len());
let topn_cost = self.cost_calculator.calculate_topn_cost(input_rows, limit);
if topn_cost < original_cost {
return Some((topn_cost, original_cost));
}
}
None
}
pub fn get_optimization_advice(&self, context: &SortContext) -> Vec<String> {
let mut advice = Vec::new();
match self.optimize(context) {
SortEliminationDecision::ConvertToTopN {
reason,
topn_cost,
original_cost,
} => {
let savings = original_cost - topn_cost;
advice.push(format!(
"It is recommended to convert Sort + Limit to TopN, reason: {:?} and expected cost savings: {:.2}",
reason, savings
));
}
SortEliminationDecision::KeepSort { reason, .. } => {
advice.push(format!("Preserve the sort operation, cause: {:?}", reason));
if matches!(reason, SortKeepReason::NoLimitForTopN) {
advice.push(
"If the query contains a LIMIT, consider converting Sort + Limit to TopN"
.to_string(),
);
}
}
}
advice
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::optimizer::stats::StatisticsManager;
fn create_test_optimizer() -> SortEliminationOptimizer {
let stats_manager = Arc::new(StatisticsManager::new());
let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
SortEliminationOptimizer::new(cost_calculator)
}
#[test]
fn test_sort_elimination_optimizer_creation() {
let optimizer = create_test_optimizer();
assert_eq!(optimizer.topn_threshold, 0.1);
assert_eq!(optimizer.min_limit_for_topn, 100); }
#[test]
fn test_with_topn_threshold() {
let stats_manager = Arc::new(StatisticsManager::new());
let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
let optimizer = SortEliminationOptimizer::new(cost_calculator).with_topn_threshold(0.2);
assert_eq!(optimizer.topn_threshold, 0.2);
}
#[test]
fn test_with_min_limit() {
let stats_manager = Arc::new(StatisticsManager::new());
let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
let optimizer = SortEliminationOptimizer::new(cost_calculator).with_min_limit(10);
assert_eq!(optimizer.min_limit_for_topn, 10);
}
#[test]
fn test_threshold_clamping() {
let stats_manager = Arc::new(StatisticsManager::new());
let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
let optimizer1 =
SortEliminationOptimizer::new(cost_calculator.clone()).with_topn_threshold(2.0); assert_eq!(optimizer1.topn_threshold, 1.0);
let optimizer2 = SortEliminationOptimizer::new(cost_calculator).with_topn_threshold(0.0001); assert_eq!(optimizer2.topn_threshold, 0.001);
}
#[test]
fn test_context_creation() {
let start_node = crate::planning::plan::core::nodes::StartNode::new();
let sort_node = SortNode::new(
crate::planning::plan::PlanNodeEnum::Start(start_node),
vec![SortItem::column_asc("name".to_string())],
)
.expect("Failed to create SortNode");
let context = SortContext::new(sort_node, 1000);
assert_eq!(context.input_rows, 1000);
assert!(!context.has_limit_child);
assert_eq!(context.limit_value, None);
}
#[test]
fn test_context_with_limit() {
let start_node = crate::planning::plan::core::nodes::StartNode::new();
let sort_node = SortNode::new(
crate::planning::plan::PlanNodeEnum::Start(start_node),
vec![SortItem::column_asc("name".to_string())],
)
.expect("Failed to create SortNode");
let context = SortContext::new(sort_node, 1000).with_limit(10);
assert!(context.has_limit_child);
assert_eq!(context.limit_value, Some(10));
}
#[test]
fn test_check_topn_conversion_cost_limit_too_small() {
let optimizer = create_test_optimizer();
let sort_items = vec![SortItem::column_asc("name".to_string())];
let result = optimizer.check_topn_conversion_cost(&sort_items, 0, 1000);
assert_eq!(result, None);
}
#[test]
fn test_sort_keep_reason_display() {
let reason = SortKeepReason::NoLimitForTopN;
assert!(format!("{:?}", reason).contains("NoLimitForTopN"));
let reason = SortKeepReason::LimitTooSmall;
assert!(format!("{:?}", reason).contains("LimitTooSmall"));
let reason = SortKeepReason::CostBasedDecision;
assert!(format!("{:?}", reason).contains("CostBasedDecision"));
}
#[test]
fn test_topn_conversion_reason_display() {
let reason = TopNConversionReason::SortWithLimit;
assert!(format!("{:?}", reason).contains("SortWithLimit"));
let reason = TopNConversionReason::SmallLimit;
assert!(format!("{:?}", reason).contains("SmallLimit"));
let reason = TopNConversionReason::CostBased;
assert!(format!("{:?}", reason).contains("CostBased"));
let reason = TopNConversionReason::MemoryConstrained;
assert!(format!("{:?}", reason).contains("MemoryConstrained"));
}
#[test]
fn test_optimize_with_memory() {
let optimizer = create_test_optimizer();
let start_node = crate::planning::plan::core::nodes::StartNode::new();
let sort_node = SortNode::new(
crate::planning::plan::PlanNodeEnum::Start(start_node),
vec![SortItem::column_asc("name".to_string())],
)
.expect("Failed to create SortNode");
let context = SortContext::new(sort_node, 10000).with_limit(100);
let decision_with_memory = optimizer.optimize_with_memory(&context, Some(10 * 1024 * 1024)); assert!(matches!(
decision_with_memory,
SortEliminationDecision::ConvertToTopN { .. }
| SortEliminationDecision::KeepSort { .. }
));
let decision_limited = optimizer.optimize_with_memory(&context, Some(1024)); match decision_limited {
SortEliminationDecision::ConvertToTopN { reason, .. } => {
assert_eq!(reason, TopNConversionReason::MemoryConstrained);
}
SortEliminationDecision::KeepSort { .. } => {
}
}
}
#[test]
fn test_optimize_with_memory_no_limit() {
let optimizer = create_test_optimizer();
let start_node = crate::planning::plan::core::nodes::StartNode::new();
let sort_node = SortNode::new(
crate::planning::plan::PlanNodeEnum::Start(start_node),
vec![SortItem::column_asc("name".to_string())],
)
.expect("Failed to create SortNode");
let context = SortContext::new(sort_node, 1000);
let decision = optimizer.optimize_with_memory(&context, Some(1024));
match decision {
SortEliminationDecision::KeepSort { .. } => {
}
_ => {
}
}
}
}