linkrs_query/optimizer/cost_based/
index.rs1use std::sync::Arc;
6
7use crate::optimizer::cost::{CostCalculator, SelectivityEstimator};
8use linkrs_core::types::{Expression, Index};
9
10#[derive(Debug)]
12pub struct IndexSelector {
13 cost_calculator: Arc<CostCalculator>,
14 selectivity_estimator: Arc<SelectivityEstimator>,
15}
16
17#[derive(Debug, Clone)]
19pub enum IndexSelection {
20 PropertyIndex {
22 index_name: String,
24 property_name: String,
26 estimated_cost: f64,
28 selectivity: f64,
30 },
31 TagIndex {
33 estimated_cost: f64,
35 vertex_count: u64,
37 },
38 FullScan {
40 estimated_cost: f64,
42 vertex_count: u64,
44 },
45}
46
47#[derive(Debug, Clone)]
49pub struct PropertyPredicate {
50 pub property_name: String,
52 pub operator: PredicateOperator,
54 pub value: Expression,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum PredicateOperator {
61 Equal,
63 NotEqual,
65 LessThan,
67 LessThanOrEqual,
69 GreaterThan,
71 GreaterThanOrEqual,
73 Like,
75 In,
77}
78
79impl IndexSelector {
80 pub fn new(
84 cost_calculator: Arc<CostCalculator>,
85 selectivity_estimator: Arc<SelectivityEstimator>,
86 ) -> Self {
87 Self {
88 cost_calculator,
89 selectivity_estimator,
90 }
91 }
92
93 pub fn with_refs<'a>(
98 cost_calculator: &'a CostCalculator,
99 selectivity_estimator: &'a SelectivityEstimator,
100 ) -> Self {
101 Self {
102 cost_calculator: Arc::new(cost_calculator.clone()),
103 selectivity_estimator: Arc::new(selectivity_estimator.clone()),
104 }
105 }
106
107 pub fn select_index(
109 &self,
110 space: &str,
111 tag_name: &str,
112 predicates: &[PropertyPredicate],
113 available_indexes: &[Index],
114 ) -> IndexSelection {
115 if predicates.is_empty() {
117 let vertex_count = self
118 .cost_calculator
119 .statistics_manager()
120 .get_vertex_count(space, tag_name);
121 let estimated_cost = self
122 .cost_calculator
123 .calculate_scan_vertices_cost(space, tag_name);
124 return IndexSelection::FullScan {
125 estimated_cost,
126 vertex_count,
127 };
128 }
129
130 let mut best_selection: Option<IndexSelection> = None;
132
133 for index in available_indexes {
134 if index.schema_name != tag_name {
136 continue;
137 }
138
139 if let Some(selection) = self.evaluate_index(space, index, predicates) {
140 match &best_selection {
141 None => best_selection = Some(selection),
142 Some(current_best) => {
143 if selection.estimated_cost() < current_best.estimated_cost() {
144 best_selection = Some(selection);
145 }
146 }
147 }
148 }
149 }
150
151 best_selection.unwrap_or_else(|| {
153 let vertex_count = self
154 .cost_calculator
155 .statistics_manager()
156 .get_vertex_count(space, tag_name);
157 let estimated_cost = self
158 .cost_calculator
159 .calculate_scan_vertices_cost(space, tag_name);
160 IndexSelection::FullScan {
161 estimated_cost,
162 vertex_count,
163 }
164 })
165 }
166
167 fn evaluate_index(
169 &self,
170 space: &str,
171 index: &Index,
172 predicates: &[PropertyPredicate],
173 ) -> Option<IndexSelection> {
174 let covered_predicates: Vec<&PropertyPredicate> = predicates
176 .iter()
177 .filter(|p| index.properties.contains(&p.property_name))
178 .collect();
179
180 if covered_predicates.is_empty() {
181 return None;
182 }
183
184 let mut total_selectivity = 1.0;
186 for predicate in &covered_predicates {
187 let selectivity = match predicate.operator {
188 PredicateOperator::Equal => {
189 let value = if let Expression::Literal(v) = &predicate.value {
191 Some(v.clone())
192 } else {
193 None
194 };
195 self.selectivity_estimator.estimate_equality_selectivity(
196 Some(space),
197 Some(&index.schema_name),
198 &predicate.property_name,
199 value.as_ref(),
200 )
201 }
202 PredicateOperator::LessThan | PredicateOperator::LessThanOrEqual => {
203 let bound = match &predicate.value {
204 Expression::Literal(v) => numeric_literal_as_f64(v),
205 _ => None,
206 };
207 self.selectivity_estimator.estimate_ordered_comparison(
208 Some(space),
209 Some(&index.schema_name),
210 &predicate.property_name,
211 bound,
212 false,
213 )
214 }
215 PredicateOperator::GreaterThan | PredicateOperator::GreaterThanOrEqual => {
216 let bound = match &predicate.value {
217 Expression::Literal(v) => numeric_literal_as_f64(v),
218 _ => None,
219 };
220 self.selectivity_estimator.estimate_ordered_comparison(
221 Some(space),
222 Some(&index.schema_name),
223 &predicate.property_name,
224 bound,
225 true,
226 )
227 }
228 PredicateOperator::Like => {
229 if let Expression::Literal(linkrs_core::value::Value::String(pattern)) =
231 &predicate.value
232 {
233 self.selectivity_estimator
234 .estimate_like_selectivity(pattern)
235 } else {
236 0.3
237 }
238 }
239 _ => 0.3,
240 };
241 total_selectivity *= selectivity;
242 }
243
244 let estimated_cost = self.cost_calculator.calculate_index_scan_cost(
246 space,
247 &index.schema_name,
248 &covered_predicates[0].property_name,
249 total_selectivity,
250 );
251
252 let property_name = covered_predicates[0].property_name.clone();
254
255 Some(IndexSelection::PropertyIndex {
256 index_name: index.name.clone(),
257 property_name,
258 estimated_cost,
259 selectivity: total_selectivity,
260 })
261 }
262
263 pub fn select_composite_index_strategy(
265 &self,
266 space: &str,
267 tag_name: &str,
268 predicates: &[PropertyPredicate],
269 available_indexes: &[Index],
270 ) -> Vec<IndexSelection> {
271 let mut strategies = Vec::new();
272
273 let vertex_count = self
275 .cost_calculator
276 .statistics_manager()
277 .get_vertex_count(space, tag_name);
278 let full_scan_cost = self
279 .cost_calculator
280 .calculate_scan_vertices_cost(space, tag_name);
281 strategies.push(IndexSelection::FullScan {
282 estimated_cost: full_scan_cost,
283 vertex_count,
284 });
285
286 for index in available_indexes {
288 if index.schema_name != tag_name {
289 continue;
290 }
291
292 if let Some(selection) = self.evaluate_index(space, index, predicates) {
293 strategies.push(selection);
294 }
295 }
296
297 strategies.sort_by(|a, b| {
299 a.estimated_cost()
300 .partial_cmp(&b.estimated_cost())
301 .unwrap_or(std::cmp::Ordering::Equal)
302 });
303
304 strategies
305 }
306}
307
308impl Clone for IndexSelector {
309 fn clone(&self) -> Self {
310 Self {
311 cost_calculator: self.cost_calculator.clone(),
312 selectivity_estimator: self.selectivity_estimator.clone(),
313 }
314 }
315}
316
317fn numeric_literal_as_f64(value: &linkrs_core::Value) -> Option<f64> {
318 match value {
319 linkrs_core::Value::SmallInt(v) => Some(*v as f64),
320 linkrs_core::Value::Int(v) => Some(*v as f64),
321 linkrs_core::Value::BigInt(v) => Some(*v as f64),
322 linkrs_core::Value::Float(v) => Some((*v).into()),
323 linkrs_core::Value::Double(v) => Some(*v),
324 _ => None,
325 }
326}
327
328impl IndexSelection {
329 pub fn estimated_cost(&self) -> f64 {
331 match self {
332 IndexSelection::PropertyIndex { estimated_cost, .. } => *estimated_cost,
333 IndexSelection::TagIndex { estimated_cost, .. } => *estimated_cost,
334 IndexSelection::FullScan { estimated_cost, .. } => *estimated_cost,
335 }
336 }
337
338 pub fn selectivity(&self) -> Option<f64> {
340 match self {
341 IndexSelection::PropertyIndex { selectivity, .. } => Some(*selectivity),
342 _ => None,
343 }
344 }
345
346 pub fn is_index_scan(&self) -> bool {
348 matches!(self, IndexSelection::PropertyIndex { .. })
349 }
350
351 pub fn is_full_scan(&self) -> bool {
353 matches!(self, IndexSelection::FullScan { .. })
354 }
355}