Skip to main content

risingwave_frontend/optimizer/rule/
index_selection_rule.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! # Index selection cost matrix
16//!
17//! |`column_idx`| 0   |  1 | 2  | 3  | 4  | remark |
18//! |-----------|-----|----|----|----|----|---|
19//! |Equal      | 1   | 1  | 1  | 1  | 1  | |
20//! |In         | 10  | 8  | 5  | 5  | 5  | take the minimum value with actual in number |
21//! |Range(Two) | 600 | 50 | 20 | 10 | 10 | `RangeTwoSideBound` like a between 1 and 2 |
22//! |Range(One) | 1400| 70 | 25 | 15 | 10 | `RangeOneSideBound` like a > 1, a >= 1, a < 1|
23//! |All        | 4000| 100| 30 | 20 | 10 | |
24//!
25//! ```text
26//! index cost = cost(match type of 0 idx)
27//! * cost(match type of 1 idx)
28//! * ... cost(match type of the last idx)
29//! ```
30//!
31//! ## Example
32//!
33//! Given index order key (a, b, c)
34//!
35//! - For `a = 1 and b = 1 and c = 1`, its cost is 1 = Equal0 * Equal1 * Equal2 = 1
36//! - For `a in (xxx) and b = 1 and c = 1`, its cost is In0 * Equal1 * Equal2 = 10
37//! - For `a = 1 and b in (xxx)`, its cost is Equal0 * In1 * All2 = 1 * 8 * 50 = 400
38//! - For `a between xxx and yyy`, its cost is Range(Two)0 = 600
39//! - For `a = 1 and b between xxx and yyy`, its cost is Equal0 * Range(Two)1 = 50
40//! - For `a = 1 and b > 1`, its cost is Equal0 * Range(One)1 = 70
41//! - For `a = 1`, its cost is 100 = Equal0 * All1 = 100
42//! - For no condition, its cost is All0 = 4000
43//!
44//! With the assumption that the most effective part of a index is its prefix,
45//! cost decreases as `column_idx` increasing.
46//!
47//! For index order key length > 5, we just ignore the rest.
48
49use std::cmp::min;
50use std::collections::hash_map::Entry::{Occupied, Vacant};
51use std::collections::{BTreeMap, HashMap};
52use std::sync::Arc;
53
54use itertools::Itertools;
55use risingwave_common::array::VectorDistanceType;
56use risingwave_common::catalog::Schema;
57use risingwave_common::types::{
58    DataType, Date, Decimal, Int256, Interval, Serial, Time, Timestamp, Timestamptz,
59};
60use risingwave_common::util::iter_util::ZipEqFast;
61use risingwave_pb::plan_common::JoinType;
62use risingwave_sqlparser::ast::AsOf;
63
64use super::prelude::{PlanRef, *};
65use crate::catalog::index_catalog::TableIndex;
66use crate::expr::{
67    Expr, ExprImpl, ExprRewriter, ExprType, ExprVisitor, FunctionCall, InputRef, to_conjunctions,
68    to_disjunctions,
69};
70use crate::optimizer::optimizer_context::OptimizerContextRef;
71use crate::optimizer::plan_node::generic::GenericPlanRef;
72use crate::optimizer::plan_node::{
73    ColumnPruningContext, LogicalJoin, LogicalScan, LogicalUnion, PlanTreeNode, PlanTreeNodeBinary,
74    PredicatePushdown, PredicatePushdownContext, generic,
75};
76use crate::utils::Condition;
77
78const INDEX_MAX_LEN: usize = 5;
79const INDEX_COST_MATRIX: [[usize; INDEX_MAX_LEN]; 5] = [
80    [1, 1, 1, 1, 1],
81    [10, 8, 5, 5, 5],
82    [600, 50, 20, 10, 10],
83    [1400, 70, 25, 15, 10],
84    [4000, 100, 30, 20, 20],
85];
86const LOOKUP_COST_CONST: usize = 3;
87const MAX_COMBINATION_SIZE: usize = 4;
88const MAX_CONJUNCTION_SIZE: usize = 8;
89
90pub struct IndexSelectionRule {}
91
92impl Rule<Logical> for IndexSelectionRule {
93    fn apply(&self, plan: PlanRef) -> Option<PlanRef> {
94        let logical_scan: &LogicalScan = plan.as_logical_scan()?;
95        let indexes = logical_scan.table_indexes();
96        if indexes.is_empty() {
97            return None;
98        }
99        let primary_table_row_size = TableScanIoEstimator::estimate_row_size(logical_scan);
100        let primary_cost = min(
101            self.estimate_table_scan_cost(logical_scan, primary_table_row_size),
102            self.estimate_full_table_scan_cost(logical_scan, primary_table_row_size),
103        );
104
105        // If it is a primary lookup plan, avoid checking other indexes.
106        if primary_cost.primary_lookup {
107            return None;
108        }
109
110        let mut final_plan: PlanRef = logical_scan.clone().into();
111        let mut min_cost = primary_cost.clone();
112
113        for index in indexes {
114            if let Some(index_scan) = logical_scan.to_index_scan_if_index_covered(index) {
115                let index_cost = self.estimate_table_scan_cost(
116                    &index_scan,
117                    TableScanIoEstimator::estimate_row_size(&index_scan),
118                );
119
120                if index_cost.le(&min_cost) {
121                    min_cost = index_cost;
122                    final_plan = index_scan.into();
123                }
124            } else {
125                // non-covering index selection
126                let (index_lookup, lookup_cost) = self.gen_index_lookup(logical_scan, index);
127                if lookup_cost.le(&min_cost) {
128                    min_cost = lookup_cost;
129                    final_plan = index_lookup;
130                }
131            }
132        }
133
134        if let Some((merge_index, merge_index_cost)) = self.index_merge_selection(logical_scan)
135            && merge_index_cost.le(&min_cost)
136        {
137            min_cost = merge_index_cost;
138            final_plan = merge_index;
139        }
140
141        if min_cost == primary_cost {
142            None
143        } else {
144            Some(final_plan)
145        }
146    }
147}
148
149struct IndexPredicateRewriter<'a> {
150    p2s_mapping: &'a BTreeMap<usize, usize>,
151    function_mapping: &'a HashMap<FunctionCall, usize>,
152    offset: usize,
153    covered_by_index: bool,
154}
155
156impl<'a> IndexPredicateRewriter<'a> {
157    fn new(
158        p2s_mapping: &'a BTreeMap<usize, usize>,
159        function_mapping: &'a HashMap<FunctionCall, usize>,
160        offset: usize,
161    ) -> Self {
162        Self {
163            p2s_mapping,
164            function_mapping,
165            offset,
166            covered_by_index: true,
167        }
168    }
169
170    fn covered_by_index(&self) -> bool {
171        self.covered_by_index
172    }
173}
174
175impl ExprRewriter for IndexPredicateRewriter<'_> {
176    fn rewrite_input_ref(&mut self, input_ref: InputRef) -> ExprImpl {
177        // transform primary predicate to index predicate if it can
178        if self.p2s_mapping.contains_key(&input_ref.index) {
179            InputRef::new(
180                *self.p2s_mapping.get(&input_ref.index()).unwrap(),
181                input_ref.return_type(),
182            )
183            .into()
184        } else {
185            self.covered_by_index = false;
186            InputRef::new(input_ref.index() + self.offset, input_ref.return_type()).into()
187        }
188    }
189
190    fn rewrite_function_call(&mut self, func_call: FunctionCall) -> ExprImpl {
191        if let Some(index) = self.function_mapping.get(&func_call) {
192            return InputRef::new(*index, func_call.return_type()).into();
193        }
194
195        let (func_type, inputs, ret) = func_call.decompose();
196        let inputs = inputs
197            .into_iter()
198            .map(|expr| self.rewrite_expr(expr))
199            .collect();
200        FunctionCall::new_unchecked(func_type, inputs, ret).into()
201    }
202}
203
204impl IndexSelectionRule {
205    fn gen_index_lookup(
206        &self,
207        logical_scan: &LogicalScan,
208        index: &TableIndex,
209    ) -> (PlanRef, IndexCost) {
210        // 1. logical_scan ->  logical_join
211        //                      /        \
212        //                index_scan   primary_table_scan
213        let index_scan = LogicalScan::create(
214            index.index_table.clone(),
215            logical_scan.ctx(),
216            logical_scan.as_of(),
217        );
218        // We use `schema.len` instead of `index_item.len` here,
219        // because schema contains system columns like `_rw_timestamp` column which is not represented in the index item.
220        let offset = index_scan.table().columns().len();
221
222        let primary_table_scan = LogicalScan::create(
223            index.primary_table.clone(),
224            logical_scan.ctx(),
225            logical_scan.as_of(),
226        );
227
228        let predicate = logical_scan.predicate().clone();
229        let mut rewriter = IndexPredicateRewriter::new(
230            index.primary_to_secondary_mapping(),
231            index.function_mapping(),
232            offset,
233        );
234        let new_predicate = predicate.rewrite_expr(&mut rewriter);
235
236        let conjunctions = index
237            .primary_table_pk_ref_to_index_table()
238            .iter()
239            .zip_eq_fast(index.primary_table.pk.iter())
240            .map(|(x, y)| {
241                Self::create_null_safe_equal_expr(
242                    x.column_index,
243                    index.index_table.columns[x.column_index]
244                        .data_type()
245                        .clone(),
246                    y.column_index + offset,
247                    index.primary_table.columns[y.column_index]
248                        .data_type()
249                        .clone(),
250                )
251            })
252            .chain(new_predicate)
253            .collect_vec();
254        let on = Condition { conjunctions };
255        let join: PlanRef = LogicalJoin::new(
256            index_scan.into(),
257            primary_table_scan.into(),
258            JoinType::Inner,
259            on,
260        )
261        .into();
262
263        // 2. push down predicate, so we can calculate the cost of index lookup
264        let join_ref = join.predicate_pushdown(
265            Condition::true_cond(),
266            &mut PredicatePushdownContext::new(join.clone()),
267        );
268
269        let join_with_predicate_push_down =
270            join_ref.as_logical_join().expect("must be a logical join");
271        let new_join_left = join_with_predicate_push_down.left();
272        let index_scan_with_predicate: &LogicalScan = new_join_left
273            .as_logical_scan()
274            .expect("must be a logical scan");
275
276        // 3. calculate index cost, index lookup use primary table to estimate row size.
277        let index_cost = self.estimate_table_scan_cost(
278            index_scan_with_predicate,
279            TableScanIoEstimator::estimate_row_size(logical_scan),
280        );
281        // lookup cost = index cost * LOOKUP_COST_CONST
282        let lookup_cost = index_cost.mul(&IndexCost::new(LOOKUP_COST_CONST, false));
283
284        // 4. keep the same schema with original logical_scan
285        let scan_output_col_idx = logical_scan.output_col_idx();
286        let lookup_join = join_ref.prune_col(
287            &scan_output_col_idx
288                .iter()
289                .map(|&col_idx| col_idx + offset)
290                .collect_vec(),
291            &mut ColumnPruningContext::new(join_ref.clone()),
292        );
293
294        (lookup_join, lookup_cost)
295    }
296
297    /// Index Merge Selection
298    /// Deal with predicate like a = 1 or b = 1
299    /// Merge index scans from a table, currently merge is union semantic.
300    fn index_merge_selection(&self, logical_scan: &LogicalScan) -> Option<(PlanRef, IndexCost)> {
301        let predicate = logical_scan.predicate().clone();
302        // Index merge is kind of index lookup join so use primary table row size to estimate index
303        // cost.
304        let primary_table_row_size = TableScanIoEstimator::estimate_row_size(logical_scan);
305        // 1. choose lowest cost index merge path
306        let paths = self.gen_paths(
307            &predicate.conjunctions,
308            logical_scan,
309            primary_table_row_size,
310        );
311        let (index_access, index_access_cost) =
312            self.choose_min_cost_path(&paths, primary_table_row_size)?;
313
314        // 2. lookup primary table
315        // the schema of index_access is the order key of primary table .
316        let schema: &Schema = index_access.schema();
317        let index_access_len = schema.len();
318
319        let mut shift_input_ref_rewriter = ShiftInputRefRewriter {
320            offset: index_access_len,
321        };
322        let new_predicate = predicate.rewrite_expr(&mut shift_input_ref_rewriter);
323
324        let primary_table = logical_scan.table();
325
326        let primary_table_scan = LogicalScan::create(
327            logical_scan.table().clone(),
328            logical_scan.ctx(),
329            logical_scan.as_of(),
330        );
331
332        let conjunctions = primary_table
333            .pk
334            .iter()
335            .enumerate()
336            .map(|(x, y)| {
337                Self::create_null_safe_equal_expr(
338                    x,
339                    schema.fields[x].data_type.clone(),
340                    y.column_index + index_access_len,
341                    primary_table.columns[y.column_index].data_type.clone(),
342                )
343            })
344            .chain(new_predicate)
345            .collect_vec();
346
347        let on = Condition { conjunctions };
348        let join: PlanRef =
349            LogicalJoin::new(index_access, primary_table_scan.into(), JoinType::Inner, on).into();
350
351        // 3 push down predicate
352        let join_ref = join.predicate_pushdown(
353            Condition::true_cond(),
354            &mut PredicatePushdownContext::new(join.clone()),
355        );
356
357        // 4. keep the same schema with original logical_scan
358        let scan_output_col_idx = logical_scan.output_col_idx();
359        let lookup_join = join_ref.prune_col(
360            &scan_output_col_idx
361                .iter()
362                .map(|&col_idx| col_idx + index_access_len)
363                .collect_vec(),
364            &mut ColumnPruningContext::new(join_ref.clone()),
365        );
366
367        Some((
368            lookup_join,
369            index_access_cost.mul(&IndexCost::new(LOOKUP_COST_CONST, false)),
370        ))
371    }
372
373    /// Generate possible paths that can be used to access.
374    /// The schema of output is the order key of primary table, so it can be used to lookup primary
375    /// table later.
376    /// Method `gen_paths` handles the complex condition recursively which may contains nested `AND`
377    /// and `OR`. However, Method `gen_index_path` handles one arm of an OR clause which is a
378    /// basic unit for index selection.
379    fn gen_paths(
380        &self,
381        conjunctions: &[ExprImpl],
382        logical_scan: &LogicalScan,
383        primary_table_row_size: usize,
384    ) -> Vec<PlanRef> {
385        let mut result = vec![];
386
387        // split by OR clause, the not_or_conjunctions could be used to generate index path by combining with each arm of OR clause.
388        let (or_conjunctions, not_or_conjunctions): (Vec<ExprImpl>, Vec<ExprImpl>) =
389            conjunctions.iter().cloned().partition(|expr| {
390                if let ExprImpl::FunctionCall(function_call) = expr
391                    && function_call.func_type() == ExprType::Or
392                {
393                    true
394                } else {
395                    false
396                }
397            });
398        // Only consider eq ,in and cmp condition for not_or_conjunctions
399        let interest_conjunctions: Vec<ExprImpl> = not_or_conjunctions
400            .into_iter()
401            .filter(|expr| {
402                expr.as_eq_const().is_some()
403                    || expr
404                        .as_in_const_list()
405                        .or_else(|| expr.as_some_eq_const_list())
406                        .is_some()
407                    || expr.as_comparison_const().is_some()
408            })
409            .collect();
410
411        for expr in or_conjunctions {
412            // it must be OR clause!
413            let mut index_to_be_merged = vec![];
414
415            let disjunctions = to_disjunctions(expr.clone());
416
417            let extended_disjunctions = disjunctions
418                .into_iter()
419                .map(|expr| {
420                    if interest_conjunctions.is_empty() {
421                        expr
422                    } else {
423                        ExprImpl::FunctionCall(
424                            FunctionCall::new_unchecked(
425                                ExprType::And,
426                                vec![expr]
427                                    .into_iter()
428                                    .chain(interest_conjunctions.iter().cloned())
429                                    .collect(),
430                                DataType::Boolean,
431                            )
432                            .into(),
433                        )
434                    }
435                })
436                .collect_vec();
437
438            let (map, others) = self.clustering_disjunction(extended_disjunctions);
439            let iter = map
440                .into_iter()
441                .map(|(column_index, expr)| (Some(column_index), expr))
442                .chain(others.into_iter().map(|expr| (None, expr)));
443            for (column_index, expr) in iter {
444                let mut index_paths = vec![];
445                let conjunctions = to_conjunctions(expr);
446                index_paths.extend(self.gen_index_path(column_index, &conjunctions, logical_scan));
447                // complex condition, recursively gen paths
448                if conjunctions.len() > 1 {
449                    index_paths.extend(self.gen_paths(
450                        &conjunctions,
451                        logical_scan,
452                        primary_table_row_size,
453                    ));
454                }
455
456                match self.choose_min_cost_path(&index_paths, primary_table_row_size) {
457                    None => {
458                        // One arm of OR clause can't use index, bail out
459                        index_to_be_merged.clear();
460                        break;
461                    }
462                    Some((path, _)) => index_to_be_merged.push(path),
463                }
464            }
465
466            if let Some(path) = self.merge(index_to_be_merged) {
467                result.push(path)
468            }
469        }
470
471        result
472    }
473
474    /// Clustering disjunction or expr by column index. If expr is complex, classify them as others.
475    ///
476    /// a = 1, b = 2, b = 3 -> map: [a, (a = 1)], [b, (b = 2 or b = 3)], others: []
477    ///
478    /// a = 1, (b = 2 and c = 3) -> map: [a, (a = 1)], others:
479    ///
480    /// (a > 1 and a < 8) or (c > 1 and c < 8)
481    /// -> map: [], others: [(a > 1 and a < 8), (c > 1 and c < 8)]
482    fn clustering_disjunction(
483        &self,
484        disjunctions: Vec<ExprImpl>,
485    ) -> (HashMap<usize, ExprImpl>, Vec<ExprImpl>) {
486        let mut map: HashMap<usize, ExprImpl> = HashMap::new();
487        let mut others = vec![];
488        for expr in disjunctions {
489            let idx = {
490                if let Some((input_ref, _const_expr)) = expr.as_eq_const() {
491                    Some(input_ref.index)
492                } else if let Some((input_ref, _in_const_list)) = expr
493                    .as_in_const_list()
494                    .or_else(|| expr.as_some_eq_const_list())
495                {
496                    Some(input_ref.index)
497                } else if let Some((input_ref, _op, _const_expr)) = expr.as_comparison_const() {
498                    Some(input_ref.index)
499                } else {
500                    None
501                }
502            };
503
504            if let Some(idx) = idx {
505                match map.entry(idx) {
506                    Occupied(mut entry) => {
507                        let expr2: ExprImpl = entry.get().to_owned();
508                        let or_expr = ExprImpl::FunctionCall(
509                            FunctionCall::new_unchecked(
510                                ExprType::Or,
511                                vec![expr, expr2],
512                                DataType::Boolean,
513                            )
514                            .into(),
515                        );
516                        entry.insert(or_expr);
517                    }
518                    Vacant(entry) => {
519                        entry.insert(expr);
520                    }
521                };
522            } else {
523                others.push(expr);
524                continue;
525            }
526        }
527
528        (map, others)
529    }
530
531    /// Given a conjunctions from one arm of an OR clause (basic unit to index selection), generate
532    /// all matching index path (including primary index) for the relation.
533    /// `column_index` (refers to primary table) is a hint can be used to prune index.
534    /// Steps:
535    /// 1. Take the combination of `conjunctions` to extract the potential clauses.
536    /// 2. For each potential clauses, generate index path if it can.
537    fn gen_index_path(
538        &self,
539        column_index: Option<usize>,
540        conjunctions: &[ExprImpl],
541        logical_scan: &LogicalScan,
542    ) -> Vec<PlanRef> {
543        // Assumption: use at most `MAX_COMBINATION_SIZE` clauses, we can determine which is the
544        // best index.
545        let combinations = conjunctions
546            .iter()
547            .take(min(conjunctions.len(), MAX_CONJUNCTION_SIZE))
548            .combinations(min(conjunctions.len(), MAX_COMBINATION_SIZE))
549            .collect_vec();
550
551        let mut result = vec![];
552
553        for index in logical_scan.table_indexes() {
554            if let Some(column_index) = column_index {
555                assert_eq!(conjunctions.len(), 1);
556                let p2s_mapping = index.primary_to_secondary_mapping();
557                match p2s_mapping.get(&column_index) {
558                    None => continue, // not found, prune this index
559                    Some(&idx) => {
560                        if index.index_table.pk()[0].column_index != idx {
561                            // not match, prune this index
562                            continue;
563                        }
564                    }
565                }
566            }
567
568            // try secondary index
569            for conj in &combinations {
570                let condition = Condition {
571                    conjunctions: conj.iter().map(|&x| x.to_owned()).collect(),
572                };
573                if let Some(index_access) = self.build_index_access(
574                    index.clone(),
575                    condition,
576                    logical_scan.ctx().clone(),
577                    logical_scan.as_of().clone(),
578                ) {
579                    result.push(index_access);
580                }
581            }
582        }
583
584        // try primary index
585        let primary_table = logical_scan.table();
586        if let Some(idx) = column_index {
587            assert_eq!(conjunctions.len(), 1);
588            if primary_table.pk[0].column_index != idx {
589                return result;
590            }
591        }
592
593        let primary_access = generic::TableScan::new(
594            primary_table
595                .pk
596                .iter()
597                .map(|x| x.column_index)
598                .collect_vec(),
599            logical_scan.table().clone(),
600            vec![],
601            vec![],
602            logical_scan.ctx(),
603            Condition {
604                conjunctions: conjunctions.to_vec(),
605            },
606            logical_scan.as_of(),
607        );
608
609        result.push(primary_access.into());
610
611        result
612    }
613
614    /// build index access if predicate (refers to primary table) is covered by index
615    fn build_index_access(
616        &self,
617        index: Arc<TableIndex>,
618        predicate: Condition,
619        ctx: OptimizerContextRef,
620        as_of: Option<AsOf>,
621    ) -> Option<PlanRef> {
622        let mut rewriter = IndexPredicateRewriter::new(
623            index.primary_to_secondary_mapping(),
624            index.function_mapping(),
625            0,
626        );
627        let new_predicate = predicate.rewrite_expr(&mut rewriter);
628
629        // check condition is covered by index.
630        if !rewriter.covered_by_index() {
631            return None;
632        }
633
634        Some(
635            generic::TableScan::new(
636                index
637                    .primary_table_pk_ref_to_index_table()
638                    .iter()
639                    .map(|x| x.column_index)
640                    .collect_vec(),
641                index.index_table.clone(),
642                vec![],
643                vec![],
644                ctx,
645                new_predicate,
646                as_of,
647            )
648            .into(),
649        )
650    }
651
652    fn merge(&self, paths: Vec<PlanRef>) -> Option<PlanRef> {
653        if paths.is_empty() {
654            return None;
655        }
656
657        let new_paths = paths
658            .iter()
659            .flat_map(|path| {
660                if let Some(union) = path.as_logical_union() {
661                    union.inputs().to_vec()
662                } else if let Some(_scan) = path.as_logical_scan() {
663                    vec![path.clone()]
664                } else {
665                    unreachable!();
666                }
667            })
668            .sorted_by(|a, b| {
669                // sort inputs to make plan deterministic
670                a.as_logical_scan()
671                    .expect("expect to be a logical scan")
672                    .table_name()
673                    .cmp(
674                        b.as_logical_scan()
675                            .expect("expect to be a logical scan")
676                            .table_name(),
677                    )
678            })
679            .collect_vec();
680
681        Some(LogicalUnion::create(false, new_paths))
682    }
683
684    fn choose_min_cost_path(
685        &self,
686        paths: &[PlanRef],
687        primary_table_row_size: usize,
688    ) -> Option<(PlanRef, IndexCost)> {
689        paths
690            .iter()
691            .map(|path| {
692                if let Some(scan) = path.as_logical_scan() {
693                    let cost = self.estimate_table_scan_cost(scan, primary_table_row_size);
694                    (scan.clone().into(), cost)
695                } else if let Some(union) = path.as_logical_union() {
696                    let cost = union
697                        .inputs()
698                        .iter()
699                        .map(|input| {
700                            self.estimate_table_scan_cost(
701                                input.as_logical_scan().expect("expect to be a scan"),
702                                primary_table_row_size,
703                            )
704                        })
705                        .reduce(|a, b| a.add(&b))
706                        .unwrap();
707                    (union.clone().into(), cost)
708                } else {
709                    unreachable!()
710                }
711            })
712            .min_by(|(_, cost1), (_, cost2)| Ord::cmp(cost1, cost2))
713    }
714
715    pub(crate) fn estimate_table_scan_cost(
716        &self,
717        scan: &LogicalScan,
718        row_size: usize,
719    ) -> IndexCost {
720        let mut table_scan_io_estimator = TableScanIoEstimator::new(scan, row_size);
721        table_scan_io_estimator.estimate(scan.predicate())
722    }
723
724    pub(crate) fn estimate_full_table_scan_cost(
725        &self,
726        scan: &LogicalScan,
727        row_size: usize,
728    ) -> IndexCost {
729        let mut table_scan_io_estimator = TableScanIoEstimator::new(scan, row_size);
730        table_scan_io_estimator.estimate(&Condition::true_cond())
731    }
732
733    pub fn create_null_safe_equal_expr(
734        left: usize,
735        left_data_type: DataType,
736        right: usize,
737        right_data_type: DataType,
738    ) -> ExprImpl {
739        ExprImpl::FunctionCall(Box::new(FunctionCall::new_unchecked(
740            ExprType::IsNotDistinctFrom,
741            vec![
742                ExprImpl::InputRef(Box::new(InputRef::new(left, left_data_type))),
743                ExprImpl::InputRef(Box::new(InputRef::new(right, right_data_type))),
744            ],
745            DataType::Boolean,
746        )))
747    }
748}
749
750pub(crate) struct TableScanIoEstimator<'a> {
751    table_scan: &'a LogicalScan,
752    row_size: usize,
753    cost: Option<IndexCost>,
754}
755
756impl<'a> TableScanIoEstimator<'a> {
757    pub fn new(table_scan: &'a LogicalScan, row_size: usize) -> Self {
758        Self {
759            table_scan,
760            row_size,
761            cost: None,
762        }
763    }
764
765    pub fn estimate_row_size(table_scan: &LogicalScan) -> usize {
766        // 5 for table_id + 1 for vnode + 8 for epoch
767        let row_meta_field_estimate_size = 14_usize;
768        let table = table_scan.table();
769        row_meta_field_estimate_size
770            + table
771                .columns
772                .iter()
773                // add order key twice for its appearance both in key and value
774                .chain(table.pk.iter().map(|x| &table.columns[x.column_index]))
775                .map(|x| TableScanIoEstimator::estimate_data_type_size(&x.data_type))
776                .sum::<usize>()
777    }
778
779    fn estimate_data_type_size(data_type: &DataType) -> usize {
780        use std::mem::size_of;
781
782        match data_type {
783            DataType::Boolean => size_of::<bool>(),
784            DataType::Int16 => size_of::<i16>(),
785            DataType::Int32 => size_of::<i32>(),
786            DataType::Int64 => size_of::<i64>(),
787            DataType::Serial => size_of::<Serial>(),
788            DataType::Float32 => size_of::<f32>(),
789            DataType::Float64 => size_of::<f64>(),
790            DataType::Decimal => size_of::<Decimal>(),
791            DataType::Date => size_of::<Date>(),
792            DataType::Time => size_of::<Time>(),
793            DataType::Timestamp => size_of::<Timestamp>(),
794            DataType::Timestamptz => size_of::<Timestamptz>(),
795            DataType::Interval => size_of::<Interval>(),
796            DataType::Int256 => Int256::size(),
797            DataType::Varchar => 20,
798            DataType::Bytea => 20,
799            DataType::Jsonb => 20,
800            DataType::Variant => 20,
801            DataType::Struct { .. } => 20,
802            DataType::List { .. } => 20,
803            DataType::Map(_) => 20,
804            DataType::Vector(d) => d * size_of::<VectorDistanceType>(),
805        }
806    }
807
808    pub fn estimate(&mut self, predicate: &Condition) -> IndexCost {
809        // try to deal with OR condition
810        if predicate.conjunctions.len() == 1 {
811            self.visit_expr(&predicate.conjunctions[0]);
812            self.cost.take().unwrap_or_default()
813        } else {
814            self.estimate_conjunctions(&predicate.conjunctions)
815        }
816    }
817
818    fn estimate_conjunctions(&mut self, conjunctions: &[ExprImpl]) -> IndexCost {
819        let mut new_conjunctions = conjunctions.to_owned();
820
821        let mut match_item_vec = vec![];
822
823        for column_idx in self.table_scan.table().order_column_indices() {
824            let match_item = self.match_index_column(column_idx, &mut new_conjunctions);
825            // seeing range, we don't need to match anymore.
826            let should_break = match match_item {
827                MatchItem::Equal | MatchItem::In(_) => false,
828                MatchItem::RangeOneSideBound | MatchItem::RangeTwoSideBound | MatchItem::All => {
829                    true
830                }
831            };
832            match_item_vec.push(match_item);
833            if should_break {
834                break;
835            }
836        }
837
838        let index_cost = match_item_vec
839            .iter()
840            .enumerate()
841            .take(INDEX_MAX_LEN)
842            .map(|(i, match_item)| match match_item {
843                MatchItem::Equal => INDEX_COST_MATRIX[0][i],
844                MatchItem::In(num) => min(INDEX_COST_MATRIX[1][i], *num),
845                MatchItem::RangeTwoSideBound => INDEX_COST_MATRIX[2][i],
846                MatchItem::RangeOneSideBound => INDEX_COST_MATRIX[3][i],
847                MatchItem::All => INDEX_COST_MATRIX[4][i],
848            })
849            .reduce(|x, y| x * y)
850            .unwrap();
851
852        // If `index_cost` equals 1, it is a primary lookup
853        let primary_lookup = index_cost == 1;
854
855        IndexCost::new(index_cost, primary_lookup)
856            .mul(&IndexCost::new(self.row_size, primary_lookup))
857    }
858
859    fn match_index_column(
860        &mut self,
861        column_idx: usize,
862        conjunctions: &mut Vec<ExprImpl>,
863    ) -> MatchItem {
864        // Equal
865        for (i, expr) in conjunctions.iter().enumerate() {
866            if let Some((input_ref, _const_expr)) = expr.as_eq_const()
867                && input_ref.index == column_idx
868            {
869                conjunctions.remove(i);
870                return MatchItem::Equal;
871            }
872        }
873
874        // In
875        for (i, expr) in conjunctions.iter().enumerate() {
876            if let Some((input_ref, in_const_list)) = expr
877                .as_in_const_list()
878                .or_else(|| expr.as_some_eq_const_list())
879                && input_ref.index == column_idx
880            {
881                conjunctions.remove(i);
882                return MatchItem::In(in_const_list.len());
883            }
884        }
885
886        // Range
887        let mut left_side_bound = false;
888        let mut right_side_bound = false;
889        let mut i = 0;
890        while i < conjunctions.len() {
891            let expr = &conjunctions[i];
892            if let Some((input_ref, op, _const_expr)) = expr.as_comparison_const()
893                && input_ref.index == column_idx
894            {
895                conjunctions.remove(i);
896                match op {
897                    ExprType::LessThan | ExprType::LessThanOrEqual => right_side_bound = true,
898                    ExprType::GreaterThan | ExprType::GreaterThanOrEqual => left_side_bound = true,
899                    _ => unreachable!(),
900                };
901            } else {
902                i += 1;
903            }
904        }
905
906        if left_side_bound && right_side_bound {
907            MatchItem::RangeTwoSideBound
908        } else if left_side_bound || right_side_bound {
909            MatchItem::RangeOneSideBound
910        } else {
911            MatchItem::All
912        }
913    }
914}
915
916enum MatchItem {
917    Equal,
918    In(usize),
919    RangeTwoSideBound,
920    RangeOneSideBound,
921    All,
922}
923
924#[derive(PartialEq, Eq, Hash, Clone, Debug, PartialOrd, Ord)]
925pub(crate) struct IndexCost {
926    cost: usize,
927    pub(crate) primary_lookup: bool,
928}
929
930impl Default for IndexCost {
931    fn default() -> Self {
932        Self {
933            cost: IndexCost::maximum(),
934            primary_lookup: false,
935        }
936    }
937}
938
939impl IndexCost {
940    fn new(cost: usize, primary_lookup: bool) -> IndexCost {
941        Self {
942            cost: min(cost, IndexCost::maximum()),
943            primary_lookup,
944        }
945    }
946
947    fn maximum() -> usize {
948        10000000
949    }
950
951    fn add(&self, other: &IndexCost) -> IndexCost {
952        IndexCost::new(
953            self.cost
954                .checked_add(other.cost)
955                .unwrap_or_else(IndexCost::maximum),
956            self.primary_lookup && other.primary_lookup,
957        )
958    }
959
960    fn mul(&self, other: &IndexCost) -> IndexCost {
961        IndexCost::new(
962            self.cost
963                .checked_mul(other.cost)
964                .unwrap_or_else(IndexCost::maximum),
965            self.primary_lookup && other.primary_lookup,
966        )
967    }
968
969    pub(crate) fn le(&self, other: &IndexCost) -> bool {
970        self.cost < other.cost
971    }
972}
973
974impl ExprVisitor for TableScanIoEstimator<'_> {
975    fn visit_function_call(&mut self, func_call: &FunctionCall) {
976        let cost = match func_call.func_type() {
977            ExprType::Or => func_call
978                .inputs()
979                .iter()
980                .map(|x| {
981                    let mut estimator = TableScanIoEstimator::new(self.table_scan, self.row_size);
982                    estimator.visit_expr(x);
983                    estimator.cost.take().unwrap_or_default()
984                })
985                .reduce(|x, y| x.add(&y))
986                .unwrap(),
987            ExprType::And => self.estimate_conjunctions(func_call.inputs()),
988            _ => {
989                let single = vec![ExprImpl::FunctionCall(func_call.clone().into())];
990                self.estimate_conjunctions(&single)
991            }
992        };
993        self.cost = Some(cost);
994    }
995}
996
997struct ShiftInputRefRewriter {
998    offset: usize,
999}
1000impl ExprRewriter for ShiftInputRefRewriter {
1001    fn rewrite_input_ref(&mut self, input_ref: InputRef) -> ExprImpl {
1002        InputRef::new(input_ref.index() + self.offset, input_ref.return_type()).into()
1003    }
1004}
1005
1006impl IndexSelectionRule {
1007    pub fn create() -> BoxedRule {
1008        Box::new(IndexSelectionRule {})
1009    }
1010}