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