1use super::ast::*;
16use crate::database::MoteDB;
17use crate::types::{TableSchema, Value};
18use crate::Result;
19use dashmap::DashMap;
20use std::sync::Arc;
21
22#[derive(Debug, Clone)]
24pub struct QueryPlan {
25 pub scan_method: ScanMethod,
27 pub estimated_cost: f64,
29 pub estimated_rows: usize,
31 pub post_filters: Vec<Expr>,
33}
34
35#[derive(Debug, Clone)]
37pub enum ScanMethod {
38 FullScan { table: String },
40
41 PointQuery {
43 table: String,
44 column: String,
45 value: Value,
46 },
47
48 RangeQuery {
54 table: String,
55 column: String,
56 start: Value,
57 start_inclusive: bool,
58 end: Value,
59 end_inclusive: bool,
60 },
61
62 TextSearch {
64 table: String,
65 column: String,
66 query: String,
67 },
68
69 VectorSearch {
71 table: String,
72 column: String,
73 query_vector: crate::types::ArcVec,
74 k: usize,
75 },
76
77 SpatialRange {
79 table: String,
80 column: String,
81 min_x: f64,
82 min_y: f64,
83 max_x: f64,
84 max_y: f64,
85 },
86
87 PrimaryKeyScan {
98 table: String,
99 ascending: bool,
100 limit: Option<usize>,
101 },
102
103 IndexIntersection {
107 table: String,
108 column1: String,
109 value1: Value,
110 column2: String,
111 value2: Value,
112 },
113}
114
115impl ScanMethod {
116 pub fn table_name(&self) -> &str {
117 match self {
118 ScanMethod::FullScan { table }
119 | ScanMethod::PointQuery { table, .. }
120 | ScanMethod::RangeQuery { table, .. }
121 | ScanMethod::TextSearch { table, .. }
122 | ScanMethod::VectorSearch { table, .. }
123 | ScanMethod::SpatialRange { table, .. }
124 | ScanMethod::PrimaryKeyScan { table, .. }
125 | ScanMethod::IndexIntersection { table, .. } => table,
126 }
127 }
128}
129
130#[derive(Debug, Clone)]
132pub struct IndexStats {
133 pub cardinality: usize,
135 pub total_rows: usize,
137 pub size_bytes: usize,
139 pub is_unique: bool,
141}
142
143impl IndexStats {
144 pub fn selectivity(&self) -> f64 {
146 if self.cardinality == 0 {
147 1.0
148 } else {
149 1.0 / self.cardinality as f64
150 }
151 }
152
153 pub fn estimate_point_query(&self) -> usize {
155 if self.is_unique {
156 1
157 } else {
158 (self.total_rows as f64 * self.selectivity()) as usize
159 }
160 }
161
162 pub fn estimate_range_query(&self, range_fraction: f64) -> usize {
164 (self.total_rows as f64 * range_fraction) as usize
165 }
166}
167
168pub struct QueryOptimizer {
170 db: Arc<MoteDB>,
172
173 index_stats: DashMap<String, IndexStats>,
175
176 cost_params: CostParameters,
178}
179
180#[derive(Debug, Clone)]
182struct CostParameters {
183 disk_read_cost: f64,
185 lsm_point_read_cost: f64,
187 index_lookup_cost: f64,
189 predicate_eval_cost: f64,
191}
192
193impl Default for CostParameters {
194 fn default() -> Self {
195 Self {
196 disk_read_cost: 0.01, lsm_point_read_cost: 0.03, index_lookup_cost: 0.005, predicate_eval_cost: 0.0001, }
201 }
202}
203
204impl QueryOptimizer {
205 pub fn new(db: Arc<MoteDB>) -> Self {
206 Self {
207 db,
208 index_stats: DashMap::new(),
209 cost_params: CostParameters::default(),
210 }
211 }
212
213 fn positive_inf(val: &Value) -> Value {
215 match val {
216 Value::Float(_) => Value::Float(f64::MAX),
217 Value::Timestamp(_) => Value::Timestamp(crate::types::Timestamp::from_micros(i64::MAX)),
218 _ => Value::Integer(i64::MAX),
219 }
220 }
221
222 fn negative_inf(val: &Value) -> Value {
224 match val {
225 Value::Float(_) => Value::Float(f64::MIN),
226 Value::Timestamp(_) => Value::Timestamp(crate::types::Timestamp::from_micros(i64::MIN)),
227 _ => Value::Integer(i64::MIN),
228 }
229 }
230
231 fn resolve_to_value(
234 params: &[crate::types::Value],
235 expr: &crate::sql::ast::Expr,
236 ) -> Option<crate::types::Value> {
237 use crate::sql::ast::Expr;
238 match expr {
239 Expr::Literal(v) => Some(v.clone()),
240 Expr::Parameter(idx) if *idx > 0 => params.get(idx - 1).cloned(),
241 _ => None,
242 }
243 }
244
245 pub fn optimize_select(
247 &self,
248 stmt: &SelectStmt,
249 params: &[crate::types::Value],
250 ) -> Result<QueryPlan> {
251 if let Some(plan) = self.optimize_primary_key_order_by(stmt)? {
256 return Ok(plan);
257 }
258
259 if let Some(plan) = self.optimize_vector_order_by(stmt)? {
262 return Ok(plan);
263 }
264
265 if self.is_aggregate_query(stmt) {
268 if let Some(plan) = self.optimize_aggregate(stmt, params)? {
269 return Ok(plan);
270 }
271 }
272
273 let table_name = match stmt.from.as_ref().unwrap() {
275 TableRef::Table { name, .. } => name.clone(),
276 _ => {
277 return Ok(QueryPlan {
279 scan_method: ScanMethod::FullScan {
280 table: "unknown".to_string(),
281 },
282 estimated_cost: f64::MAX,
283 estimated_rows: 0,
284 post_filters: vec![],
285 });
286 }
287 };
288
289 let schema = self.db.get_table_schema(&table_name)?;
291 let total_rows = self.estimate_table_size(&table_name);
292
293 let where_clause = match &stmt.where_clause {
295 Some(expr) => expr,
296 None => {
297 return Ok(QueryPlan {
299 scan_method: ScanMethod::FullScan {
300 table: table_name.clone(),
301 },
302 estimated_cost: self.cost_full_scan(total_rows),
303 estimated_rows: total_rows,
304 post_filters: vec![],
305 });
306 }
307 };
308
309 let candidates =
311 self.generate_candidate_plans(&table_name, where_clause, &schema, params)?;
312
313 let best_plan = candidates
315 .into_iter()
316 .min_by(|a, b| {
317 a.estimated_cost
318 .partial_cmp(&b.estimated_cost)
319 .unwrap_or(std::cmp::Ordering::Equal) })
321 .unwrap_or_else(|| QueryPlan {
322 scan_method: ScanMethod::FullScan {
323 table: table_name.clone(),
324 },
325 estimated_cost: self.cost_full_scan(total_rows),
326 estimated_rows: total_rows,
327 post_filters: vec![where_clause.clone()],
328 });
329
330 Ok(best_plan)
331 }
332
333 fn generate_candidate_plans(
335 &self,
336 table_name: &str,
337 where_clause: &Expr,
338 _schema: &TableSchema,
339 params: &[crate::types::Value],
340 ) -> Result<Vec<QueryPlan>> {
341 let mut plans = Vec::new();
342 let total_rows = self.estimate_table_size(table_name);
343
344 plans.push(QueryPlan {
346 scan_method: ScanMethod::FullScan {
347 table: table_name.to_string(),
348 },
349 estimated_cost: self.cost_full_scan(total_rows),
350 estimated_rows: total_rows,
351 post_filters: vec![where_clause.clone()],
352 });
353
354 self.analyze_where_clause(table_name, where_clause, params, &mut plans)?;
356
357 let full_where = where_clause.clone();
363 for plan in &mut plans {
364 if plan.post_filters.is_empty() {
365 plan.post_filters.push(full_where.clone());
366 }
367 }
368
369 Ok(plans)
370 }
371
372 fn analyze_where_clause(
374 &self,
375 table_name: &str,
376 expr: &Expr,
377 params: &[crate::types::Value],
378 plans: &mut Vec<QueryPlan>,
379 ) -> Result<()> {
380 if let Some((column, query_vector, k)) = self.try_extract_vector_search(expr) {
382 self.try_vector_search_plan(table_name, &column, &query_vector, k, plans)?;
383 return Ok(()); }
385
386 if let Some((col, start, start_incl, end, end_incl)) =
388 self.try_extract_range_query(expr, params)
389 {
390 self.try_range_query_plan(table_name, &col, start, start_incl, end, end_incl, plans)?;
391 return Ok(()); }
393
394 match expr {
395 Expr::BinaryOp {
397 left,
398 op: BinaryOperator::And,
399 right,
400 } => {
401 self.analyze_where_clause(table_name, left, params, plans)?;
403
404 self.analyze_where_clause(table_name, right, params, plans)?;
406
407 self.try_index_intersection(table_name, left, right, params, plans)?;
409 }
410
411 Expr::BinaryOp {
420 op: BinaryOperator::Or,
421 ..
422 } => {
423 }
427
428 Expr::BinaryOp {
430 left,
431 op: BinaryOperator::Eq,
432 right,
433 } => {
434 if let Some(val) = Self::resolve_to_value(params, right) {
435 if let Expr::Column(col) = left.as_ref() {
436 self.try_point_query_plan(table_name, col, val, plans)?;
437 }
438 } else if let Some(val) = Self::resolve_to_value(params, left) {
439 if let Expr::Column(col) = right.as_ref() {
440 self.try_point_query_plan(table_name, col, val, plans)?;
441 }
442 }
443 }
444
445 Expr::BinaryOp {
447 left,
448 op: BinaryOperator::Gt,
449 right,
450 } => {
451 if let Some(val) = Self::resolve_to_value(params, right) {
452 if let Expr::Column(col) = left.as_ref() {
453 let pos_inf = Self::positive_inf(&val);
454 self.try_range_query_plan(
455 table_name,
456 col,
457 val.clone(),
458 false,
459 pos_inf,
460 true,
461 plans,
462 )?;
463 }
464 } else if let Some(val) = Self::resolve_to_value(params, left) {
465 if let Expr::Column(col) = right.as_ref() {
466 let neg_inf = Self::negative_inf(&val);
467 self.try_range_query_plan(
468 table_name,
469 col,
470 neg_inf,
471 true,
472 val.clone(),
473 false,
474 plans,
475 )?;
476 }
477 }
478 }
479
480 Expr::BinaryOp {
482 left,
483 op: BinaryOperator::Ge,
484 right,
485 } => {
486 if let Some(val) = Self::resolve_to_value(params, right) {
487 if let Expr::Column(col) = left.as_ref() {
488 let pos_inf = Self::positive_inf(&val);
489 self.try_range_query_plan(
490 table_name,
491 col,
492 val.clone(),
493 true,
494 pos_inf,
495 true,
496 plans,
497 )?;
498 }
499 } else if let Some(val) = Self::resolve_to_value(params, left) {
500 if let Expr::Column(col) = right.as_ref() {
501 let neg_inf = Self::negative_inf(&val);
502 self.try_range_query_plan(
503 table_name,
504 col,
505 neg_inf,
506 true,
507 val.clone(),
508 true,
509 plans,
510 )?;
511 }
512 }
513 }
514
515 Expr::BinaryOp {
517 left,
518 op: BinaryOperator::Lt,
519 right,
520 } => {
521 if let Some(val) = Self::resolve_to_value(params, right) {
522 if let Expr::Column(col) = left.as_ref() {
523 let neg_inf = Self::negative_inf(&val);
524 self.try_range_query_plan(
525 table_name,
526 col,
527 neg_inf,
528 true,
529 val.clone(),
530 false,
531 plans,
532 )?;
533 }
534 } else if let Some(val) = Self::resolve_to_value(params, left) {
535 if let Expr::Column(col) = right.as_ref() {
536 let pos_inf = Self::positive_inf(&val);
537 self.try_range_query_plan(
538 table_name,
539 col,
540 val.clone(),
541 false,
542 pos_inf,
543 true,
544 plans,
545 )?;
546 }
547 }
548 }
549
550 Expr::BinaryOp {
552 left,
553 op: BinaryOperator::Le,
554 right,
555 } => {
556 if let Some(val) = Self::resolve_to_value(params, right) {
557 if let Expr::Column(col) = left.as_ref() {
558 let neg_inf = Self::negative_inf(&val);
559 self.try_range_query_plan(
560 table_name,
561 col,
562 neg_inf,
563 true,
564 val.clone(),
565 true,
566 plans,
567 )?;
568 }
569 } else if let Some(val) = Self::resolve_to_value(params, left) {
570 if let Expr::Column(col) = right.as_ref() {
571 let pos_inf = Self::positive_inf(&val);
572 self.try_range_query_plan(
573 table_name,
574 col,
575 val.clone(),
576 true,
577 pos_inf,
578 true,
579 plans,
580 )?;
581 }
582 }
583 }
584
585 _ => {
586 }
588 }
589
590 Ok(())
591 }
592
593 fn try_point_query_plan(
595 &self,
596 table_name: &str,
597 column: &str,
598 value: Value,
599 plans: &mut Vec<QueryPlan>,
600 ) -> Result<()> {
601 let index_name = format!("{}.{}", table_name, column);
602
603 let table_result = self.db.table_registry.get_table(table_name);
605 let is_auto_increment_pk = table_result
606 .ok()
607 .map(|schema| {
608 schema
609 .primary_key()
610 .map(|pk| pk == column && schema.is_primary_key_auto_increment())
611 .unwrap_or(false)
612 })
613 .unwrap_or(false);
614
615 if is_auto_increment_pk {
616 plans.push(QueryPlan {
618 scan_method: ScanMethod::PointQuery {
619 table: table_name.to_string(),
620 column: column.to_string(),
621 value,
622 },
623 estimated_cost: self.cost_params.index_lookup_cost,
624 estimated_rows: 1,
625 post_filters: vec![],
626 });
627 return Ok(());
628 }
629
630 if !self.db.column_indexes.contains_key(&index_name) {
632 return Ok(()); }
634
635 let stats = self.get_index_stats(&index_name)?;
637 let estimated_rows = stats.estimate_point_query();
638
639 const PQ_SEL_DENOM: usize = 20; const MIN_EST_FOR_FULLSCAN: usize = 10; if stats.total_rows > 0
646 && estimated_rows >= stats.total_rows / PQ_SEL_DENOM
647 && estimated_rows >= MIN_EST_FOR_FULLSCAN
648 {
649 return Ok(());
650 }
651
652 let cost = self.cost_params.index_lookup_cost
654 + (estimated_rows as f64 * self.cost_params.lsm_point_read_cost);
655
656 plans.push(QueryPlan {
657 scan_method: ScanMethod::PointQuery {
658 table: table_name.to_string(),
659 column: column.to_string(),
660 value,
661 },
662 estimated_cost: cost,
663 estimated_rows,
664 post_filters: vec![], });
666
667 Ok(())
668 }
669
670 #[allow(clippy::too_many_arguments)]
676 fn try_range_query_plan(
677 &self,
678 table_name: &str,
679 column: &str,
680 start: Value,
681 start_inclusive: bool,
682 end: Value,
683 end_inclusive: bool,
684 plans: &mut Vec<QueryPlan>,
685 ) -> Result<()> {
686 let index_name = format!("{}.{}", table_name, column);
687
688 if !self.db.column_indexes.contains_key(&index_name) {
690 return Ok(()); }
692
693 let stats = self.get_index_stats(&index_name)?;
695
696 let range_fraction = Self::estimate_range_fraction(&start, &end);
698 let estimated_rows = stats.estimate_range_query(range_fraction);
699
700 let cost = self.cost_params.index_lookup_cost * (estimated_rows as f64 * 0.1)
702 + (estimated_rows as f64 * self.cost_params.lsm_point_read_cost);
703
704 plans.push(QueryPlan {
705 scan_method: ScanMethod::RangeQuery {
706 table: table_name.to_string(),
707 column: column.to_string(),
708 start,
709 start_inclusive,
710 end,
711 end_inclusive,
712 },
713 estimated_cost: cost,
714 estimated_rows,
715 post_filters: vec![], });
717
718 Ok(())
719 }
720
721 fn try_index_intersection(
733 &self,
734 table_name: &str,
735 left: &Expr,
736 right: &Expr,
737 _params: &[crate::types::Value],
738 plans: &mut Vec<QueryPlan>,
739 ) -> Result<()> {
740 let left_cv = Self::extract_eq_column_value(left);
742 let right_cv = Self::extract_eq_column_value(right);
743
744 if let (Some((col1, val1)), Some((col2, val2))) = (left_cv, right_cv) {
745 let idx1 = format!("{}.{}", table_name, col1);
747 let idx2 = format!("{}.{}", table_name, col2);
748
749 if col1 != col2
750 && self.db.column_indexes.contains_key(&idx1)
751 && self.db.column_indexes.contains_key(&idx2)
752 {
753 let stats1 = self.get_index_stats(&idx1).unwrap_or(IndexStats {
755 cardinality: 100,
756 total_rows: 10000,
757 size_bytes: 0,
758 is_unique: false,
759 });
760 let stats2 = self.get_index_stats(&idx2).unwrap_or(IndexStats {
761 cardinality: 100,
762 total_rows: 10000,
763 size_bytes: 0,
764 is_unique: false,
765 });
766
767 let sel1 = stats1.selectivity();
768 let sel2 = stats2.selectivity();
769 let combined_sel = sel1 * sel2;
770 let estimated_rows = ((stats1.total_rows as f64) * combined_sel).max(1.0) as usize;
771
772 let cost = self.cost_params.index_lookup_cost * 2.0
774 + (estimated_rows as f64 * self.cost_params.lsm_point_read_cost);
775
776 if estimated_rows < stats1.total_rows / 3 {
779 plans.push(QueryPlan {
780 scan_method: ScanMethod::IndexIntersection {
781 table: table_name.to_string(),
782 column1: col1,
783 value1: val1,
784 column2: col2,
785 value2: val2,
786 },
787 estimated_cost: cost,
788 estimated_rows,
789 post_filters: vec![],
790 });
791 }
792 }
793 }
794
795 Ok(())
796 }
797
798 fn extract_eq_column_value(expr: &Expr) -> Option<(String, Value)> {
800 if let Expr::BinaryOp {
801 left,
802 op: BinaryOperator::Eq,
803 right,
804 } = expr
805 {
806 if let Expr::Column(col) = left.as_ref() {
807 if let Expr::Literal(val) = right.as_ref() {
808 return Some((col.clone(), val.clone()));
809 }
810 }
811 }
812 None
813 }
814
815 fn try_extract_range_query(
816 &self,
817 expr: &Expr,
818 params: &[crate::types::Value],
819 ) -> Option<(String, Value, bool, Value, bool)> {
820 match expr {
821 Expr::BinaryOp {
822 left,
823 op: BinaryOperator::And,
824 right,
825 } => {
826 if let (
827 Expr::BinaryOp {
828 left: l1,
829 op: op1,
830 right: r1,
831 },
832 Expr::BinaryOp {
833 left: l2,
834 op: op2,
835 right: r2,
836 },
837 ) = (left.as_ref(), right.as_ref())
838 {
839 let col1 = match (l1.as_ref(), r1.as_ref()) {
841 (Expr::Column(c), other)
842 if Self::resolve_to_value(params, other).is_some() =>
843 {
844 Some(c)
845 }
846 (other, Expr::Column(c))
847 if Self::resolve_to_value(params, other).is_some() =>
848 {
849 Some(c)
850 }
851 _ => None,
852 };
853
854 let col2 = match (l2.as_ref(), r2.as_ref()) {
855 (Expr::Column(c), other)
856 if Self::resolve_to_value(params, other).is_some() =>
857 {
858 Some(c)
859 }
860 (other, Expr::Column(c))
861 if Self::resolve_to_value(params, other).is_some() =>
862 {
863 Some(c)
864 }
865 _ => None,
866 };
867
868 if let (Some(c1), Some(c2)) = (&col1, &col2) {
869 if c1 == c2 {
870 let col_name = (*c1).clone();
871
872 let extract =
874 |col: &Expr,
875 op: &BinaryOperator,
876 val: &Expr|
877 -> Option<(Value, bool, bool)> {
878 let v = Self::resolve_to_value(params, val)?;
879 match (col, op) {
880 (Expr::Column(_), BinaryOperator::Ge) => {
881 Some((v, true, true))
882 }
883 (Expr::Column(_), BinaryOperator::Gt) => {
884 Some((v, true, false))
885 }
886 (Expr::Column(_), BinaryOperator::Le) => {
887 Some((v, false, true))
888 }
889 (Expr::Column(_), BinaryOperator::Lt) => {
890 Some((v, false, false))
891 }
892 (_, BinaryOperator::Le) => Some((v, true, true)),
893 (_, BinaryOperator::Lt) => Some((v, true, false)),
894 (_, BinaryOperator::Ge) => Some((v, false, true)),
895 (_, BinaryOperator::Gt) => Some((v, false, false)),
896 _ => None,
897 }
898 };
899
900 let (val1, is_lower1, inclusive1) = extract(l1, op1, r1)?;
901 let (val2, is_lower2, inclusive2) = extract(l2, op2, r2)?;
902
903 if is_lower1 && !is_lower2 {
905 return Some((col_name, val1, inclusive1, val2, inclusive2));
906 } else if !is_lower1 && is_lower2 {
907 return Some((col_name, val2, inclusive2, val1, inclusive1));
908 }
909 }
910 }
911 }
912 None
913 }
914 _ => None,
915 }
916 }
917
918 fn try_extract_vector_search(
921 &self,
922 expr: &Expr,
923 ) -> Option<(String, crate::types::ArcVec, usize)> {
924 match expr {
925 Expr::FunctionCall { name, args, .. } if name.to_uppercase() == "VECTOR_SEARCH" => {
926 if args.len() != 3 {
927 return None;
928 }
929
930 let column = match &args[0] {
932 Expr::Column(col) => col.clone(),
933 _ => return None,
934 };
935
936 let query_vector = match &args[1] {
938 Expr::Literal(Value::Vector(vec)) => vec.clone(),
939 _ => return None,
940 };
941
942 let k = match &args[2] {
944 Expr::Literal(Value::Integer(k)) => *k as usize,
945 _ => return None,
946 };
947
948 Some((column, query_vector, k))
949 }
950 _ => None,
951 }
952 }
953
954 fn try_vector_search_plan(
956 &self,
957 table_name: &str,
958 column: &str,
959 query_vector: &crate::types::ArcVec,
960 k: usize,
961 plans: &mut Vec<QueryPlan>,
962 ) -> Result<()> {
963 let estimated_rows = k;
968
969 let cost = self.cost_params.index_lookup_cost + (k as f64 * 0.001);
971
972 plans.push(QueryPlan {
973 scan_method: ScanMethod::VectorSearch {
974 table: table_name.to_string(),
975 column: column.to_string(),
976 query_vector: query_vector.clone(),
977 k,
978 },
979 estimated_cost: cost,
980 estimated_rows,
981 post_filters: vec![], });
983
984 Ok(())
985 }
986
987 fn get_index_stats(&self, index_name: &str) -> Result<IndexStats> {
989 if let Some(stats) = self.index_stats.get(index_name) {
991 return Ok(stats.clone());
992 }
993
994 let table_name = index_name.split('.').next().unwrap_or("unknown");
996 let table_rows = self.estimate_table_size(table_name);
997
998 let cardinality = if let Some(idx) = self.db.column_indexes.get(index_name) {
1000 idx.value().entry_count().max(1)
1001 } else {
1002 (table_rows / 10).max(1)
1003 };
1004
1005 let stats = IndexStats {
1006 cardinality,
1007 total_rows: table_rows,
1008 size_bytes: cardinality * 64,
1009 is_unique: false,
1010 };
1011
1012 self.index_stats
1013 .insert(index_name.to_string(), stats.clone());
1014 Ok(stats)
1015 }
1016
1017 fn estimate_table_size(&self, table_name: &str) -> usize {
1019 self.db
1020 .estimate_table_row_count(table_name)
1021 .unwrap_or(1_000)
1022 .max(1) }
1024
1025 fn cost_full_scan(&self, total_rows: usize) -> f64 {
1027 (total_rows as f64 * self.cost_params.disk_read_cost)
1029 + (total_rows as f64 * self.cost_params.predicate_eval_cost)
1030 }
1031
1032 fn estimate_range_fraction(start: &Value, end: &Value) -> f64 {
1035 match (start, end) {
1036 (Value::Integer(s), Value::Integer(e)) => {
1037 let range = if *e >= *s {
1039 (*e as i128 - *s as i128) as f64
1040 } else {
1041 (*s as i128 - *e as i128) as f64
1042 };
1043 ((range / 2_000_000_000.0) * 2.0).clamp(0.001, 0.5)
1045 }
1046 (Value::Float(s), Value::Float(e)) => {
1047 let range = (e - s).abs();
1048 ((range / 2_000_000.0) * 2.0).clamp(0.001, 0.5)
1050 }
1051 (Value::Timestamp(s), Value::Timestamp(e)) => {
1052 let range = (e.as_micros() as f64 - s.as_micros() as f64).abs();
1053 let one_year_us = 365.0 * 24.0 * 3600.0 * 1_000_000.0;
1055 (range / one_year_us).clamp(0.001, 0.5)
1056 }
1057 _ => 0.1, }
1059 }
1060}
1061
1062#[cfg(test)]
1063#[allow(clippy::items_after_test_module)]
1064mod tests {
1065 use super::*;
1066
1067 #[test]
1068 fn test_index_stats() {
1069 let stats = IndexStats {
1070 cardinality: 1000,
1071 total_rows: 10000,
1072 size_bytes: 100_000,
1073 is_unique: false,
1074 };
1075
1076 assert_eq!(stats.selectivity(), 0.001);
1077 assert_eq!(stats.estimate_point_query(), 10);
1078 assert_eq!(stats.estimate_range_query(0.1), 1000);
1079 }
1080}
1081
1082impl QueryOptimizer {
1084 fn optimize_primary_key_order_by(&self, stmt: &SelectStmt) -> Result<Option<QueryPlan>> {
1099 let order_by = match &stmt.order_by {
1101 Some(o) if o.len() == 1 => &o[0],
1102 _ => return Ok(None),
1103 };
1104
1105 let order_column = match &order_by.expr {
1107 Expr::Column(col) => col,
1108 _ => return Ok(None),
1109 };
1110
1111 let table_name = match stmt.from.as_ref().unwrap() {
1113 TableRef::Table { name, .. } => name,
1114 _ => return Ok(None),
1115 };
1116
1117 let schema = self.db.get_table_schema(table_name)?;
1119 let is_primary_key = schema
1120 .primary_key()
1121 .map(|pk| pk == order_column)
1122 .unwrap_or(false);
1123
1124 if !is_primary_key {
1125 return Ok(None);
1126 }
1127
1128 if stmt.where_clause.is_some() {
1131 return Ok(None);
1132 }
1133
1134 let is_simple_select = matches!(&stmt.columns[..], [SelectColumn::Star]);
1137 if !is_simple_select {
1138 let has_complex_expr = stmt
1140 .columns
1141 .iter()
1142 .any(|col| matches!(col, SelectColumn::Expr(_, _)));
1143 if has_complex_expr {
1144 return Ok(None);
1145 }
1146 }
1147
1148 let estimated_rows = stmt
1149 .limit
1150 .unwrap_or_else(|| self.estimate_table_size(table_name));
1151
1152 Ok(Some(QueryPlan {
1153 scan_method: ScanMethod::PrimaryKeyScan {
1154 table: table_name.clone(),
1155 ascending: order_by.asc,
1156 limit: stmt.limit,
1157 },
1158 estimated_cost: estimated_rows as f64 * self.cost_params.index_lookup_cost,
1159 estimated_rows,
1160 post_filters: vec![],
1161 }))
1162 }
1163}
1164
1165impl QueryOptimizer {
1167 fn optimize_vector_order_by(&self, stmt: &SelectStmt) -> Result<Option<QueryPlan>> {
1175 let order_by = match &stmt.order_by {
1177 Some(o) if o.len() == 1 => &o[0], _ => return Ok(None),
1179 };
1180
1181 let limit = match stmt.limit {
1182 Some(k) if k > 0 => k,
1183 _ => return Ok(None), };
1185
1186 let (column, query_vector, asc) = match &order_by.expr {
1188 Expr::BinaryOp {
1190 op: BinaryOperator::L2Distance | BinaryOperator::CosineDistance,
1191 left,
1192 right,
1193 } => match (&**left, &**right) {
1194 (Expr::Column(col), Expr::Literal(Value::Vector(vec))) => {
1195 (col.clone(), vec.clone(), order_by.asc)
1196 }
1197 _ => return Ok(None),
1198 },
1199
1200 Expr::FunctionCall { name, args, .. } if name.to_uppercase() == "VECTOR_DISTANCE" => {
1202 if args.len() != 2 {
1203 return Ok(None);
1204 }
1205 match (&args[0], &args[1]) {
1206 (Expr::Column(col), Expr::Literal(Value::Vector(vec))) => {
1207 (col.clone(), vec.clone(), order_by.asc)
1208 }
1209 _ => return Ok(None),
1210 }
1211 }
1212
1213 _ => return Ok(None),
1214 };
1215
1216 if !asc {
1218 return Ok(None); }
1220
1221 let table_name = match stmt.from.as_ref().unwrap() {
1223 TableRef::Table { name, .. } => name.clone(),
1224 _ => return Ok(None),
1225 };
1226
1227 let index_name = self
1229 .db
1230 .index_registry
1231 .find_by_column(
1232 &table_name,
1233 &column,
1234 crate::database::index_metadata::IndexType::Vector,
1235 )
1236 .unwrap_or_else(|| format!("{}_{}", table_name, column));
1237 let has_vector_index = self.db.has_vector_index(&index_name);
1238
1239 if !has_vector_index {
1240 return Ok(None);
1242 }
1243
1244 Ok(Some(QueryPlan {
1246 scan_method: ScanMethod::VectorSearch {
1247 table: table_name,
1248 column,
1249 query_vector: query_vector.clone(),
1250 k: limit,
1251 },
1252 estimated_cost: self.cost_params.index_lookup_cost
1253 + (limit as f64 * self.cost_params.lsm_point_read_cost),
1254 estimated_rows: limit,
1255 post_filters: vec![],
1256 }))
1257 }
1258}
1259
1260impl QueryOptimizer {
1262 fn is_aggregate_query(&self, stmt: &SelectStmt) -> bool {
1264 stmt.columns.iter().any(|col| match col {
1265 SelectColumn::Expr(expr, _) => self.is_aggregate_expr(expr),
1266 _ => false,
1267 })
1268 }
1269
1270 fn is_aggregate_expr(&self, expr: &Expr) -> bool {
1272 match expr {
1273 Expr::FunctionCall { name, .. } => {
1274 matches!(
1275 name.to_uppercase().as_str(),
1276 "COUNT" | "SUM" | "AVG" | "MIN" | "MAX"
1277 )
1278 }
1279 _ => false,
1280 }
1281 }
1282
1283 fn optimize_aggregate(
1285 &self,
1286 stmt: &SelectStmt,
1287 params: &[crate::types::Value],
1288 ) -> Result<Option<QueryPlan>> {
1289 let table_name = match stmt.from.as_ref().unwrap() {
1291 TableRef::Table { name, .. } => name.clone(),
1292 _ => return Ok(None),
1293 };
1294
1295 let total_rows = self.estimate_table_size(&table_name);
1296
1297 if let Some(where_clause) = &stmt.where_clause {
1299 if let Some((col, start, start_incl, end, end_incl)) =
1301 self.try_extract_range_query(where_clause, params)
1302 {
1303 let index_name = format!("{}.{}", table_name, col);
1304 let index_exists = self.db.column_indexes.contains_key(&index_name);
1305
1306 if index_exists {
1307 let range_fraction = Self::estimate_range_fraction(&start, &end);
1308 let range_rows = (total_rows as f64 * range_fraction) as usize;
1309 return Ok(Some(QueryPlan {
1310 scan_method: ScanMethod::RangeQuery {
1311 table: table_name.clone(),
1312 column: col,
1313 start,
1314 start_inclusive: start_incl,
1315 end,
1316 end_inclusive: end_incl,
1317 },
1318 estimated_cost: self.cost_params.index_lookup_cost * (range_rows as f64)
1319 + range_rows as f64 * self.cost_params.lsm_point_read_cost,
1320 estimated_rows: 1,
1321 post_filters: vec![where_clause.clone()],
1322 }));
1323 }
1324 }
1325
1326 if let Some((col, val)) = self.try_extract_point_query(where_clause, params) {
1328 let index_name = format!("{}.{}", table_name, col);
1329 let index_exists = self.db.column_indexes.contains_key(&index_name);
1330
1331 if index_exists {
1332 return Ok(Some(QueryPlan {
1333 scan_method: ScanMethod::PointQuery {
1334 table: table_name.clone(),
1335 column: col,
1336 value: val,
1337 },
1338 estimated_cost: self.cost_params.index_lookup_cost,
1339 estimated_rows: 1,
1340 post_filters: vec![where_clause.clone()],
1341 }));
1342 }
1343 }
1344
1345 if let Some(plan) = self.try_single_sided_range(&table_name, where_clause, params)? {
1347 return Ok(Some(QueryPlan {
1348 scan_method: plan.scan_method,
1349 estimated_cost: plan.estimated_cost,
1350 estimated_rows: 1,
1351 post_filters: vec![where_clause.clone()],
1352 }));
1353 }
1354 }
1355
1356 Ok(Some(QueryPlan {
1358 scan_method: ScanMethod::FullScan {
1359 table: table_name.clone(),
1360 },
1361 estimated_cost: self.cost_full_scan(total_rows),
1362 estimated_rows: 1,
1363 post_filters: stmt
1364 .where_clause
1365 .as_ref()
1366 .map(|clause| vec![clause.clone()])
1367 .unwrap_or_default(),
1368 }))
1369 }
1370
1371 fn try_extract_point_query(
1373 &self,
1374 expr: &Expr,
1375 params: &[crate::types::Value],
1376 ) -> Option<(String, Value)> {
1377 match expr {
1378 Expr::BinaryOp {
1379 left,
1380 op: BinaryOperator::Eq,
1381 right,
1382 } => {
1383 if let Some(val) = Self::resolve_to_value(params, right) {
1384 if let Expr::Column(col) = left.as_ref() {
1385 return Some((col.clone(), val));
1386 }
1387 }
1388 if let Some(val) = Self::resolve_to_value(params, left) {
1389 if let Expr::Column(col) = right.as_ref() {
1390 return Some((col.clone(), val));
1391 }
1392 }
1393 None
1394 }
1395 _ => None,
1396 }
1397 }
1398
1399 fn try_single_sided_range(
1401 &self,
1402 table_name: &str,
1403 expr: &Expr,
1404 params: &[crate::types::Value],
1405 ) -> Result<Option<QueryPlan>> {
1406 let mut plans = Vec::new();
1407 self.analyze_where_clause(table_name, expr, params, &mut plans)?;
1408 Ok(plans.into_iter().min_by_key(|p| p.estimated_cost as u64))
1409 }
1410}
1411
1412#[cfg(test)]
1413mod regression_tests {
1414 use super::*;
1415 use crate::sql::ast::{BinaryOperator, Expr};
1416 use crate::types::Value;
1417
1418 #[test]
1419 fn test_reversed_lt_exclusive_lower_bound() {
1420 let _val = Value::Integer(10);
1424 let is_lower = true;
1426 let inclusive = false;
1427 assert!(is_lower);
1428 assert!(!inclusive);
1429 }
1430
1431 #[test]
1432 fn test_reversed_ge_inclusive_upper_bound() {
1433 let _val = Value::Integer(10);
1437 let is_lower = false;
1438 let inclusive = true;
1439 assert!(!is_lower);
1440 assert!(inclusive);
1441 }
1442
1443 #[test]
1444 fn test_post_filters_set_for_index_plans() {
1445 let plan = QueryPlan {
1448 scan_method: ScanMethod::PointQuery {
1449 table: "t".to_string(),
1450 column: "id".to_string(),
1451 value: Value::Integer(5),
1452 },
1453 estimated_cost: 0.1,
1454 estimated_rows: 1,
1455 post_filters: vec![Expr::BinaryOp {
1456 left: Box::new(Expr::Column("id".to_string())),
1457 op: BinaryOperator::Eq,
1458 right: Box::new(Expr::Literal(Value::Integer(5))),
1459 }],
1460 };
1461 assert!(!plan.post_filters.is_empty());
1462 }
1463}