1use crate::ast::{JoinQuery, JoinType, QueryExpr};
14use crate::sql_lowering::{effective_table_filter, effective_vector_filter};
15
16pub trait OptimizationPass: Send + Sync {
18 fn name(&self) -> &str;
20
21 fn apply(&self, query: QueryExpr) -> QueryExpr;
23
24 fn benefit(&self) -> u32;
26}
27
28pub struct QueryOptimizer {
30 passes: Vec<Box<dyn OptimizationPass>>,
32 cost_based: bool,
34}
35
36impl QueryOptimizer {
37 pub fn new() -> Self {
39 let passes: Vec<Box<dyn OptimizationPass>> = vec![
40 Box::new(PredicatePushdownPass),
41 Box::new(ProjectionPushdownPass),
42 Box::new(JoinReorderingPass),
43 Box::new(IndexSelectionPass),
44 Box::new(LimitPushdownPass),
45 ];
46
47 Self {
48 passes,
49 cost_based: true,
50 }
51 }
52
53 pub fn add_pass(&mut self, pass: Box<dyn OptimizationPass>) {
55 self.passes.push(pass);
56 self.passes.sort_by_key(|b| std::cmp::Reverse(b.benefit()));
58 }
59
60 pub fn optimize(&self, query: QueryExpr) -> (QueryExpr, Vec<String>) {
62 let mut optimized = query;
63 let mut applied_passes = Vec::new();
64
65 for pass in &self.passes {
66 let before = format!("{:?}", optimized);
67 optimized = pass.apply(optimized);
68 let after = format!("{:?}", optimized);
69
70 if before != after {
71 applied_passes.push(pass.name().to_string());
72 }
73 }
74
75 (optimized, applied_passes)
76 }
77
78 pub fn optimize_with_hints(&self, query: QueryExpr, hints: &OptimizationHints) -> QueryExpr {
80 let mut optimized = query;
81
82 for pass in &self.passes {
83 if hints.disabled_passes.contains(&pass.name().to_string()) {
85 continue;
86 }
87
88 optimized = pass.apply(optimized);
89 }
90
91 optimized
92 }
93}
94
95impl Default for QueryOptimizer {
96 fn default() -> Self {
97 Self::new()
98 }
99}
100
101#[derive(Debug, Clone, Default)]
103pub struct OptimizationHints {
104 pub disabled_passes: Vec<String>,
106 pub join_order: Option<Vec<String>>,
108 pub force_index: Option<String>,
110 pub no_parallel: bool,
112}
113
114struct PredicatePushdownPass;
120
121impl OptimizationPass for PredicatePushdownPass {
122 fn name(&self) -> &str {
123 "PredicatePushdown"
124 }
125
126 fn apply(&self, query: QueryExpr) -> QueryExpr {
127 match query {
128 QueryExpr::Join(jq) => self.optimize_join(jq),
129 other => other,
130 }
131 }
132
133 fn benefit(&self) -> u32 {
134 100 }
136}
137
138impl PredicatePushdownPass {
139 fn optimize_join(&self, query: JoinQuery) -> QueryExpr {
140 let left = self.apply(*query.left);
145 let right = self.apply(*query.right);
146
147 QueryExpr::Join(JoinQuery {
148 left: Box::new(left),
149 right: Box::new(right),
150 ..query
151 })
152 }
153}
154
155struct ProjectionPushdownPass;
157
158impl OptimizationPass for ProjectionPushdownPass {
159 fn name(&self) -> &str {
160 "ProjectionPushdown"
161 }
162
163 fn apply(&self, query: QueryExpr) -> QueryExpr {
164 match query {
165 QueryExpr::Join(jq) => {
166 let left = self.apply(*jq.left);
168 let right = self.apply(*jq.right);
169
170 QueryExpr::Join(JoinQuery {
171 left: Box::new(left),
172 right: Box::new(right),
173 ..jq
174 })
175 }
176 QueryExpr::Table(tq) => {
177 QueryExpr::Table(tq)
180 }
181 other => other,
182 }
183 }
184
185 fn benefit(&self) -> u32 {
186 80 }
188}
189
190struct JoinReorderingPass;
192
193impl OptimizationPass for JoinReorderingPass {
194 fn name(&self) -> &str {
195 "JoinReordering"
196 }
197
198 fn apply(&self, query: QueryExpr) -> QueryExpr {
199 match query {
200 QueryExpr::Join(jq) => {
201 self.optimize_join_order(jq)
204 }
205 other => other,
206 }
207 }
208
209 fn benefit(&self) -> u32 {
210 90 }
212}
213
214impl JoinReorderingPass {
215 fn optimize_join_order(&self, query: JoinQuery) -> QueryExpr {
216 let left_size = Self::estimate_size(&query.left);
218 let right_size = Self::estimate_size(&query.right);
219
220 if left_size > right_size && query.join_type == JoinType::Inner {
222 let JoinQuery {
224 left,
225 right,
226 join_type,
227 on,
228 filter,
229 order_by,
230 limit,
231 offset,
232 return_items,
233 return_,
234 } = query;
235 QueryExpr::Join(JoinQuery {
236 left: right,
237 right: left,
238 join_type,
239 on: swap_condition(on),
240 filter,
241 order_by,
242 limit,
243 offset,
244 return_items,
245 return_,
246 })
247 } else {
248 QueryExpr::Join(query)
249 }
250 }
251
252 fn estimate_size(query: &QueryExpr) -> f64 {
253 match query {
254 QueryExpr::Table(tq) => {
255 let base = 1000.0;
256 if effective_table_filter(tq).is_some() {
257 base * 0.1
258 } else if tq.limit.is_some() {
259 tq.limit.unwrap() as f64
260 } else {
261 base
262 }
263 }
264 QueryExpr::Graph(_) => 100.0,
265 QueryExpr::Join(jq) => {
266 Self::estimate_size(&jq.left) * Self::estimate_size(&jq.right) * 0.1
267 }
268 QueryExpr::Path(_) => 10.0,
269 QueryExpr::Vector(vq) => {
270 if effective_vector_filter(vq).is_some() {
272 (vq.k as f64).min(100.0)
273 } else {
274 vq.k as f64
275 }
276 }
277 QueryExpr::Hybrid(hq) => {
278 let structured_size = Self::estimate_size(&hq.structured);
280 let vector_size = hq.vector.k as f64;
281 let base = structured_size.min(vector_size);
283 hq.limit.map(|l| base.min(l as f64)).unwrap_or(base)
284 }
285 QueryExpr::Explain(explain) => Self::estimate_size(&explain.inner),
286 QueryExpr::Insert(_)
288 | QueryExpr::Update(_)
289 | QueryExpr::Delete(_)
290 | QueryExpr::CreateTable(_)
291 | QueryExpr::CreateCollection(_)
292 | QueryExpr::CreateVector(_)
293 | QueryExpr::DropTable(_)
294 | QueryExpr::DropGraph(_)
295 | QueryExpr::DropVector(_)
296 | QueryExpr::DropDocument(_)
297 | QueryExpr::DropKv(_)
298 | QueryExpr::DropCollection(_)
299 | QueryExpr::Truncate(_)
300 | QueryExpr::AlterTable(_)
301 | QueryExpr::CreateVcsRef(_)
302 | QueryExpr::DropVcsRef(_)
303 | QueryExpr::ForkStore(_)
304 | QueryExpr::PromoteFork(_)
305 | QueryExpr::DropFork(_)
306 | QueryExpr::GraphCommand(_)
307 | QueryExpr::SearchCommand(_)
308 | QueryExpr::CreateIndex(_)
309 | QueryExpr::DropIndex(_)
310 | QueryExpr::ProbabilisticCommand(_)
311 | QueryExpr::Ask(_)
312 | QueryExpr::SetConfig { .. }
313 | QueryExpr::ShowConfig { .. }
314 | QueryExpr::Scrub { .. }
315 | QueryExpr::SetSecret { .. }
316 | QueryExpr::DeleteSecret { .. }
317 | QueryExpr::ShowSecrets { .. }
318 | QueryExpr::SetKv { .. }
319 | QueryExpr::DeleteKv { .. }
320 | QueryExpr::SetTenant(_)
321 | QueryExpr::ShowTenant
322 | QueryExpr::CreateTimeSeries(_)
323 | QueryExpr::CreateMetric(_)
324 | QueryExpr::AlterMetric(_)
325 | QueryExpr::CreateSlo(_)
326 | QueryExpr::DropTimeSeries(_)
327 | QueryExpr::CreateQueue(_)
328 | QueryExpr::AlterQueue(_)
329 | QueryExpr::DropQueue(_)
330 | QueryExpr::QueueSelect(_)
331 | QueryExpr::QueueCommand(_)
332 | QueryExpr::KvCommand(_)
333 | QueryExpr::ConfigCommand(_)
334 | QueryExpr::CreateTree(_)
335 | QueryExpr::DropTree(_)
336 | QueryExpr::TreeCommand(_)
337 | QueryExpr::ExplainAlter(_)
338 | QueryExpr::TransactionControl(_)
339 | QueryExpr::MaintenanceCommand(_)
340 | QueryExpr::VcsCommand(_)
341 | QueryExpr::CreateSchema(_)
342 | QueryExpr::DropSchema(_)
343 | QueryExpr::CreateSequence(_)
344 | QueryExpr::DropSequence(_)
345 | QueryExpr::CopyFrom(_)
346 | QueryExpr::CreateView(_)
347 | QueryExpr::DropView(_)
348 | QueryExpr::RefreshMaterializedView(_)
349 | QueryExpr::CreatePolicy(_)
350 | QueryExpr::DropPolicy(_)
351 | QueryExpr::CreateServer(_)
352 | QueryExpr::DropServer(_)
353 | QueryExpr::CreateForeignTable(_)
354 | QueryExpr::DropForeignTable(_)
355 | QueryExpr::Grant(_)
356 | QueryExpr::Revoke(_)
357 | QueryExpr::AlterUser(_)
358 | QueryExpr::CreateUser(_)
359 | QueryExpr::CreateIamPolicy { .. }
360 | QueryExpr::DropIamPolicy { .. }
361 | QueryExpr::AttachPolicy { .. }
362 | QueryExpr::DetachPolicy { .. }
363 | QueryExpr::ShowPolicies { .. }
364 | QueryExpr::ShowEffectivePermissions { .. }
365 | QueryExpr::RankOf(_)
366 | QueryExpr::ApproxRankOf(_)
367 | QueryExpr::RankRange(_)
368 | QueryExpr::SimulatePolicy { .. }
369 | QueryExpr::LintPolicy { .. }
370 | QueryExpr::MigratePolicyMode { .. }
371 | QueryExpr::CreateMigration(_)
372 | QueryExpr::ApplyMigration(_)
373 | QueryExpr::RollbackMigration(_)
374 | QueryExpr::ExplainMigration(_)
375 | QueryExpr::EventsBackfill(_)
376 | QueryExpr::EventsBackfillStatus { .. } => 1.0,
377 }
378 }
379}
380
381struct IndexSelectionPass;
394
395impl OptimizationPass for IndexSelectionPass {
396 fn name(&self) -> &str {
397 "IndexSelection"
398 }
399
400 fn apply(&self, query: QueryExpr) -> QueryExpr {
401 match query {
402 QueryExpr::Table(mut tq) => {
403 if let Some(filter) = effective_table_filter(&tq).as_ref() {
404 if let Some(hint) = Self::analyze_filter(filter) {
405 let expand = tq.expand.get_or_insert_with(Default::default);
407 expand.index_hint = Some(hint);
408 }
409 }
410 QueryExpr::Table(tq)
411 }
412 other => other,
413 }
414 }
415
416 fn benefit(&self) -> u32 {
417 70
418 }
419}
420
421impl IndexSelectionPass {
422 fn analyze_filter(filter: &crate::ast::Filter) -> Option<IndexHint> {
424 match filter {
425 crate::ast::Filter::Compare { field, op, .. } if *op == crate::ast::CompareOp::Eq => {
427 let col = Self::field_name(field);
428 Some(IndexHint {
429 method: IndexHintMethod::Hash,
430 column: col,
431 })
432 }
433 crate::ast::Filter::Compare {
435 field,
436 op:
437 crate::ast::CompareOp::Lt
438 | crate::ast::CompareOp::Le
439 | crate::ast::CompareOp::Gt
440 | crate::ast::CompareOp::Ge,
441 ..
442 } => {
443 let col = Self::field_name(field);
444 Some(IndexHint {
445 method: IndexHintMethod::BTree,
446 column: col,
447 })
448 }
449 crate::ast::Filter::Between { field, .. } => {
451 let col = Self::field_name(field);
452 Some(IndexHint {
453 method: IndexHintMethod::BTree,
454 column: col,
455 })
456 }
457 crate::ast::Filter::In { field, values } if values.len() <= 10 => {
459 let col = Self::field_name(field);
460 Some(IndexHint {
461 method: IndexHintMethod::Bitmap,
462 column: col,
463 })
464 }
465 crate::ast::Filter::And(left, right) => {
467 Self::analyze_filter(left).or_else(|| Self::analyze_filter(right))
468 }
469 _ => None,
470 }
471 }
472
473 fn field_name(field: &crate::ast::FieldRef) -> String {
474 match field {
475 crate::ast::FieldRef::TableColumn { column, .. } => column.clone(),
476 crate::ast::FieldRef::NodeProperty { property, .. } => property.clone(),
477 crate::ast::FieldRef::EdgeProperty { property, .. } => property.clone(),
478 crate::ast::FieldRef::NodeId { alias } => {
479 format!("{}.id", alias)
480 }
481 }
482 }
483}
484
485pub use reddb_types::index_hint::{IndexHint, IndexHintMethod};
490
491struct LimitPushdownPass;
493
494impl OptimizationPass for LimitPushdownPass {
495 fn name(&self) -> &str {
496 "LimitPushdown"
497 }
498
499 fn apply(&self, query: QueryExpr) -> QueryExpr {
500 match query {
501 QueryExpr::Join(jq) => {
502 let left = self.apply(*jq.left);
504 let right = self.apply(*jq.right);
505
506 QueryExpr::Join(JoinQuery {
507 left: Box::new(left),
508 right: Box::new(right),
509 ..jq
510 })
511 }
512 other => other,
513 }
514 }
515
516 fn benefit(&self) -> u32 {
517 60
518 }
519}
520
521fn swap_condition(condition: crate::ast::JoinCondition) -> crate::ast::JoinCondition {
526 crate::ast::JoinCondition {
527 left_field: condition.right_field,
528 right_field: condition.left_field,
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::ast::{
536 CompareOp, DistanceMetric, FieldRef, Filter, FusionStrategy, JoinCondition, Projection,
537 TableQuery,
538 };
539 use reddb_types::Value;
540
541 fn make_table_query(name: &str) -> QueryExpr {
542 QueryExpr::Table(TableQuery {
543 table: name.to_string(),
544 source: None,
545 alias: Some(name.to_string()),
546 select_items: Vec::new(),
547 columns: vec![Projection::All],
548 where_expr: None,
549 filter: None,
550 group_by_exprs: Vec::new(),
551 group_by: Vec::new(),
552 having_expr: None,
553 having: None,
554 order_by: vec![],
555 limit: None,
556 limit_param: None,
557 offset: None,
558 offset_param: None,
559 expand: None,
560 as_of: None,
561 sessionize: None,
562 distinct: false,
563 })
564 }
565
566 #[test]
567 fn test_optimizer_applies_passes() {
568 let optimizer = QueryOptimizer::new();
569 let query = make_table_query("hosts");
570
571 let (optimized, passes) = optimizer.optimize(query);
572 assert!(matches!(optimized, QueryExpr::Table(_)));
574 }
575
576 #[test]
577 fn test_join_reordering() {
578 let optimizer = QueryOptimizer::new();
579
580 let small = QueryExpr::Table(TableQuery {
581 table: "small".to_string(),
582 source: None,
583 alias: None,
584 select_items: Vec::new(),
585 columns: vec![Projection::All],
586 where_expr: None,
587 filter: None,
588 group_by_exprs: Vec::new(),
589 group_by: Vec::new(),
590 having_expr: None,
591 having: None,
592 order_by: vec![],
593 limit: Some(10), limit_param: None,
595 offset: None,
596 offset_param: None,
597 expand: None,
598 as_of: None,
599 sessionize: None,
600 distinct: false,
601 });
602
603 let large = QueryExpr::Table(TableQuery {
604 table: "large".to_string(),
605 source: None,
606 alias: None,
607 select_items: Vec::new(),
608 columns: vec![Projection::All],
609 where_expr: None,
610 filter: None,
611 group_by_exprs: Vec::new(),
612 group_by: Vec::new(),
613 having_expr: None,
614 having: None,
615 order_by: vec![],
616 limit: None, limit_param: None,
618 offset: None,
619 offset_param: None,
620 expand: None,
621 as_of: None,
622 sessionize: None,
623 distinct: false,
624 });
625
626 let join = QueryExpr::Join(JoinQuery {
627 left: Box::new(large.clone()),
628 right: Box::new(small.clone()),
629 join_type: JoinType::Inner,
630 on: JoinCondition {
631 left_field: FieldRef::TableColumn {
632 table: "large".to_string(),
633 column: "id".to_string(),
634 },
635 right_field: FieldRef::TableColumn {
636 table: "small".to_string(),
637 column: "id".to_string(),
638 },
639 },
640 filter: None,
641 order_by: Vec::new(),
642 limit: None,
643 offset: None,
644 return_items: Vec::new(),
645 return_: Vec::new(),
646 });
647
648 let (optimized, passes) = optimizer.optimize(join);
649
650 assert!(passes.iter().any(|pass| pass == "JoinReordering"));
652 if let QueryExpr::Join(jq) = optimized {
653 if let QueryExpr::Table(left) = jq.left.as_ref() {
655 assert_eq!(left.table, "small");
656 }
657 assert!(matches!(
658 &jq.on.left_field,
659 FieldRef::TableColumn { table, column } if table == "small" && column == "id"
660 ));
661 }
662 }
663
664 #[test]
665 fn optimize_with_hints_can_disable_join_reordering() {
666 let optimizer = QueryOptimizer::new();
667
668 let large = make_table_query("large");
669 let mut small_table = TableQuery::new("small");
670 small_table.limit = Some(1);
671 let small = QueryExpr::Table(small_table);
672
673 let join = QueryExpr::Join(JoinQuery {
674 left: Box::new(large),
675 right: Box::new(small),
676 join_type: JoinType::Inner,
677 on: JoinCondition {
678 left_field: FieldRef::TableColumn {
679 table: "large".to_string(),
680 column: "id".to_string(),
681 },
682 right_field: FieldRef::TableColumn {
683 table: "small".to_string(),
684 column: "id".to_string(),
685 },
686 },
687 filter: None,
688 order_by: Vec::new(),
689 limit: None,
690 offset: None,
691 return_items: Vec::new(),
692 return_: Vec::new(),
693 });
694 let hints = OptimizationHints {
695 disabled_passes: vec!["JoinReordering".to_string()],
696 ..OptimizationHints::default()
697 };
698
699 let optimized = optimizer.optimize_with_hints(join, &hints);
700 let QueryExpr::Join(join) = optimized else {
701 panic!("expected join query");
702 };
703 let QueryExpr::Table(left) = join.left.as_ref() else {
704 panic!("expected table on left side");
705 };
706
707 assert_eq!(left.table, "large");
708 assert!(matches!(
709 &join.on.left_field,
710 FieldRef::TableColumn { table, column } if table == "large" && column == "id"
711 ));
712 }
713
714 #[test]
715 fn optimizer_sets_index_hint_on_table_filters() {
716 let optimizer = QueryOptimizer::new();
717 let mut table = TableQuery::new("hosts");
718 table.filter = Some(Filter::Compare {
719 field: FieldRef::column("", "host_id"),
720 op: CompareOp::Eq,
721 value: Value::Integer(7),
722 });
723
724 let (optimized, passes) = optimizer.optimize(QueryExpr::Table(table));
725 let QueryExpr::Table(table) = optimized else {
726 panic!("expected table query");
727 };
728 let hint = table
729 .expand
730 .and_then(|expand| expand.index_hint)
731 .expect("expected optimizer index hint");
732
733 assert!(passes.iter().any(|pass| pass == "IndexSelection"));
734 assert_eq!(hint.method, IndexHintMethod::Hash);
735 assert_eq!(hint.column, "host_id");
736 }
737
738 #[test]
739 fn index_selection_analyzes_supported_filter_shapes() {
740 let range = IndexSelectionPass::analyze_filter(&Filter::Between {
741 field: FieldRef::node_prop("n", "score"),
742 low: Value::Integer(1),
743 high: Value::Integer(9),
744 })
745 .expect("expected range hint");
746 assert_eq!(range.method, IndexHintMethod::BTree);
747 assert_eq!(range.column, "score");
748
749 let bitmap = IndexSelectionPass::analyze_filter(&Filter::In {
750 field: FieldRef::edge_prop("e", "kind"),
751 values: vec![Value::text("http"), Value::text("ssh")],
752 })
753 .expect("expected bitmap hint");
754 assert_eq!(bitmap.method, IndexHintMethod::Bitmap);
755 assert_eq!(bitmap.column, "kind");
756
757 assert!(IndexSelectionPass::analyze_filter(&Filter::In {
758 field: FieldRef::column("", "status"),
759 values: (0..11).map(Value::Integer).collect(),
760 })
761 .is_none());
762
763 let fallback_right = IndexSelectionPass::analyze_filter(&Filter::And(
764 Box::new(Filter::IsNull(FieldRef::column("", "deleted_at"))),
765 Box::new(Filter::Compare {
766 field: FieldRef::node_id("n"),
767 op: CompareOp::Eq,
768 value: Value::Integer(1),
769 }),
770 ))
771 .expect("expected right-side AND hint");
772 assert_eq!(fallback_right.method, IndexHintMethod::Hash);
773 assert_eq!(fallback_right.column, "n.id");
774 }
775}