1use super::value_range::{compare_histogram_value, range_bound_matches, ValueRangeBound};
16use crate::cardinality_defaults::{
17 NULL_SELECTIVITY_DIVISOR_CAP, RANGE_SELECTIVITY_DIVISOR, SAMPLED_HISTOGRAM_MATCH_PSEUDOCOUNT,
18 SAMPLED_HISTOGRAM_TOTAL_PSEUDOCOUNT,
19};
20use hawdb_core::{Catalog, GraphStatistics, IndexKind, Value};
21use hawdb_plan_cypher::ComparisonOp;
22use std::collections::{BTreeMap, BTreeSet};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub(super) struct ExpandEstimate {
26 pub(super) path_count: Option<u64>,
27 pub(super) rel_count: u64,
28 pub(super) source_count: u64,
29 pub(super) average_fanout: u64,
30 pub(super) property_distinct_product: u64,
31 pub(super) estimated_rows: u64,
32 pub(super) hop_estimates: Vec<HopEstimate>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub(super) struct HopEstimate {
37 pub(super) hop: usize,
38 pub(super) rows: u64,
39 pub(super) exact: bool,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct OptimizerIndexStatistics {
44 pub index_size: u64,
45 pub distinct_count: u64,
46}
47
48#[derive(Debug, Default, Clone, PartialEq, Eq)]
49pub struct OptimizerCatalog {
50 pub(super) assume_all_indexes: bool,
51 pub(super) equality_property_indexes: BTreeSet<(String, String)>,
52 pub(super) composite_property_indexes: BTreeSet<(String, Vec<String>)>,
53 pub(super) range_property_indexes: BTreeSet<(String, String)>,
54 pub(super) ordered_range_indexes: BTreeSet<(String, String)>,
55 pub(super) full_text_property_indexes: BTreeSet<(String, String)>,
56 pub(super) label_counts: BTreeMap<String, u64>,
57 pub(super) rel_type_counts: BTreeMap<String, u64>,
58 pub(super) rel_type_source_counts: BTreeMap<String, u64>,
59 pub(super) rel_type_target_counts: BTreeMap<String, u64>,
60 pub(super) path_counts: BTreeMap<(String, String, String), u64>,
61 pub(super) path_source_distinct_counts: BTreeMap<(String, String, String), u64>,
62 pub(super) path_target_distinct_counts: BTreeMap<(String, String, String), u64>,
63 pub(super) bounded_path_counts: BTreeMap<(String, String, String, usize), u64>,
64 pub(super) bounded_path_source_distinct_counts: BTreeMap<(String, String, String, usize), u64>,
65 pub(super) bounded_path_target_distinct_counts: BTreeMap<(String, String, String, usize), u64>,
66 pub(super) property_index_statistics: BTreeMap<(String, String), OptimizerIndexStatistics>,
67 property_equality_row_count: Option<((String, String), u64)>,
68 pub(super) composite_index_statistics:
69 BTreeMap<(String, Vec<String>), OptimizerIndexStatistics>,
70 pub(super) property_distinct_counts: BTreeMap<(String, String), u64>,
71 pub(super) rel_property_distinct_counts: BTreeMap<(String, String), u64>,
72 pub(super) property_histograms: BTreeMap<(String, String), Vec<Value>>,
73 pub(super) rel_property_histograms: BTreeMap<(String, String), Vec<Value>>,
74 pub(super) sampled_property_histograms: BTreeMap<(String, String), bool>,
75 pub(super) sampled_rel_property_histograms: BTreeMap<(String, String), bool>,
76}
77
78#[derive(Debug, Default, Clone, PartialEq, Eq)]
79pub struct OptimizerCatalogIndexes {
80 pub(super) equality_property_indexes: BTreeSet<(String, String)>,
81 pub(super) composite_property_indexes: BTreeSet<(String, Vec<String>)>,
82 pub(super) range_property_indexes: BTreeSet<(String, String)>,
83 pub(super) full_text_property_indexes: BTreeSet<(String, String)>,
84}
85
86#[derive(Debug, Default, Clone, PartialEq, Eq)]
87pub struct OptimizerCatalogStatistics {
88 pub(super) label_counts: BTreeMap<String, u64>,
89 pub(super) rel_type_counts: BTreeMap<String, u64>,
90 pub(super) rel_type_source_counts: BTreeMap<String, u64>,
91 pub(super) rel_type_target_counts: BTreeMap<String, u64>,
92 pub(super) path_counts: BTreeMap<(String, String, String), u64>,
93 pub(super) path_source_distinct_counts: BTreeMap<(String, String, String), u64>,
94 pub(super) path_target_distinct_counts: BTreeMap<(String, String, String), u64>,
95 pub(super) bounded_path_counts: BTreeMap<(String, String, String, usize), u64>,
96 pub(super) bounded_path_source_distinct_counts: BTreeMap<(String, String, String, usize), u64>,
97 pub(super) bounded_path_target_distinct_counts: BTreeMap<(String, String, String, usize), u64>,
98 pub(super) property_index_statistics: BTreeMap<(String, String), OptimizerIndexStatistics>,
99 pub(super) composite_index_statistics:
100 BTreeMap<(String, Vec<String>), OptimizerIndexStatistics>,
101 pub(super) property_distinct_counts: BTreeMap<(String, String), u64>,
102 pub(super) rel_property_distinct_counts: BTreeMap<(String, String), u64>,
103 pub(super) property_histograms: BTreeMap<(String, String), Vec<Value>>,
104 pub(super) rel_property_histograms: BTreeMap<(String, String), Vec<Value>>,
105 pub(super) sampled_property_histograms: BTreeMap<(String, String), bool>,
106 pub(super) sampled_rel_property_histograms: BTreeMap<(String, String), bool>,
107}
108
109#[doc(hidden)]
115pub fn optimizer_catalog_from_graph_statistics(
116 catalog: &Catalog,
117 statistics: &GraphStatistics,
118) -> OptimizerCatalog {
119 let advanced_statistics = statistics
120 .advanced_statistics_complete
121 .then_some(statistics);
122 let equality_property_indexes = catalog.property_indexes().filter_map(|index| {
123 if index.kind != IndexKind::Equality {
124 return None;
125 }
126 catalog
127 .label_name(index.label_id)
128 .map(|label| (label.to_string(), index.property.clone()))
129 });
130 let composite_property_indexes = catalog.composite_property_indexes().filter_map(|index| {
131 catalog
132 .label_name(index.label_id)
133 .map(|label| (label.to_string(), index.properties.clone()))
134 });
135 let range_property_indexes = catalog.property_indexes().filter_map(|index| {
136 if index.kind != IndexKind::Range {
137 return None;
138 }
139 catalog
140 .label_name(index.label_id)
141 .map(|label| (label.to_string(), index.property.clone()))
142 });
143 let full_text_property_indexes = catalog.property_indexes().filter_map(|index| {
144 if index.kind != IndexKind::FullText {
145 return None;
146 }
147 catalog
148 .label_name(index.label_id)
149 .map(|label| (label.to_string(), index.property.clone()))
150 });
151 let label_counts = statistics
152 .label_counts
153 .iter()
154 .filter_map(|(label_id, count)| {
155 catalog
156 .label_name(*label_id)
157 .map(|label| (label.to_string(), *count))
158 });
159 let rel_type_counts = statistics
160 .rel_type_counts
161 .iter()
162 .filter_map(|(rel_type_id, count)| {
163 catalog
164 .rel_type_name(*rel_type_id)
165 .map(|rel_type| (rel_type.to_string(), *count))
166 });
167 let rel_type_source_counts = advanced_statistics
168 .into_iter()
169 .flat_map(|statistics| statistics.rel_type_source_counts.iter())
170 .filter_map(|(rel_type_id, count)| {
171 catalog
172 .rel_type_name(*rel_type_id)
173 .map(|rel_type| (rel_type.to_string(), *count))
174 });
175 let rel_type_target_counts = advanced_statistics
176 .into_iter()
177 .flat_map(|statistics| statistics.rel_type_target_counts.iter())
178 .filter_map(|(rel_type_id, count)| {
179 catalog
180 .rel_type_name(*rel_type_id)
181 .map(|rel_type| (rel_type.to_string(), *count))
182 });
183 let path_counts = advanced_statistics
184 .into_iter()
185 .flat_map(|statistics| statistics.path_counts.iter())
186 .filter_map(|((source_label_id, rel_type_id, target_label_id), count)| {
187 Some((
188 (
189 catalog.label_name(*source_label_id)?.to_string(),
190 catalog.rel_type_name(*rel_type_id)?.to_string(),
191 catalog.label_name(*target_label_id)?.to_string(),
192 ),
193 *count,
194 ))
195 });
196 let path_source_distinct_counts = advanced_statistics
197 .into_iter()
198 .flat_map(|statistics| statistics.path_source_distinct_counts.iter())
199 .filter_map(|((source_label_id, rel_type_id, target_label_id), count)| {
200 Some((
201 (
202 catalog.label_name(*source_label_id)?.to_string(),
203 catalog.rel_type_name(*rel_type_id)?.to_string(),
204 catalog.label_name(*target_label_id)?.to_string(),
205 ),
206 *count,
207 ))
208 });
209 let path_target_distinct_counts = advanced_statistics
210 .into_iter()
211 .flat_map(|statistics| statistics.path_target_distinct_counts.iter())
212 .filter_map(|((source_label_id, rel_type_id, target_label_id), count)| {
213 Some((
214 (
215 catalog.label_name(*source_label_id)?.to_string(),
216 catalog.rel_type_name(*rel_type_id)?.to_string(),
217 catalog.label_name(*target_label_id)?.to_string(),
218 ),
219 *count,
220 ))
221 });
222 let bounded_path_counts = advanced_statistics
223 .into_iter()
224 .flat_map(|statistics| statistics.bounded_path_counts.iter())
225 .filter_map(
226 |((source_label_id, rel_type_id, target_label_id, hops), count)| {
227 Some((
228 (
229 catalog.label_name(*source_label_id)?.to_string(),
230 catalog.rel_type_name(*rel_type_id)?.to_string(),
231 catalog.label_name(*target_label_id)?.to_string(),
232 *hops,
233 ),
234 *count,
235 ))
236 },
237 );
238 let bounded_path_source_distinct_counts = advanced_statistics
239 .into_iter()
240 .flat_map(|statistics| statistics.bounded_path_source_distinct_counts.iter())
241 .filter_map(
242 |((source_label_id, rel_type_id, target_label_id, hops), count)| {
243 Some((
244 (
245 catalog.label_name(*source_label_id)?.to_string(),
246 catalog.rel_type_name(*rel_type_id)?.to_string(),
247 catalog.label_name(*target_label_id)?.to_string(),
248 *hops,
249 ),
250 *count,
251 ))
252 },
253 );
254 let bounded_path_target_distinct_counts = advanced_statistics
255 .into_iter()
256 .flat_map(|statistics| statistics.bounded_path_target_distinct_counts.iter())
257 .filter_map(
258 |((source_label_id, rel_type_id, target_label_id, hops), count)| {
259 Some((
260 (
261 catalog.label_name(*source_label_id)?.to_string(),
262 catalog.rel_type_name(*rel_type_id)?.to_string(),
263 catalog.label_name(*target_label_id)?.to_string(),
264 *hops,
265 ),
266 *count,
267 ))
268 },
269 );
270 let property_distinct_counts = advanced_statistics
271 .into_iter()
272 .flat_map(|statistics| statistics.property_distinct_counts.iter())
273 .filter_map(|((label_id, property), count)| {
274 catalog
275 .label_name(*label_id)
276 .map(|label| ((label.to_string(), property.clone()), *count))
277 });
278 let property_histograms = advanced_statistics
279 .into_iter()
280 .flat_map(|statistics| statistics.property_histograms.iter())
281 .filter_map(|((label_id, property), values)| {
282 catalog
283 .label_name(*label_id)
284 .map(|label| ((label.to_string(), property.clone()), values.clone()))
285 });
286 let sampled_property_histograms = advanced_statistics
287 .into_iter()
288 .flat_map(|statistics| statistics.sampled_property_histograms.iter())
289 .filter_map(|((label_id, property), sampled)| {
290 catalog
291 .label_name(*label_id)
292 .map(|label| ((label.to_string(), property.clone()), *sampled))
293 });
294 let rel_property_distinct_counts = advanced_statistics
295 .into_iter()
296 .flat_map(|statistics| statistics.rel_property_distinct_counts.iter())
297 .filter_map(|((rel_type_id, property), count)| {
298 catalog
299 .rel_type_name(*rel_type_id)
300 .map(|rel_type| ((rel_type.to_string(), property.clone()), *count))
301 });
302 let rel_property_histograms = advanced_statistics
303 .into_iter()
304 .flat_map(|statistics| statistics.rel_property_histograms.iter())
305 .filter_map(|((rel_type_id, property), values)| {
306 catalog
307 .rel_type_name(*rel_type_id)
308 .map(|rel_type| ((rel_type.to_string(), property.clone()), values.clone()))
309 });
310 let sampled_rel_property_histograms = advanced_statistics
311 .into_iter()
312 .flat_map(|statistics| statistics.sampled_rel_property_histograms.iter())
313 .filter_map(|((rel_type_id, property), sampled)| {
314 catalog
315 .rel_type_name(*rel_type_id)
316 .map(|rel_type| ((rel_type.to_string(), property.clone()), *sampled))
317 });
318 let property_index_statistics = catalog.property_indexes().filter_map(|index| {
319 if index.kind == IndexKind::FullText {
320 return None;
321 }
322 let statistics = advanced_statistics?;
323 let sample = statistics.index_samples.get(&index.id)?;
324 let distinct_count = sample.estimated_unique_values()?;
325 let label = catalog.label_name(index.label_id)?;
326 Some((
327 (label.to_string(), index.property.clone()),
328 OptimizerIndexStatistics {
329 index_size: sample.index_size,
330 distinct_count,
331 },
332 ))
333 });
334 let composite_index_statistics = catalog.composite_property_indexes().filter_map(|index| {
335 let statistics = advanced_statistics?;
336 let sample = statistics.index_samples.get(&index.id)?;
337 let distinct_count = sample.estimated_unique_values()?;
338 let label = catalog.label_name(index.label_id)?;
339 Some((
340 (label.to_string(), index.properties.clone()),
341 OptimizerIndexStatistics {
342 index_size: sample.index_size,
343 distinct_count,
344 },
345 ))
346 });
347 OptimizerCatalog::new(
348 OptimizerCatalogIndexes::new(
349 equality_property_indexes,
350 composite_property_indexes,
351 range_property_indexes,
352 full_text_property_indexes,
353 ),
354 OptimizerCatalogStatistics::new(
355 label_counts,
356 rel_type_counts,
357 rel_type_source_counts,
358 path_counts,
359 bounded_path_counts,
360 property_distinct_counts,
361 property_histograms,
362 )
363 .with_property_index_statistics(property_index_statistics)
364 .with_composite_index_statistics(composite_index_statistics)
365 .with_relationship_type_target_counts(rel_type_target_counts)
366 .with_path_source_distinct_counts(path_source_distinct_counts)
367 .with_path_target_distinct_counts(path_target_distinct_counts)
368 .with_bounded_path_source_distinct_counts(bounded_path_source_distinct_counts)
369 .with_bounded_path_target_distinct_counts(bounded_path_target_distinct_counts)
370 .with_relationship_property_distinct_counts(rel_property_distinct_counts)
371 .with_relationship_property_histograms(rel_property_histograms)
372 .with_sampled_property_histograms(sampled_property_histograms)
373 .with_sampled_relationship_property_histograms(sampled_rel_property_histograms),
374 )
375}
376
377impl OptimizerCatalog {
378 pub fn new(indexes: OptimizerCatalogIndexes, statistics: OptimizerCatalogStatistics) -> Self {
379 Self {
380 assume_all_indexes: false,
381 equality_property_indexes: indexes.equality_property_indexes,
382 composite_property_indexes: indexes.composite_property_indexes,
383 range_property_indexes: indexes.range_property_indexes,
384 ordered_range_indexes: BTreeSet::new(),
385 full_text_property_indexes: indexes.full_text_property_indexes,
386 label_counts: statistics.label_counts,
387 rel_type_counts: statistics.rel_type_counts,
388 rel_type_source_counts: statistics.rel_type_source_counts,
389 rel_type_target_counts: statistics.rel_type_target_counts,
390 path_counts: statistics.path_counts,
391 path_source_distinct_counts: statistics.path_source_distinct_counts,
392 path_target_distinct_counts: statistics.path_target_distinct_counts,
393 bounded_path_counts: statistics.bounded_path_counts,
394 bounded_path_source_distinct_counts: statistics.bounded_path_source_distinct_counts,
395 bounded_path_target_distinct_counts: statistics.bounded_path_target_distinct_counts,
396 property_index_statistics: statistics.property_index_statistics,
397 property_equality_row_count: None,
398 composite_index_statistics: statistics.composite_index_statistics,
399 property_distinct_counts: statistics.property_distinct_counts,
400 rel_property_distinct_counts: statistics.rel_property_distinct_counts,
401 property_histograms: statistics.property_histograms,
402 rel_property_histograms: statistics.rel_property_histograms,
403 sampled_property_histograms: statistics.sampled_property_histograms,
404 sampled_rel_property_histograms: statistics.sampled_rel_property_histograms,
405 }
406 }
407
408 #[doc(hidden)]
412 pub fn with_property_equality_row_count(
413 mut self,
414 label: String,
415 property: String,
416 rows: u64,
417 ) -> Self {
418 self.property_equality_row_count = Some(((label, property), rows));
419 self
420 }
421
422 fn query_local_property_eq_rows(&self, label: &str, property: &str) -> Option<u64> {
423 self.property_equality_row_count
424 .as_ref()
425 .filter(|((local_label, local_property), _)| {
426 local_label == label && local_property == property
427 })
428 .map(|(_, rows)| *rows)
429 }
430
431 pub fn with_ordered_range_indexes(
435 mut self,
436 indexes: impl IntoIterator<Item = (String, String)>,
437 ) -> Self {
438 self.ordered_range_indexes = indexes
439 .into_iter()
440 .filter(|index| self.range_property_indexes.contains(index))
441 .collect();
442 self
443 }
444
445 pub(super) fn optimistic() -> Self {
446 Self {
447 assume_all_indexes: true,
448 ..Self::default()
449 }
450 }
451
452 pub(super) fn has_property_index(&self, label: &str, property: &str) -> bool {
453 self.assume_all_indexes
454 || self
455 .equality_property_indexes
456 .contains(&(label.to_string(), property.to_string()))
457 }
458
459 pub(super) fn has_range_property_index(&self, label: &str, property: &str) -> bool {
460 self.assume_all_indexes
461 || self
462 .range_property_indexes
463 .contains(&(label.to_string(), property.to_string()))
464 }
465
466 pub(super) fn has_full_text_property_index(&self, label: &str, property: &str) -> bool {
467 self.assume_all_indexes
468 || self
469 .full_text_property_indexes
470 .contains(&(label.to_string(), property.to_string()))
471 }
472
473 pub(super) fn has_composite_property_index(&self, label: &str, properties: &[String]) -> bool {
474 self.assume_all_indexes
475 || self
476 .composite_property_indexes
477 .contains(&(label.to_string(), properties.to_vec()))
478 }
479
480 pub(super) fn composite_property_indexes_for_label(&self, label: &str) -> Vec<Vec<String>> {
481 if self.assume_all_indexes {
482 return Vec::new();
483 }
484 self.composite_property_indexes
485 .iter()
486 .filter_map(|(candidate_label, properties)| {
487 if candidate_label == label {
488 Some(properties.clone())
489 } else {
490 None
491 }
492 })
493 .collect()
494 }
495
496 pub(super) fn label_count(&self, label: &str) -> u64 {
497 self.label_counts.get(label).copied().unwrap_or(1).max(1)
498 }
499
500 pub(super) fn relationship_count(&self, rel_type: &str) -> u64 {
501 self.rel_type_counts
502 .get(rel_type)
503 .copied()
504 .unwrap_or(1)
505 .max(1)
506 }
507
508 pub(super) fn path_source_distinct_count(
509 &self,
510 source_label: &str,
511 rel_type: &str,
512 target_label: &str,
513 ) -> Option<u64> {
514 self.path_source_distinct_counts
515 .get(&(
516 source_label.to_string(),
517 rel_type.to_string(),
518 target_label.to_string(),
519 ))
520 .copied()
521 }
522
523 pub(super) fn path_target_distinct_count(
524 &self,
525 source_label: &str,
526 rel_type: &str,
527 target_label: &str,
528 ) -> Option<u64> {
529 self.path_target_distinct_counts
530 .get(&(
531 source_label.to_string(),
532 rel_type.to_string(),
533 target_label.to_string(),
534 ))
535 .copied()
536 }
537
538 pub(super) fn bounded_path_source_distinct_count(
539 &self,
540 source_label: &str,
541 rel_type: &str,
542 target_label: &str,
543 min_hops: usize,
544 max_hops: usize,
545 ) -> Option<u64> {
546 self.bounded_path_distinct_count(
547 &self.bounded_path_source_distinct_counts,
548 source_label,
549 rel_type,
550 target_label,
551 min_hops,
552 max_hops,
553 )
554 }
555
556 pub(super) fn bounded_path_target_distinct_count(
557 &self,
558 source_label: &str,
559 rel_type: &str,
560 target_label: &str,
561 min_hops: usize,
562 max_hops: usize,
563 ) -> Option<u64> {
564 self.bounded_path_distinct_count(
565 &self.bounded_path_target_distinct_counts,
566 source_label,
567 rel_type,
568 target_label,
569 min_hops,
570 max_hops,
571 )
572 }
573
574 pub(super) fn bounded_path_distinct_count(
575 &self,
576 counts: &BTreeMap<(String, String, String, usize), u64>,
577 source_label: &str,
578 rel_type: &str,
579 target_label: &str,
580 min_hops: usize,
581 max_hops: usize,
582 ) -> Option<u64> {
583 let mut total = 0_u64;
584 let mut found = false;
585 for hop in min_hops..=max_hops.max(min_hops) {
586 if let Some(count) = counts.get(&(
587 source_label.to_string(),
588 rel_type.to_string(),
589 target_label.to_string(),
590 hop,
591 )) {
592 found = true;
593 total = total.saturating_add(*count);
594 }
595 }
596 found.then_some(total.max(1))
597 }
598
599 pub(super) fn distinct_count(&self, label: &str, property: &str) -> u64 {
600 self.property_index_statistics
601 .get(&(label.to_string(), property.to_string()))
602 .map(|statistics| statistics.distinct_count)
603 .or_else(|| {
604 self.property_distinct_counts
605 .get(&(label.to_string(), property.to_string()))
606 .copied()
607 })
608 .unwrap_or_else(|| self.label_count(label).max(1))
609 }
610
611 pub(super) fn estimate_property_index_eq_rows(&self, label: &str, property: &str) -> u64 {
612 if let Some(rows) = self.query_local_property_eq_rows(label, property) {
613 return rows.min(self.label_count(label)).max(1);
614 }
615 self.property_index_statistics
616 .get(&(label.to_string(), property.to_string()))
617 .map(|statistics| {
618 statistics
619 .index_size
620 .div_ceil(statistics.distinct_count.max(1))
621 .min(self.label_count(label))
622 .max(1)
623 })
624 .unwrap_or_else(|| {
625 self.label_count(label)
626 .div_ceil(self.distinct_count(label, property).max(1))
627 .max(1)
628 })
629 }
630
631 pub(super) fn estimate_property_index_in_rows(
632 &self,
633 label: &str,
634 property: &str,
635 value_count: u64,
636 ) -> u64 {
637 let upper_bound = self
638 .property_index_statistics
639 .get(&(label.to_string(), property.to_string()))
640 .map_or_else(
641 || self.label_count(label),
642 |statistics| statistics.index_size.min(self.label_count(label)),
643 );
644 self.estimate_property_index_eq_rows(label, property)
645 .saturating_mul(value_count)
646 .min(upper_bound)
647 .max(1)
648 }
649
650 pub(super) fn composite_distinct_count(&self, label: &str, properties: &[String]) -> u64 {
651 self.composite_index_statistics
652 .get(&(label.to_string(), properties.to_vec()))
653 .map(|statistics| statistics.distinct_count)
654 .unwrap_or_else(|| {
655 properties
656 .iter()
657 .map(|property| self.distinct_count(label, property).max(1))
658 .fold(1_u64, |product, distinct| product.saturating_mul(distinct))
659 .max(1)
660 })
661 }
662
663 pub(super) fn estimate_composite_property_index_rows(
664 &self,
665 label: &str,
666 properties: &[String],
667 ) -> u64 {
668 self.composite_index_statistics
669 .get(&(label.to_string(), properties.to_vec()))
670 .map(|statistics| {
671 statistics
672 .index_size
673 .div_ceil(statistics.distinct_count.max(1))
674 .min(self.label_count(label))
675 .max(1)
676 })
677 .unwrap_or_else(|| {
678 self.label_count(label)
679 .div_ceil(self.composite_distinct_count(label, properties))
680 .max(1)
681 })
682 }
683
684 pub(super) fn estimate_composite_prefix_range_rows(
685 &self,
686 label: &str,
687 index_properties: &[String],
688 equality_properties: &[String],
689 range_property: &str,
690 lower: Option<&ValueRangeBound>,
691 upper: Option<&ValueRangeBound>,
692 ) -> u64 {
693 let label_count = self.label_count(label);
694 let equality_distinct = equality_properties
695 .iter()
696 .map(|property| self.distinct_count(label, property).max(1))
697 .fold(1u64, u64::saturating_mul);
698 let equality_rows = label_count.div_ceil(equality_distinct).max(1);
699 let range_rows = self.estimate_range_bounds_rows(label, range_property, lower, upper);
700 let estimated = equality_rows
701 .saturating_mul(range_rows)
702 .div_ceil(label_count)
703 .max(1);
704 self.composite_index_statistics
705 .get(&(label.to_string(), index_properties.to_vec()))
706 .map_or(estimated, |statistics| {
707 estimated.min(statistics.index_size.max(1))
708 })
709 }
710
711 pub(super) fn known_distinct_count(&self, label: &str, property: &str) -> Option<u64> {
712 self.property_index_statistics
713 .get(&(label.to_string(), property.to_string()))
714 .map(|statistics| statistics.distinct_count)
715 .or_else(|| {
716 self.property_distinct_counts
717 .get(&(label.to_string(), property.to_string()))
718 .copied()
719 })
720 }
721
722 pub(super) fn known_rel_property_distinct_count(
723 &self,
724 rel_type: &str,
725 property: &str,
726 ) -> Option<u64> {
727 self.rel_property_distinct_counts
728 .get(&(rel_type.to_string(), property.to_string()))
729 .copied()
730 }
731
732 pub(super) fn rel_property_distinct_count(&self, rel_type: &str, property: &str) -> u64 {
733 self.rel_property_distinct_counts
734 .get(&(rel_type.to_string(), property.to_string()))
735 .copied()
736 .unwrap_or_else(|| {
737 self.rel_type_counts
738 .get(rel_type)
739 .copied()
740 .unwrap_or(1)
741 .max(1)
742 })
743 }
744
745 pub(super) fn estimate_rel_property_eq_rows(
746 &self,
747 rel_type: &str,
748 property: &str,
749 input_rows: u64,
750 ) -> u64 {
751 input_rows
752 .div_ceil(self.rel_property_distinct_count(rel_type, property).max(1))
753 .max(1)
754 }
755
756 pub(super) fn estimate_rel_property_not_eq_rows(
757 &self,
758 rel_type: &str,
759 property: &str,
760 input_rows: u64,
761 ) -> u64 {
762 input_rows
763 .saturating_sub(self.estimate_rel_property_eq_rows(rel_type, property, input_rows))
764 }
765
766 pub(super) fn estimate_rel_property_in_rows(
767 &self,
768 rel_type: &str,
769 property: &str,
770 values: &[Value],
771 input_rows: u64,
772 ) -> u64 {
773 if values.is_empty() {
774 return 0;
775 }
776 let distinct_count = self.rel_property_distinct_count(rel_type, property).max(1);
777 let value_count = values
778 .iter()
779 .collect::<BTreeSet<_>>()
780 .len()
781 .min(distinct_count as usize) as u64;
782 input_rows
783 .saturating_mul(value_count)
784 .div_ceil(distinct_count)
785 .max(1)
786 }
787
788 pub(super) fn estimate_rel_property_string_match_rows(
789 &self,
790 rel_type: &str,
791 property: &str,
792 input_rows: u64,
793 max_divisor: u64,
794 ) -> u64 {
795 let divisor = self
796 .rel_property_distinct_count(rel_type, property)
797 .max(1)
798 .min(max_divisor)
799 .max(1);
800 input_rows.div_ceil(divisor).max(1)
801 }
802
803 pub(super) fn estimate_rel_property_null_rows(
804 &self,
805 rel_type: &str,
806 property: &str,
807 input_rows: u64,
808 ) -> u64 {
809 let distinct_count = self.rel_property_distinct_count(rel_type, property).max(1);
810 input_rows
811 .div_ceil(distinct_count.min(NULL_SELECTIVITY_DIVISOR_CAP))
812 .max(1)
813 }
814
815 pub(super) fn estimate_rel_property_not_null_rows(
816 &self,
817 rel_type: &str,
818 property: &str,
819 input_rows: u64,
820 ) -> u64 {
821 input_rows
822 .saturating_sub(self.estimate_rel_property_null_rows(rel_type, property, input_rows))
823 .max(1)
824 }
825
826 pub(super) fn estimate_rel_property_range_rows(
827 &self,
828 rel_type: &str,
829 property: &str,
830 op: ComparisonOp,
831 value: &Value,
832 input_rows: u64,
833 ) -> u64 {
834 let key = (rel_type.to_string(), property.to_string());
835 let Some(histogram) = self
836 .rel_property_histograms
837 .get(&key)
838 .filter(|values| !values.is_empty())
839 else {
840 return input_rows.div_ceil(RANGE_SELECTIVITY_DIVISOR).max(1);
841 };
842 let matching_values = histogram
843 .iter()
844 .filter(|candidate| compare_histogram_value(candidate, op, value))
845 .count() as u64;
846 estimate_histogram_rows(
847 input_rows,
848 matching_values,
849 histogram.len() as u64,
850 self.sampled_rel_property_histograms
851 .get(&key)
852 .copied()
853 .unwrap_or(false),
854 )
855 }
856
857 pub(super) fn estimate_range_rows(
858 &self,
859 label: &str,
860 property: &str,
861 op: ComparisonOp,
862 value: &Value,
863 ) -> u64 {
864 let label_count = self.label_count(label);
865 let key = (label.to_string(), property.to_string());
866 let Some(histogram) = self
867 .property_histograms
868 .get(&key)
869 .filter(|values| !values.is_empty())
870 else {
871 return label_count.div_ceil(RANGE_SELECTIVITY_DIVISOR).max(1);
872 };
873 let matching_values = histogram
874 .iter()
875 .filter(|candidate| compare_histogram_value(candidate, op, value))
876 .count() as u64;
877 estimate_histogram_rows(
878 label_count,
879 matching_values,
880 histogram.len() as u64,
881 self.sampled_property_histograms
882 .get(&key)
883 .copied()
884 .unwrap_or(false),
885 )
886 }
887
888 pub(super) fn estimate_property_eq_rows(
889 &self,
890 label: &str,
891 property: &str,
892 input_rows: u64,
893 ) -> u64 {
894 if let Some(rows) = self.query_local_property_eq_rows(label, property) {
895 return rows.min(input_rows).max(1);
896 }
897 input_rows
898 .div_ceil(self.distinct_count(label, property).max(1))
899 .max(1)
900 }
901
902 pub(super) fn estimate_property_not_eq_rows(
903 &self,
904 label: &str,
905 property: &str,
906 input_rows: u64,
907 ) -> u64 {
908 input_rows.saturating_sub(self.estimate_property_eq_rows(label, property, input_rows))
909 }
910
911 pub(super) fn estimate_property_in_rows(
912 &self,
913 label: &str,
914 property: &str,
915 values: &[Value],
916 input_rows: u64,
917 ) -> u64 {
918 if values.is_empty() {
919 return 0;
920 }
921 let distinct_count = self.distinct_count(label, property).max(1);
922 let value_count = values
923 .iter()
924 .collect::<BTreeSet<_>>()
925 .len()
926 .min(distinct_count as usize) as u64;
927 input_rows
928 .saturating_mul(value_count)
929 .div_ceil(distinct_count)
930 .max(1)
931 }
932
933 pub(super) fn estimate_property_string_match_rows(
934 &self,
935 label: &str,
936 property: &str,
937 input_rows: u64,
938 max_divisor: u64,
939 ) -> u64 {
940 let divisor = self
941 .distinct_count(label, property)
942 .max(1)
943 .min(max_divisor)
944 .max(1);
945 input_rows.div_ceil(divisor).max(1)
946 }
947
948 pub(super) fn estimate_property_null_rows(
949 &self,
950 label: &str,
951 property: &str,
952 input_rows: u64,
953 ) -> u64 {
954 let distinct_count = self.distinct_count(label, property).max(1);
955 input_rows
956 .div_ceil(distinct_count.min(NULL_SELECTIVITY_DIVISOR_CAP))
957 .max(1)
958 }
959
960 pub(super) fn estimate_property_not_null_rows(
961 &self,
962 label: &str,
963 property: &str,
964 input_rows: u64,
965 ) -> u64 {
966 input_rows
967 .saturating_sub(self.estimate_property_null_rows(label, property, input_rows))
968 .max(1)
969 }
970
971 pub(super) fn estimate_property_range_rows(
972 &self,
973 label: &str,
974 property: &str,
975 op: ComparisonOp,
976 value: &Value,
977 input_rows: u64,
978 ) -> u64 {
979 let key = (label.to_string(), property.to_string());
980 let Some(histogram) = self
981 .property_histograms
982 .get(&key)
983 .filter(|values| !values.is_empty())
984 else {
985 return input_rows.div_ceil(RANGE_SELECTIVITY_DIVISOR).max(1);
986 };
987 let matching_values = histogram
988 .iter()
989 .filter(|candidate| compare_histogram_value(candidate, op, value))
990 .count() as u64;
991 estimate_histogram_rows(
992 input_rows,
993 matching_values,
994 histogram.len() as u64,
995 self.sampled_property_histograms
996 .get(&key)
997 .copied()
998 .unwrap_or(false),
999 )
1000 }
1001
1002 pub(super) fn estimate_range_bounds_rows(
1003 &self,
1004 label: &str,
1005 property: &str,
1006 lower: Option<&ValueRangeBound>,
1007 upper: Option<&ValueRangeBound>,
1008 ) -> u64 {
1009 let label_count = self.label_count(label);
1010 let key = (label.to_string(), property.to_string());
1011 let Some(histogram) = self
1012 .property_histograms
1013 .get(&key)
1014 .filter(|values| !values.is_empty())
1015 else {
1016 return label_count.div_ceil(RANGE_SELECTIVITY_DIVISOR).max(1);
1017 };
1018 let matching_values = histogram
1019 .iter()
1020 .filter(|candidate| range_bound_matches(candidate, lower, upper))
1021 .count() as u64;
1022 estimate_histogram_rows(
1023 label_count,
1024 matching_values,
1025 histogram.len() as u64,
1026 self.sampled_property_histograms
1027 .get(&key)
1028 .copied()
1029 .unwrap_or(false),
1030 )
1031 }
1032
1033 pub(super) fn estimate_expand_rows(
1034 &self,
1035 source_label: &str,
1036 rel_type: &str,
1037 rel_properties: &BTreeMap<String, Value>,
1038 target_label: &str,
1039 min_hops: usize,
1040 max_hops: usize,
1041 ) -> ExpandEstimate {
1042 let path_count = self
1043 .path_counts
1044 .get(&(
1045 source_label.to_string(),
1046 rel_type.to_string(),
1047 target_label.to_string(),
1048 ))
1049 .copied();
1050 let rel_count = self.rel_type_counts.get(rel_type).copied().unwrap_or(1);
1051 let source_count = self
1052 .rel_type_source_counts
1053 .get(rel_type)
1054 .copied()
1055 .unwrap_or_else(|| self.label_count(source_label).max(1));
1056 let average_fanout = rel_count.div_ceil(source_count.max(1)).max(1);
1057 let one_hop = path_count.unwrap_or_else(|| {
1058 self.label_count(source_label)
1059 .min(self.label_count(target_label))
1060 .max(1)
1061 });
1062 let property_distinct_product = rel_properties
1063 .keys()
1064 .map(|property| self.rel_property_distinct_count(rel_type, property).max(1))
1065 .fold(1_u64, |acc, value| acc.saturating_mul(value))
1066 .max(1);
1067 let mut estimated_rows = 0_u64;
1068 let mut hop_rows = one_hop.max(1);
1069 let mut hop_estimates = Vec::new();
1070 for hop in 1..=max_hops.max(1) {
1071 let exact_hop_rows = self
1072 .bounded_path_counts
1073 .get(&(
1074 source_label.to_string(),
1075 rel_type.to_string(),
1076 target_label.to_string(),
1077 hop,
1078 ))
1079 .copied();
1080 let current_hop_rows = exact_hop_rows
1081 .unwrap_or(hop_rows)
1082 .div_ceil(property_distinct_product)
1083 .max(1);
1084 hop_estimates.push(HopEstimate {
1085 hop,
1086 rows: current_hop_rows,
1087 exact: exact_hop_rows.is_some(),
1088 });
1089 if hop >= min_hops {
1090 estimated_rows = estimated_rows.saturating_add(current_hop_rows);
1091 }
1092 hop_rows = current_hop_rows.max(1).saturating_mul(average_fanout);
1093 }
1094 ExpandEstimate {
1095 path_count,
1096 rel_count,
1097 source_count,
1098 average_fanout,
1099 property_distinct_product,
1100 estimated_rows: estimated_rows.max(1),
1101 hop_estimates,
1102 }
1103 }
1104}
1105
1106fn estimate_histogram_rows(
1107 input_rows: u64,
1108 matching_values: u64,
1109 histogram_values: u64,
1110 sampled: bool,
1111) -> u64 {
1112 let (matching_values, histogram_values) = if sampled {
1116 (
1117 matching_values.saturating_add(SAMPLED_HISTOGRAM_MATCH_PSEUDOCOUNT),
1118 histogram_values.saturating_add(SAMPLED_HISTOGRAM_TOTAL_PSEUDOCOUNT),
1119 )
1120 } else {
1121 (matching_values, histogram_values)
1122 };
1123 input_rows
1124 .saturating_mul(matching_values)
1125 .div_ceil(histogram_values.max(1))
1126 .max(1)
1127}
1128
1129impl OptimizerCatalogIndexes {
1130 pub fn new(
1131 equality_property_indexes: impl IntoIterator<Item = (String, String)>,
1132 composite_property_indexes: impl IntoIterator<Item = (String, Vec<String>)>,
1133 range_property_indexes: impl IntoIterator<Item = (String, String)>,
1134 full_text_property_indexes: impl IntoIterator<Item = (String, String)>,
1135 ) -> Self {
1136 Self {
1137 equality_property_indexes: equality_property_indexes.into_iter().collect(),
1138 composite_property_indexes: composite_property_indexes.into_iter().collect(),
1139 range_property_indexes: range_property_indexes.into_iter().collect(),
1140 full_text_property_indexes: full_text_property_indexes.into_iter().collect(),
1141 }
1142 }
1143}
1144
1145impl OptimizerCatalogStatistics {
1146 pub fn new(
1147 label_counts: impl IntoIterator<Item = (String, u64)>,
1148 rel_type_counts: impl IntoIterator<Item = (String, u64)>,
1149 rel_type_source_counts: impl IntoIterator<Item = (String, u64)>,
1150 path_counts: impl IntoIterator<Item = ((String, String, String), u64)>,
1151 bounded_path_counts: impl IntoIterator<Item = ((String, String, String, usize), u64)>,
1152 property_distinct_counts: impl IntoIterator<Item = ((String, String), u64)>,
1153 property_histograms: impl IntoIterator<Item = ((String, String), Vec<Value>)>,
1154 ) -> Self {
1155 Self {
1156 label_counts: label_counts.into_iter().collect(),
1157 rel_type_counts: rel_type_counts.into_iter().collect(),
1158 rel_type_source_counts: rel_type_source_counts.into_iter().collect(),
1159 rel_type_target_counts: BTreeMap::new(),
1160 path_counts: path_counts.into_iter().collect(),
1161 path_source_distinct_counts: BTreeMap::new(),
1162 path_target_distinct_counts: BTreeMap::new(),
1163 bounded_path_counts: bounded_path_counts.into_iter().collect(),
1164 bounded_path_source_distinct_counts: BTreeMap::new(),
1165 bounded_path_target_distinct_counts: BTreeMap::new(),
1166 property_index_statistics: BTreeMap::new(),
1167 composite_index_statistics: BTreeMap::new(),
1168 property_distinct_counts: property_distinct_counts.into_iter().collect(),
1169 rel_property_distinct_counts: BTreeMap::new(),
1170 property_histograms: property_histograms.into_iter().collect(),
1171 rel_property_histograms: BTreeMap::new(),
1172 sampled_property_histograms: BTreeMap::new(),
1173 sampled_rel_property_histograms: BTreeMap::new(),
1174 }
1175 }
1176
1177 pub fn with_property_index_statistics(
1178 mut self,
1179 statistics: impl IntoIterator<Item = ((String, String), OptimizerIndexStatistics)>,
1180 ) -> Self {
1181 self.property_index_statistics = statistics.into_iter().collect();
1182 self
1183 }
1184
1185 pub fn with_composite_index_statistics(
1186 mut self,
1187 statistics: impl IntoIterator<Item = ((String, Vec<String>), OptimizerIndexStatistics)>,
1188 ) -> Self {
1189 self.composite_index_statistics = statistics.into_iter().collect();
1190 self
1191 }
1192
1193 pub fn with_relationship_property_distinct_counts(
1194 mut self,
1195 rel_property_distinct_counts: impl IntoIterator<Item = ((String, String), u64)>,
1196 ) -> Self {
1197 self.rel_property_distinct_counts = rel_property_distinct_counts.into_iter().collect();
1198 self
1199 }
1200
1201 pub fn with_relationship_type_target_counts(
1202 mut self,
1203 rel_type_target_counts: impl IntoIterator<Item = (String, u64)>,
1204 ) -> Self {
1205 self.rel_type_target_counts = rel_type_target_counts.into_iter().collect();
1206 self
1207 }
1208
1209 pub fn with_path_source_distinct_counts(
1210 mut self,
1211 path_source_distinct_counts: impl IntoIterator<Item = ((String, String, String), u64)>,
1212 ) -> Self {
1213 self.path_source_distinct_counts = path_source_distinct_counts.into_iter().collect();
1214 self
1215 }
1216
1217 pub fn with_path_target_distinct_counts(
1218 mut self,
1219 path_target_distinct_counts: impl IntoIterator<Item = ((String, String, String), u64)>,
1220 ) -> Self {
1221 self.path_target_distinct_counts = path_target_distinct_counts.into_iter().collect();
1222 self
1223 }
1224
1225 pub fn with_bounded_path_source_distinct_counts(
1226 mut self,
1227 bounded_path_source_distinct_counts: impl IntoIterator<
1228 Item = ((String, String, String, usize), u64),
1229 >,
1230 ) -> Self {
1231 self.bounded_path_source_distinct_counts =
1232 bounded_path_source_distinct_counts.into_iter().collect();
1233 self
1234 }
1235
1236 pub fn with_bounded_path_target_distinct_counts(
1237 mut self,
1238 bounded_path_target_distinct_counts: impl IntoIterator<
1239 Item = ((String, String, String, usize), u64),
1240 >,
1241 ) -> Self {
1242 self.bounded_path_target_distinct_counts =
1243 bounded_path_target_distinct_counts.into_iter().collect();
1244 self
1245 }
1246
1247 pub fn with_relationship_property_histograms(
1248 mut self,
1249 rel_property_histograms: impl IntoIterator<Item = ((String, String), Vec<Value>)>,
1250 ) -> Self {
1251 self.rel_property_histograms = rel_property_histograms.into_iter().collect();
1252 self
1253 }
1254
1255 pub fn with_sampled_property_histograms(
1256 mut self,
1257 sampled_property_histograms: impl IntoIterator<Item = ((String, String), bool)>,
1258 ) -> Self {
1259 self.sampled_property_histograms = sampled_property_histograms.into_iter().collect();
1260 self
1261 }
1262
1263 pub fn with_sampled_relationship_property_histograms(
1264 mut self,
1265 sampled_rel_property_histograms: impl IntoIterator<Item = ((String, String), bool)>,
1266 ) -> Self {
1267 self.sampled_rel_property_histograms =
1268 sampled_rel_property_histograms.into_iter().collect();
1269 self
1270 }
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275 use super::*;
1276
1277 #[test]
1278 fn graph_statistics_catalog_keeps_basic_counts_when_advanced_stats_are_incomplete() {
1279 let mut source_catalog = Catalog::default();
1280 let memory = source_catalog.get_or_create_label("Memory");
1281 let mut statistics = GraphStatistics {
1282 advanced_statistics_complete: false,
1283 label_counts: BTreeMap::from([(memory, 64)]),
1284 property_distinct_counts: BTreeMap::from([((memory, "kind".to_string()), 4)]),
1285 property_histograms: BTreeMap::from([(
1286 (memory, "kind".to_string()),
1287 vec![Value::String("note".to_string())],
1288 )]),
1289 ..GraphStatistics::default()
1290 };
1291
1292 let incomplete = optimizer_catalog_from_graph_statistics(&source_catalog, &statistics);
1293 assert_eq!(incomplete.label_count("Memory"), 64);
1294 assert_eq!(incomplete.known_distinct_count("Memory", "kind"), None);
1295 assert!(incomplete.property_histograms.is_empty());
1296
1297 statistics.advanced_statistics_complete = true;
1298 let complete = optimizer_catalog_from_graph_statistics(&source_catalog, &statistics);
1299 assert_eq!(complete.label_count("Memory"), 64);
1300 assert_eq!(complete.known_distinct_count("Memory", "kind"), Some(4));
1301 assert_eq!(
1302 complete.property_histograms,
1303 BTreeMap::from([(
1304 ("Memory".to_string(), "kind".to_string()),
1305 vec![Value::String("note".to_string())],
1306 )])
1307 );
1308 }
1309
1310 #[test]
1311 fn range_cardinality_honors_half_open_histogram_bounds() {
1312 let catalog = OptimizerCatalog::new(
1313 OptimizerCatalogIndexes::default(),
1314 OptimizerCatalogStatistics::new(
1315 [("Memory".to_string(), 1_000)],
1316 [],
1317 [],
1318 [],
1319 [],
1320 [],
1321 [(
1322 ("Memory".to_string(), "created_at".to_string()),
1323 (0..100).map(Value::Int).collect::<Vec<_>>(),
1324 )],
1325 ),
1326 );
1327
1328 assert_eq!(
1329 catalog.estimate_range_bounds_rows(
1330 "Memory",
1331 "created_at",
1332 Some(&(Value::Int(10), true)),
1333 Some(&(Value::Int(20), false)),
1334 ),
1335 100
1336 );
1337 }
1338
1339 #[test]
1340 fn sampled_histograms_shrink_range_selectivity_toward_default() {
1341 let property_key = ("Memory".to_string(), "score".to_string());
1342 let rel_property_key = ("MENTIONS".to_string(), "score".to_string());
1343 let histogram = (0..100).map(Value::Int).collect::<Vec<_>>();
1344 let statistics = OptimizerCatalogStatistics::new(
1345 [("Memory".to_string(), 1_000)],
1346 [],
1347 [],
1348 [],
1349 [],
1350 [(property_key.clone(), 100)],
1351 [(property_key.clone(), histogram.clone())],
1352 )
1353 .with_relationship_property_histograms([(rel_property_key.clone(), histogram)]);
1354 let exhaustive =
1355 OptimizerCatalog::new(OptimizerCatalogIndexes::default(), statistics.clone());
1356 let sampled = OptimizerCatalog::new(
1357 OptimizerCatalogIndexes::default(),
1358 statistics
1359 .with_sampled_property_histograms([(property_key, true)])
1360 .with_sampled_relationship_property_histograms([(rel_property_key, true)]),
1361 );
1362
1363 assert_eq!(
1364 exhaustive.estimate_range_rows("Memory", "score", ComparisonOp::Lt, &Value::Int(10)),
1365 100
1366 );
1367 assert_eq!(
1368 sampled.estimate_range_rows("Memory", "score", ComparisonOp::Lt, &Value::Int(10)),
1369 108
1370 );
1371 assert_eq!(
1372 sampled.estimate_property_range_rows(
1373 "Memory",
1374 "score",
1375 ComparisonOp::Lt,
1376 &Value::Int(10),
1377 1_000,
1378 ),
1379 108
1380 );
1381 assert_eq!(
1382 sampled.estimate_range_bounds_rows(
1383 "Memory",
1384 "score",
1385 None,
1386 Some(&(Value::Int(10), false)),
1387 ),
1388 108
1389 );
1390 assert_eq!(
1391 exhaustive.estimate_rel_property_range_rows(
1392 "MENTIONS",
1393 "score",
1394 ComparisonOp::Lt,
1395 &Value::Int(10),
1396 1_000,
1397 ),
1398 100
1399 );
1400 assert_eq!(
1401 sampled.estimate_rel_property_range_rows(
1402 "MENTIONS",
1403 "score",
1404 ComparisonOp::Lt,
1405 &Value::Int(10),
1406 1_000,
1407 ),
1408 108
1409 );
1410 }
1411
1412 #[test]
1413 fn index_cardinality_uses_sparse_index_size_and_joint_ndv() {
1414 let statistics = OptimizerCatalogStatistics {
1415 label_counts: BTreeMap::from([("Memory".to_string(), 100)]),
1416 property_index_statistics: BTreeMap::from([(
1417 ("Memory".to_string(), "kind".to_string()),
1418 OptimizerIndexStatistics {
1419 index_size: 10,
1420 distinct_count: 2,
1421 },
1422 )]),
1423 composite_index_statistics: BTreeMap::from([(
1424 (
1425 "Memory".to_string(),
1426 vec!["kind".to_string(), "source_id".to_string()],
1427 ),
1428 OptimizerIndexStatistics {
1429 index_size: 8,
1430 distinct_count: 4,
1431 },
1432 )]),
1433 ..OptimizerCatalogStatistics::default()
1434 };
1435 let catalog = OptimizerCatalog::new(OptimizerCatalogIndexes::default(), statistics);
1436
1437 assert_eq!(catalog.estimate_property_index_eq_rows("Memory", "kind"), 5);
1438 assert_eq!(
1439 catalog.estimate_property_index_in_rows("Memory", "kind", 10),
1440 10
1441 );
1442 assert_eq!(
1443 catalog.estimate_composite_property_index_rows(
1444 "Memory",
1445 &["kind".to_string(), "source_id".to_string()],
1446 ),
1447 2
1448 );
1449 }
1450
1451 #[test]
1452 fn query_local_equality_counts_do_not_replace_whole_index_statistics() {
1453 let catalog = OptimizerCatalog::new(
1454 OptimizerCatalogIndexes::default(),
1455 OptimizerCatalogStatistics {
1456 label_counts: BTreeMap::from([("Item".into(), 100)]),
1457 property_index_statistics: BTreeMap::from([(
1458 ("Item".into(), "id".into()),
1459 OptimizerIndexStatistics {
1460 index_size: 90,
1461 distinct_count: 10,
1462 },
1463 )]),
1464 ..OptimizerCatalogStatistics::default()
1465 },
1466 );
1467 let local = catalog
1468 .clone()
1469 .with_property_equality_row_count("Item".into(), "id".into(), 2);
1470 assert_eq!(catalog.estimate_property_index_eq_rows("Item", "id"), 9);
1471 assert_eq!(local.estimate_property_index_eq_rows("Item", "id"), 2);
1472 assert_eq!(local.estimate_property_eq_rows("Item", "id", 100), 2);
1473 assert_eq!(
1474 local.property_index_statistics,
1475 catalog.property_index_statistics
1476 );
1477 assert_eq!(
1478 local.estimate_property_index_eq_rows("Item", "other"),
1479 catalog.estimate_property_index_eq_rows("Item", "other")
1480 );
1481 let empty = catalog
1482 .clone()
1483 .with_property_equality_row_count("Item".into(), "id".into(), 0);
1484 assert_eq!(empty.estimate_property_index_eq_rows("Item", "id"), 1);
1485 }
1486}