1use crate::storage::query::ast::{JoinQuery, JoinType, QueryExpr};
14use crate::storage::query::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::Insert(_)
287 | QueryExpr::Update(_)
288 | QueryExpr::Delete(_)
289 | QueryExpr::CreateTable(_)
290 | QueryExpr::DropTable(_)
291 | QueryExpr::DropGraph(_)
292 | QueryExpr::DropVector(_)
293 | QueryExpr::DropDocument(_)
294 | QueryExpr::DropKv(_)
295 | QueryExpr::DropCollection(_)
296 | QueryExpr::Truncate(_)
297 | QueryExpr::AlterTable(_)
298 | QueryExpr::GraphCommand(_)
299 | QueryExpr::SearchCommand(_)
300 | QueryExpr::CreateIndex(_)
301 | QueryExpr::DropIndex(_)
302 | QueryExpr::ProbabilisticCommand(_)
303 | QueryExpr::Ask(_)
304 | QueryExpr::SetConfig { .. }
305 | QueryExpr::ShowConfig { .. }
306 | QueryExpr::SetSecret { .. }
307 | QueryExpr::DeleteSecret { .. }
308 | QueryExpr::ShowSecrets { .. }
309 | QueryExpr::SetTenant(_)
310 | QueryExpr::ShowTenant
311 | QueryExpr::CreateTimeSeries(_)
312 | QueryExpr::DropTimeSeries(_)
313 | QueryExpr::CreateQueue(_)
314 | QueryExpr::AlterQueue(_)
315 | QueryExpr::DropQueue(_)
316 | QueryExpr::QueueSelect(_)
317 | QueryExpr::QueueCommand(_)
318 | QueryExpr::KvCommand(_)
319 | QueryExpr::ConfigCommand(_)
320 | QueryExpr::CreateTree(_)
321 | QueryExpr::DropTree(_)
322 | QueryExpr::TreeCommand(_)
323 | QueryExpr::ExplainAlter(_)
324 | QueryExpr::TransactionControl(_)
325 | QueryExpr::MaintenanceCommand(_)
326 | QueryExpr::CreateSchema(_)
327 | QueryExpr::DropSchema(_)
328 | QueryExpr::CreateSequence(_)
329 | QueryExpr::DropSequence(_)
330 | QueryExpr::CopyFrom(_)
331 | QueryExpr::CreateView(_)
332 | QueryExpr::DropView(_)
333 | QueryExpr::RefreshMaterializedView(_)
334 | QueryExpr::CreatePolicy(_)
335 | QueryExpr::DropPolicy(_)
336 | QueryExpr::CreateServer(_)
337 | QueryExpr::DropServer(_)
338 | QueryExpr::CreateForeignTable(_)
339 | QueryExpr::DropForeignTable(_)
340 | QueryExpr::Grant(_)
341 | QueryExpr::Revoke(_)
342 | QueryExpr::AlterUser(_)
343 | QueryExpr::CreateIamPolicy { .. }
344 | QueryExpr::DropIamPolicy { .. }
345 | QueryExpr::AttachPolicy { .. }
346 | QueryExpr::DetachPolicy { .. }
347 | QueryExpr::ShowPolicies { .. }
348 | QueryExpr::ShowEffectivePermissions { .. }
349 | QueryExpr::SimulatePolicy { .. }
350 | QueryExpr::CreateMigration(_)
351 | QueryExpr::ApplyMigration(_)
352 | QueryExpr::RollbackMigration(_)
353 | QueryExpr::ExplainMigration(_)
354 | QueryExpr::EventsBackfill(_)
355 | QueryExpr::EventsBackfillStatus { .. } => 1.0,
356 }
357 }
358}
359
360struct IndexSelectionPass;
373
374impl OptimizationPass for IndexSelectionPass {
375 fn name(&self) -> &str {
376 "IndexSelection"
377 }
378
379 fn apply(&self, query: QueryExpr) -> QueryExpr {
380 match query {
381 QueryExpr::Table(mut tq) => {
382 if let Some(filter) = effective_table_filter(&tq).as_ref() {
383 if let Some(hint) = Self::analyze_filter(filter) {
384 let expand = tq.expand.get_or_insert_with(Default::default);
386 expand.index_hint = Some(hint);
387 }
388 }
389 QueryExpr::Table(tq)
390 }
391 other => other,
392 }
393 }
394
395 fn benefit(&self) -> u32 {
396 70
397 }
398}
399
400impl IndexSelectionPass {
401 fn analyze_filter(filter: &crate::storage::query::ast::Filter) -> Option<IndexHint> {
403 match filter {
404 crate::storage::query::ast::Filter::Compare { field, op, .. }
406 if *op == crate::storage::query::ast::CompareOp::Eq =>
407 {
408 let col = Self::field_name(field);
409 Some(IndexHint {
410 method: IndexHintMethod::Hash,
411 column: col,
412 })
413 }
414 crate::storage::query::ast::Filter::Compare {
416 field,
417 op:
418 crate::storage::query::ast::CompareOp::Lt
419 | crate::storage::query::ast::CompareOp::Le
420 | crate::storage::query::ast::CompareOp::Gt
421 | crate::storage::query::ast::CompareOp::Ge,
422 ..
423 } => {
424 let col = Self::field_name(field);
425 Some(IndexHint {
426 method: IndexHintMethod::BTree,
427 column: col,
428 })
429 }
430 crate::storage::query::ast::Filter::Between { field, .. } => {
432 let col = Self::field_name(field);
433 Some(IndexHint {
434 method: IndexHintMethod::BTree,
435 column: col,
436 })
437 }
438 crate::storage::query::ast::Filter::In { field, values } if values.len() <= 10 => {
440 let col = Self::field_name(field);
441 Some(IndexHint {
442 method: IndexHintMethod::Bitmap,
443 column: col,
444 })
445 }
446 crate::storage::query::ast::Filter::And(left, right) => {
448 Self::analyze_filter(left).or_else(|| Self::analyze_filter(right))
449 }
450 _ => None,
451 }
452 }
453
454 fn field_name(field: &crate::storage::query::ast::FieldRef) -> String {
455 match field {
456 crate::storage::query::ast::FieldRef::TableColumn { column, .. } => column.clone(),
457 crate::storage::query::ast::FieldRef::NodeProperty { property, .. } => property.clone(),
458 crate::storage::query::ast::FieldRef::EdgeProperty { property, .. } => property.clone(),
459 crate::storage::query::ast::FieldRef::NodeId { alias } => {
460 format!("{}.id", alias)
461 }
462 }
463 }
464}
465
466#[derive(Debug, Clone)]
468pub struct IndexHint {
469 pub method: IndexHintMethod,
471 pub column: String,
473}
474
475#[derive(Debug, Clone, Copy, PartialEq, Eq)]
477pub enum IndexHintMethod {
478 Hash,
479 BTree,
480 Bitmap,
481 Spatial,
482}
483
484struct LimitPushdownPass;
486
487impl OptimizationPass for LimitPushdownPass {
488 fn name(&self) -> &str {
489 "LimitPushdown"
490 }
491
492 fn apply(&self, query: QueryExpr) -> QueryExpr {
493 match query {
494 QueryExpr::Join(jq) => {
495 let left = self.apply(*jq.left);
497 let right = self.apply(*jq.right);
498
499 QueryExpr::Join(JoinQuery {
500 left: Box::new(left),
501 right: Box::new(right),
502 ..jq
503 })
504 }
505 other => other,
506 }
507 }
508
509 fn benefit(&self) -> u32 {
510 60
511 }
512}
513
514fn swap_condition(
519 condition: crate::storage::query::ast::JoinCondition,
520) -> crate::storage::query::ast::JoinCondition {
521 crate::storage::query::ast::JoinCondition {
522 left_field: condition.right_field,
523 right_field: condition.left_field,
524 }
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530 use crate::storage::query::ast::{
531 DistanceMetric, FieldRef, FusionStrategy, JoinCondition, Projection, TableQuery,
532 };
533
534 fn make_table_query(name: &str) -> QueryExpr {
535 QueryExpr::Table(TableQuery {
536 table: name.to_string(),
537 source: None,
538 alias: Some(name.to_string()),
539 select_items: Vec::new(),
540 columns: vec![Projection::All],
541 where_expr: None,
542 filter: None,
543 group_by_exprs: Vec::new(),
544 group_by: Vec::new(),
545 having_expr: None,
546 having: None,
547 order_by: vec![],
548 limit: None,
549 limit_param: None,
550 offset: None,
551 offset_param: None,
552 expand: None,
553 as_of: None,
554 })
555 }
556
557 #[test]
558 fn test_optimizer_applies_passes() {
559 let optimizer = QueryOptimizer::new();
560 let query = make_table_query("hosts");
561
562 let (optimized, passes) = optimizer.optimize(query);
563 assert!(matches!(optimized, QueryExpr::Table(_)));
565 }
566
567 #[test]
568 fn test_join_reordering() {
569 let optimizer = QueryOptimizer::new();
570
571 let small = QueryExpr::Table(TableQuery {
572 table: "small".to_string(),
573 source: None,
574 alias: None,
575 select_items: Vec::new(),
576 columns: vec![Projection::All],
577 where_expr: None,
578 filter: None,
579 group_by_exprs: Vec::new(),
580 group_by: Vec::new(),
581 having_expr: None,
582 having: None,
583 order_by: vec![],
584 limit: Some(10), limit_param: None,
586 offset: None,
587 offset_param: None,
588 expand: None,
589 as_of: None,
590 });
591
592 let large = QueryExpr::Table(TableQuery {
593 table: "large".to_string(),
594 source: None,
595 alias: None,
596 select_items: Vec::new(),
597 columns: vec![Projection::All],
598 where_expr: None,
599 filter: None,
600 group_by_exprs: Vec::new(),
601 group_by: Vec::new(),
602 having_expr: None,
603 having: None,
604 order_by: vec![],
605 limit: None, limit_param: None,
607 offset: None,
608 offset_param: None,
609 expand: None,
610 as_of: None,
611 });
612
613 let join = QueryExpr::Join(JoinQuery {
614 left: Box::new(large.clone()),
615 right: Box::new(small.clone()),
616 join_type: JoinType::Inner,
617 on: JoinCondition {
618 left_field: FieldRef::TableColumn {
619 table: "large".to_string(),
620 column: "id".to_string(),
621 },
622 right_field: FieldRef::TableColumn {
623 table: "small".to_string(),
624 column: "id".to_string(),
625 },
626 },
627 filter: None,
628 order_by: Vec::new(),
629 limit: None,
630 offset: None,
631 return_items: Vec::new(),
632 return_: Vec::new(),
633 });
634
635 let (optimized, passes) = optimizer.optimize(join);
636
637 if let QueryExpr::Join(jq) = optimized {
639 if let QueryExpr::Table(left) = *jq.left {
641 assert_eq!(left.table, "small");
642 }
643 }
644 }
645}