linkrs_query/optimizer/cost_based/
topn_optimization.rs1use std::sync::Arc;
34
35use crate::optimizer::cost::CostCalculator;
36use crate::planning::plan::core::nodes::{SortItem, SortNode};
37
38#[derive(Debug, Clone, PartialEq)]
40pub enum SortEliminationDecision {
41 KeepSort {
43 reason: SortKeepReason,
45 estimated_cost: f64,
47 },
48 ConvertToTopN {
50 reason: TopNConversionReason,
52 topn_cost: f64,
54 original_cost: f64,
56 },
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub enum SortKeepReason {
62 NoLimitForTopN,
64 LimitTooSmall,
66 CostBasedDecision,
68}
69
70#[derive(Debug, Clone, PartialEq)]
72pub enum TopNConversionReason {
73 SortWithLimit,
75 SmallLimit,
77 CostBased,
79 MemoryConstrained,
81}
82
83#[derive(Debug, Clone)]
85pub struct SortContext {
86 pub sort_node: SortNode,
88 pub input_rows: u64,
90 pub has_limit_child: bool,
92 pub limit_value: Option<i64>,
94}
95
96impl SortContext {
97 pub fn new(sort_node: SortNode, input_rows: u64) -> Self {
99 Self {
100 sort_node,
101 input_rows,
102 has_limit_child: false,
103 limit_value: None,
104 }
105 }
106
107 pub fn with_limit(mut self, limit: i64) -> Self {
109 self.has_limit_child = true;
110 self.limit_value = Some(limit);
111 self
112 }
113}
114
115#[derive(Debug)]
121pub struct SortEliminationOptimizer {
122 cost_calculator: Arc<CostCalculator>,
123 topn_threshold: f64,
125 min_limit_for_topn: i64,
127}
128
129impl SortEliminationOptimizer {
130 pub fn new(cost_calculator: Arc<CostCalculator>) -> Self {
132 let thresholds = cost_calculator.config().strategy_thresholds;
133 Self {
134 cost_calculator,
135 topn_threshold: thresholds.topn_threshold,
136 min_limit_for_topn: thresholds.topn_default_limit as i64,
137 }
138 }
139
140 pub fn with_topn_threshold(mut self, threshold: f64) -> Self {
142 self.topn_threshold = threshold.clamp(0.001, 1.0);
143 self
144 }
145
146 pub fn with_min_limit(mut self, min_limit: i64) -> Self {
148 self.min_limit_for_topn = min_limit.max(1);
149 self
150 }
151
152 pub fn optimize(&self, context: &SortContext) -> SortEliminationDecision {
162 self.optimize_with_memory(context, None)
163 }
164
165 pub fn optimize_with_memory(
177 &self,
178 context: &SortContext,
179 available_memory: Option<usize>,
180 ) -> SortEliminationDecision {
181 let sort_items = context.sort_node.sort_items();
182
183 if let Some(memory) = available_memory {
185 let sort_memory = self
186 .cost_calculator
187 .estimate_sort_memory(context.input_rows, sort_items.len());
188
189 if sort_memory > memory {
191 if let Some(limit) = context.limit_value {
192 if limit >= self.min_limit_for_topn {
193 let original_cost =
194 self.calculate_sort_cost(context.input_rows, sort_items.len());
195 let topn_cost = self
196 .cost_calculator
197 .calculate_topn_cost(context.input_rows, limit);
198
199 return SortEliminationDecision::ConvertToTopN {
200 reason: TopNConversionReason::MemoryConstrained,
201 topn_cost,
202 original_cost,
203 };
204 }
205 }
206
207 let sort_cost = self.calculate_sort_cost(context.input_rows, sort_items.len());
209 return SortEliminationDecision::KeepSort {
210 reason: SortKeepReason::CostBasedDecision,
211 estimated_cost: sort_cost,
212 };
213 }
214 }
215
216 if let Some(decision) = self.check_topn_conversion(context, sort_items) {
218 return decision;
219 }
220
221 let sort_cost = self.calculate_sort_cost(context.input_rows, sort_items.len());
223 SortEliminationDecision::KeepSort {
224 reason: SortKeepReason::NoLimitForTopN,
225 estimated_cost: sort_cost,
226 }
227 }
228
229 fn check_topn_conversion(
234 &self,
235 context: &SortContext,
236 sort_items: &[SortItem],
237 ) -> Option<SortEliminationDecision> {
238 let limit = context.limit_value?;
239
240 if limit < self.min_limit_for_topn {
241 return None;
242 }
243
244 let limit_ratio = limit as f64 / context.input_rows as f64;
247
248 if limit_ratio < self.topn_threshold
249 || context.input_rows > self.cost_calculator.config().memory_sort_threshold
250 {
251 let original_cost = self.calculate_sort_cost(context.input_rows, sort_items.len());
252 let topn_cost = self
253 .cost_calculator
254 .calculate_topn_cost(context.input_rows, limit);
255
256 if topn_cost < original_cost {
257 return Some(SortEliminationDecision::ConvertToTopN {
258 reason: if context.has_limit_child {
259 TopNConversionReason::SortWithLimit
260 } else {
261 TopNConversionReason::CostBased
262 },
263 topn_cost,
264 original_cost,
265 });
266 }
267 }
268
269 None
270 }
271
272 fn calculate_sort_cost(&self, input_rows: u64, sort_columns: usize) -> f64 {
274 self.cost_calculator
275 .calculate_sort_cost(input_rows, sort_columns, None)
276 }
277
278 pub fn check_topn_conversion_cost(
288 &self,
289 sort_items: &[SortItem],
290 limit: i64,
291 input_rows: u64,
292 ) -> Option<(f64, f64)> {
293 if limit < self.min_limit_for_topn {
294 return None;
295 }
296
297 let limit_ratio = limit as f64 / input_rows as f64;
298
299 if limit_ratio < self.topn_threshold
300 || input_rows > self.cost_calculator.config().memory_sort_threshold
301 {
302 let original_cost = self.calculate_sort_cost(input_rows, sort_items.len());
303 let topn_cost = self.cost_calculator.calculate_topn_cost(input_rows, limit);
304
305 if topn_cost < original_cost {
306 return Some((topn_cost, original_cost));
307 }
308 }
309
310 None
311 }
312
313 pub fn get_optimization_advice(&self, context: &SortContext) -> Vec<String> {
317 let mut advice = Vec::new();
318
319 match self.optimize(context) {
320 SortEliminationDecision::ConvertToTopN {
321 reason,
322 topn_cost,
323 original_cost,
324 } => {
325 let savings = original_cost - topn_cost;
326 advice.push(format!(
327 "It is recommended to convert Sort + Limit to TopN, reason: {:?} and expected cost savings: {:.2}",
328 reason, savings
329 ));
330 }
331 SortEliminationDecision::KeepSort { reason, .. } => {
332 advice.push(format!("Preserve the sort operation, cause: {:?}", reason));
333
334 if matches!(reason, SortKeepReason::NoLimitForTopN) {
335 advice.push(
336 "If the query contains a LIMIT, consider converting Sort + Limit to TopN"
337 .to_string(),
338 );
339 }
340 }
341 }
342
343 advice
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::optimizer::stats::StatisticsManager;
351
352 fn create_test_optimizer() -> SortEliminationOptimizer {
353 let stats_manager = Arc::new(StatisticsManager::new());
354 let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
355 SortEliminationOptimizer::new(cost_calculator)
356 }
357
358 #[test]
359 fn test_sort_elimination_optimizer_creation() {
360 let optimizer = create_test_optimizer();
361 assert_eq!(optimizer.topn_threshold, 0.1);
363 assert_eq!(optimizer.min_limit_for_topn, 100); }
365
366 #[test]
367 fn test_with_topn_threshold() {
368 let stats_manager = Arc::new(StatisticsManager::new());
369 let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
370 let optimizer = SortEliminationOptimizer::new(cost_calculator).with_topn_threshold(0.2);
371
372 assert_eq!(optimizer.topn_threshold, 0.2);
373 }
374
375 #[test]
376 fn test_with_min_limit() {
377 let stats_manager = Arc::new(StatisticsManager::new());
378 let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
379 let optimizer = SortEliminationOptimizer::new(cost_calculator).with_min_limit(10);
380
381 assert_eq!(optimizer.min_limit_for_topn, 10);
382 }
383
384 #[test]
385 fn test_threshold_clamping() {
386 let stats_manager = Arc::new(StatisticsManager::new());
387 let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
388
389 let optimizer1 =
390 SortEliminationOptimizer::new(cost_calculator.clone()).with_topn_threshold(2.0); assert_eq!(optimizer1.topn_threshold, 1.0);
392
393 let optimizer2 = SortEliminationOptimizer::new(cost_calculator).with_topn_threshold(0.0001); assert_eq!(optimizer2.topn_threshold, 0.001);
395 }
396
397 #[test]
398 fn test_context_creation() {
399 let start_node = crate::planning::plan::core::nodes::StartNode::new();
400 let sort_node = SortNode::new(
401 crate::planning::plan::PlanNodeEnum::Start(start_node),
402 vec![SortItem::column_asc("name".to_string())],
403 )
404 .expect("Failed to create SortNode");
405
406 let context = SortContext::new(sort_node, 1000);
407 assert_eq!(context.input_rows, 1000);
408 assert!(!context.has_limit_child);
409 assert_eq!(context.limit_value, None);
410 }
411
412 #[test]
413 fn test_context_with_limit() {
414 let start_node = crate::planning::plan::core::nodes::StartNode::new();
415 let sort_node = SortNode::new(
416 crate::planning::plan::PlanNodeEnum::Start(start_node),
417 vec![SortItem::column_asc("name".to_string())],
418 )
419 .expect("Failed to create SortNode");
420
421 let context = SortContext::new(sort_node, 1000).with_limit(10);
422
423 assert!(context.has_limit_child);
424 assert_eq!(context.limit_value, Some(10));
425 }
426
427 #[test]
428 fn test_check_topn_conversion_cost_limit_too_small() {
429 let optimizer = create_test_optimizer();
430 let sort_items = vec![SortItem::column_asc("name".to_string())];
431
432 let result = optimizer.check_topn_conversion_cost(&sort_items, 0, 1000);
433 assert_eq!(result, None);
434 }
435
436 #[test]
437 fn test_sort_keep_reason_display() {
438 let reason = SortKeepReason::NoLimitForTopN;
439 assert!(format!("{:?}", reason).contains("NoLimitForTopN"));
440
441 let reason = SortKeepReason::LimitTooSmall;
442 assert!(format!("{:?}", reason).contains("LimitTooSmall"));
443
444 let reason = SortKeepReason::CostBasedDecision;
445 assert!(format!("{:?}", reason).contains("CostBasedDecision"));
446 }
447
448 #[test]
449 fn test_topn_conversion_reason_display() {
450 let reason = TopNConversionReason::SortWithLimit;
451 assert!(format!("{:?}", reason).contains("SortWithLimit"));
452
453 let reason = TopNConversionReason::SmallLimit;
454 assert!(format!("{:?}", reason).contains("SmallLimit"));
455
456 let reason = TopNConversionReason::CostBased;
457 assert!(format!("{:?}", reason).contains("CostBased"));
458
459 let reason = TopNConversionReason::MemoryConstrained;
460 assert!(format!("{:?}", reason).contains("MemoryConstrained"));
461 }
462
463 #[test]
464 fn test_optimize_with_memory() {
465 let optimizer = create_test_optimizer();
466
467 let start_node = crate::planning::plan::core::nodes::StartNode::new();
469 let sort_node = SortNode::new(
470 crate::planning::plan::PlanNodeEnum::Start(start_node),
471 vec![SortItem::column_asc("name".to_string())],
472 )
473 .expect("Failed to create SortNode");
474
475 let context = SortContext::new(sort_node, 10000).with_limit(100);
476
477 let decision_with_memory = optimizer.optimize_with_memory(&context, Some(10 * 1024 * 1024)); assert!(matches!(
480 decision_with_memory,
481 SortEliminationDecision::ConvertToTopN { .. }
482 | SortEliminationDecision::KeepSort { .. }
483 ));
484
485 let decision_limited = optimizer.optimize_with_memory(&context, Some(1024)); match decision_limited {
489 SortEliminationDecision::ConvertToTopN { reason, .. } => {
490 assert_eq!(reason, TopNConversionReason::MemoryConstrained);
491 }
492 SortEliminationDecision::KeepSort { .. } => {
493 }
495 }
496 }
497
498 #[test]
499 fn test_optimize_with_memory_no_limit() {
500 let optimizer = create_test_optimizer();
501
502 let start_node = crate::planning::plan::core::nodes::StartNode::new();
504 let sort_node = SortNode::new(
505 crate::planning::plan::PlanNodeEnum::Start(start_node),
506 vec![SortItem::column_asc("name".to_string())],
507 )
508 .expect("Failed to create SortNode");
509
510 let context = SortContext::new(sort_node, 1000); let decision = optimizer.optimize_with_memory(&context, Some(1024));
514 match decision {
515 SortEliminationDecision::KeepSort { .. } => {
516 }
518 _ => {
519 }
521 }
522 }
523}