1use crate::cardinality_defaults::JOIN_SELECTIVITY_DIVISOR;
18use crate::cost::PlanCostBreakdown;
19use crate::{RelationalAccessPathDescriptor, RelationalAccessPathKind};
20use hawdb_expression::BindingId;
21use std::collections::BTreeMap;
22use std::num::NonZeroU64;
23
24const FULL_SCAN_SETUP_CPU: u64 = 4;
25const SNAPSHOT_ROOT_ARTIFACT_SETUP_CPU: u64 = 2;
27const SNAPSHOT_ROOT_DESCRIPTOR_READS: u64 = 3;
29
30#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
37pub struct RelationalAccessCostContext {
38 snapshot_rows: Option<RelationalSnapshotRowCost>,
39}
40
41#[derive(Debug, Default, Clone, PartialEq, Eq)]
47pub struct RelationalJoinCostContexts {
48 relations: BTreeMap<BindingId, RelationalAccessCostContext>,
49}
50
51impl RelationalJoinCostContexts {
52 pub fn with_relation(
53 mut self,
54 binding: BindingId,
55 context: RelationalAccessCostContext,
56 ) -> Self {
57 self.relations.insert(binding, context);
58 self
59 }
60
61 pub fn for_relation(&self, binding: BindingId) -> RelationalAccessCostContext {
62 self.relations.get(&binding).copied().unwrap_or_default()
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67struct RelationalSnapshotRowCost {
68 table_rows: NonZeroU64,
69 table_pages: NonZeroU64,
70}
71
72impl RelationalSnapshotRowCost {
73 fn point_work(self, rows: u64) -> SnapshotRootWork {
74 let search_steps = u64::from(u64::BITS - self.table_pages.get().leading_zeros());
75 SnapshotRootWork {
76 cpu: rows.saturating_mul(SNAPSHOT_ROOT_ARTIFACT_SETUP_CPU.saturating_add(search_steps)),
77 random: rows
78 .saturating_mul(search_steps)
79 .saturating_mul(SNAPSHOT_ROOT_DESCRIPTOR_READS),
80 sequential: 0,
81 }
82 }
83}
84
85#[derive(Default)]
86struct SnapshotRootWork {
87 cpu: u64,
88 random: u64,
89 sequential: u64,
90}
91
92impl RelationalAccessCostContext {
93 pub const fn for_snapshot_rows(table_rows: NonZeroU64, table_pages: NonZeroU64) -> Self {
97 Self {
98 snapshot_rows: Some(RelationalSnapshotRowCost {
99 table_rows,
100 table_pages,
101 }),
102 }
103 }
104
105 fn additional_work(
106 self,
107 access: &RelationalAccessPathDescriptor,
108 rows: u64,
109 ) -> SnapshotRootWork {
110 let Some(snapshot) = self.snapshot_rows else {
111 return SnapshotRootWork::default();
112 };
113 match access.kind {
122 RelationalAccessPathKind::FullScan => {
123 let pages = snapshot.table_pages.get();
124 SnapshotRootWork {
125 cpu: pages.saturating_mul(SNAPSHOT_ROOT_ARTIFACT_SETUP_CPU.saturating_add(1)),
126 random: 0,
127 sequential: pages.saturating_mul(SNAPSHOT_ROOT_DESCRIPTOR_READS),
128 }
129 }
130 RelationalAccessPathKind::PrimaryKey => snapshot.point_work(rows),
131 RelationalAccessPathKind::Index if access.requires_row_fetch => {
132 snapshot.point_work(rows)
133 }
134 RelationalAccessPathKind::Index => SnapshotRootWork::default(),
135 }
136 }
137
138 fn locator_fetch_work(self, rows: u64) -> SnapshotRootWork {
139 let Some(snapshot) = self.snapshot_rows else {
140 return SnapshotRootWork::default();
143 };
144 let root = snapshot.point_work(rows);
145 SnapshotRootWork {
146 cpu: rows.saturating_add(root.cpu),
147 random: rows.saturating_add(root.random),
148 sequential: 0,
149 }
150 }
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum RelationalJoinCardinality {
155 Inner,
156 PreserveLeft,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum RelationalJoinRightInput {
161 Probe,
164 Materialized,
166 Hash,
168 Merge,
170}
171
172#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
173pub enum RelationalJoinSelectivity {
174 #[default]
178 Unknown,
179 EquiJoin {
184 left_distinct_values: Option<u64>,
185 right_distinct_values: Option<u64>,
186 },
187}
188
189impl RelationalJoinSelectivity {
190 pub const fn equi_join(
191 left_distinct_values: Option<u64>,
192 right_distinct_values: Option<u64>,
193 ) -> Self {
194 Self::EquiJoin {
195 left_distinct_values,
196 right_distinct_values,
197 }
198 }
199
200 fn materialized_divisor(self, left_rows: u64, right_rows: u64) -> u64 {
201 let cap = |distinct_values: u64, rows: u64| distinct_values.max(1).min(rows.max(1));
202 match self {
203 Self::Unknown => JOIN_SELECTIVITY_DIVISOR,
204 Self::EquiJoin {
205 left_distinct_values,
206 right_distinct_values,
207 } => left_distinct_values
208 .map(|distinct_values| cap(distinct_values, left_rows))
209 .into_iter()
210 .chain(
211 right_distinct_values.map(|distinct_values| cap(distinct_values, right_rows)),
212 )
213 .max()
214 .unwrap_or(JOIN_SELECTIVITY_DIVISOR),
215 }
216 }
217}
218
219pub fn estimate_relational_access_cost(estimated_rows: usize) -> PlanCostBreakdown {
220 let rows = u64::try_from(estimated_rows).unwrap_or(u64::MAX).max(1);
221 PlanCostBreakdown::new(rows, rows, 0, 0, 0)
222}
223
224pub fn estimate_relational_access_path_cost(
234 access: &RelationalAccessPathDescriptor,
235) -> PlanCostBreakdown {
236 estimate_relational_access_path_cost_with_context(
237 access,
238 RelationalAccessCostContext::default(),
239 )
240}
241
242pub fn estimate_relational_access_path_cost_with_context(
247 access: &RelationalAccessPathDescriptor,
248 context: RelationalAccessCostContext,
249) -> PlanCostBreakdown {
250 let rows = u64::try_from(access.estimated_rows)
251 .unwrap_or(u64::MAX)
252 .max(1);
253 let input_rows = match (access.kind, context.snapshot_rows) {
258 (RelationalAccessPathKind::FullScan, Some(snapshot)) => rows.max(snapshot.table_rows.get()),
259 _ => rows,
260 };
261 let (cpu, random, sequential) = match access.kind {
262 RelationalAccessPathKind::FullScan => (
263 input_rows.saturating_add(FULL_SCAN_SETUP_CPU),
264 0,
265 input_rows,
266 ),
267 RelationalAccessPathKind::PrimaryKey => (rows, rows, 0),
268 RelationalAccessPathKind::Index => {
269 let fetches = if access.requires_row_fetch { rows } else { 0 };
270 (
271 rows.saturating_add(fetches),
272 1_u64.saturating_add(fetches),
273 if access.unique_point { 0 } else { rows },
274 )
275 }
276 };
277 let root_work = context.additional_work(access, input_rows);
278 PlanCostBreakdown::new(
279 rows,
280 cpu.saturating_add(root_work.cpu),
281 random.saturating_add(root_work.random),
282 sequential.saturating_add(root_work.sequential),
283 rows,
284 )
285}
286
287pub fn estimate_relational_probe_join_cost(
290 left: PlanCostBreakdown,
291 inner_estimated_rows: usize,
292 cardinality: RelationalJoinCardinality,
293) -> PlanCostBreakdown {
294 estimate_relational_join_cost(
295 left,
296 estimate_relational_access_cost(inner_estimated_rows),
297 cardinality,
298 RelationalJoinRightInput::Probe,
299 RelationalJoinSelectivity::Unknown,
300 )
301}
302
303pub fn estimate_relational_join_cost(
304 left: PlanCostBreakdown,
305 right: PlanCostBreakdown,
306 cardinality: RelationalJoinCardinality,
307 right_input: RelationalJoinRightInput,
308 selectivity: RelationalJoinSelectivity,
309) -> PlanCostBreakdown {
310 estimate_relational_join_cost_with_contexts(
311 left,
312 right,
313 cardinality,
314 right_input,
315 selectivity,
316 RelationalAccessCostContext::default(),
317 RelationalAccessCostContext::default(),
318 )
319}
320
321pub fn estimate_relational_join_cost_with_contexts(
333 left: PlanCostBreakdown,
334 right: PlanCostBreakdown,
335 cardinality: RelationalJoinCardinality,
336 right_input: RelationalJoinRightInput,
337 selectivity: RelationalJoinSelectivity,
338 left_context: RelationalAccessCostContext,
339 right_context: RelationalAccessCostContext,
340) -> PlanCostBreakdown {
341 let candidate_pairs = left.estimated_rows.saturating_mul(right.estimated_rows);
342 let joined_rows = match right_input {
343 RelationalJoinRightInput::Probe => candidate_pairs,
344 RelationalJoinRightInput::Materialized
345 | RelationalJoinRightInput::Hash
346 | RelationalJoinRightInput::Merge => candidate_pairs
347 .div_ceil(selectivity.materialized_divisor(left.estimated_rows, right.estimated_rows)),
348 };
349 let estimated_rows = match cardinality {
350 RelationalJoinCardinality::Inner => joined_rows,
351 RelationalJoinCardinality::PreserveLeft => joined_rows.max(left.estimated_rows),
352 };
353 let (right_multiplier, join_cpu) = match right_input {
354 RelationalJoinRightInput::Probe => (left.estimated_rows, 0),
355 RelationalJoinRightInput::Materialized => (1, candidate_pairs),
356 RelationalJoinRightInput::Hash => (
357 1,
358 left.estimated_rows
359 .saturating_add(right.estimated_rows.saturating_mul(2))
360 .saturating_add(joined_rows),
361 ),
362 RelationalJoinRightInput::Merge => (
363 1,
364 left.estimated_rows
365 .saturating_add(right.estimated_rows)
366 .saturating_add(joined_rows),
367 ),
368 };
369 let (left_replay, right_replay) = if right_input == RelationalJoinRightInput::Hash {
370 let left_fetches = joined_rows.saturating_add(match cardinality {
371 RelationalJoinCardinality::Inner => 0,
372 RelationalJoinCardinality::PreserveLeft => left.estimated_rows,
373 });
374 (
375 left_context.locator_fetch_work(left_fetches),
376 right_context.locator_fetch_work(joined_rows),
377 )
378 } else {
379 (SnapshotRootWork::default(), SnapshotRootWork::default())
380 };
381 PlanCostBreakdown::new(
382 estimated_rows,
383 left.cpu
384 .saturating_add(right.cpu.saturating_mul(right_multiplier))
385 .saturating_add(join_cpu)
386 .saturating_add(left_replay.cpu)
387 .saturating_add(right_replay.cpu),
388 left.random_io
389 .saturating_add(right.random_io.saturating_mul(right_multiplier))
390 .saturating_add(left_replay.random)
391 .saturating_add(right_replay.random),
392 left.sequential_io
393 .saturating_add(right.sequential_io.saturating_mul(right_multiplier)),
394 left.output_rows
395 .saturating_add(right.output_rows.saturating_mul(right_multiplier)),
396 )
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use crate::PlanCost;
403
404 #[test]
405 fn snapshot_hash_locator_replay_preserves_mixed_sources_outer_floor_and_saturation() {
406 let snapshot = |rows, pages| {
407 RelationalAccessCostContext::for_snapshot_rows(
408 NonZeroU64::new(rows).unwrap(),
409 NonZeroU64::new(pages).unwrap(),
410 )
411 };
412 let empty = RelationalAccessCostContext::default();
413 let left_context = snapshot(10, 4);
414 let right_context = snapshot(4, 1);
415 let left = PlanCostBreakdown::new(10, 11, 13, 17, 19);
416 let right = PlanCostBreakdown::new(4, 23, 29, 31, 37);
417 let selectivity = RelationalJoinSelectivity::equi_join(Some(10), Some(4));
418 for (left_source, right_source, cpu, random) in [
422 (empty, empty, 56, 42),
423 (left_context, empty, 80, 82),
424 (empty, right_context, 72, 58),
425 (left_context, right_context, 96, 98),
426 ] {
427 let result = estimate_relational_join_cost_with_contexts(
428 left,
429 right,
430 RelationalJoinCardinality::Inner,
431 RelationalJoinRightInput::Hash,
432 selectivity,
433 left_source,
434 right_source,
435 );
436 assert_eq!(result, PlanCostBreakdown::new(4, cpu, random, 48, 56));
437 }
438 assert_eq!(
442 estimate_relational_join_cost_with_contexts(
443 left,
444 right,
445 RelationalJoinCardinality::PreserveLeft,
446 RelationalJoinRightInput::Hash,
447 selectivity,
448 left_context,
449 right_context,
450 ),
451 PlanCostBreakdown::new(10, 156, 198, 48, 56),
452 );
453 for cardinality in [
454 RelationalJoinCardinality::Inner,
455 RelationalJoinCardinality::PreserveLeft,
456 ] {
457 for input in [
458 RelationalJoinRightInput::Probe,
459 RelationalJoinRightInput::Materialized,
460 RelationalJoinRightInput::Merge,
461 ] {
462 assert_eq!(
463 estimate_relational_join_cost_with_contexts(
464 left,
465 right,
466 cardinality,
467 input,
468 selectivity,
469 left_context,
470 right_context
471 ),
472 estimate_relational_join_cost(left, right, cardinality, input, selectivity),
473 );
474 }
475 assert_eq!(
476 estimate_relational_join_cost_with_contexts(
477 left,
478 right,
479 cardinality,
480 RelationalJoinRightInput::Hash,
481 selectivity,
482 empty,
483 empty
484 ),
485 estimate_relational_join_cost(
486 left,
487 right,
488 cardinality,
489 RelationalJoinRightInput::Hash,
490 selectivity
491 ),
492 );
493 }
494 let max = PlanCostBreakdown::new(u64::MAX, u64::MAX, u64::MAX, u64::MAX, u64::MAX);
495 let max_context = snapshot(u64::MAX, u64::MAX);
496 assert_eq!(
497 estimate_relational_join_cost_with_contexts(
498 max,
499 max,
500 RelationalJoinCardinality::PreserveLeft,
501 RelationalJoinRightInput::Hash,
502 RelationalJoinSelectivity::Unknown,
503 max_context,
504 max_context
505 ),
506 max,
507 );
508 }
509
510 #[test]
511 fn equi_join_algorithms_charge_linear_work_and_preserve_component_costs() {
512 for input in [
513 RelationalJoinRightInput::Hash,
514 RelationalJoinRightInput::Merge,
515 ] {
516 let left = PlanCostBreakdown::new(100, 7, 11, 13, 17);
517 let right = PlanCostBreakdown::new(200, 19, 23, 29, 31);
518 let result = estimate_relational_join_cost(
519 left,
520 right,
521 RelationalJoinCardinality::Inner,
522 input,
523 RelationalJoinSelectivity::equi_join(Some(100), Some(200)),
524 );
525 assert_eq!(result.estimated_rows, 100);
526 assert_eq!(
527 result.cpu,
528 26 + if input == RelationalJoinRightInput::Hash {
529 600
530 } else {
531 400
532 }
533 );
534 assert_eq!(
535 (result.random_io, result.sequential_io, result.output_rows),
536 (34, 42, 48)
537 );
538 let empty = PlanCostBreakdown::new(0, 0, 0, 0, 0);
539 let outer = estimate_relational_join_cost(
540 left,
541 empty,
542 RelationalJoinCardinality::PreserveLeft,
543 input,
544 RelationalJoinSelectivity::Unknown,
545 );
546 assert_eq!(outer.estimated_rows, 100);
547 }
548 }
549
550 #[test]
551 fn probe_join_preserves_the_existing_scalar_cost() {
552 let left = estimate_relational_access_cost(5);
553 let cost = estimate_relational_probe_join_cost(left, 2, RelationalJoinCardinality::Inner);
554
555 assert_eq!(
556 cost.as_plan_cost(),
557 PlanCost {
558 estimated_rows: 10,
559 cost: 15,
560 }
561 );
562 }
563
564 #[test]
565 fn materialized_join_charges_the_right_plan_once() {
566 let left = estimate_relational_access_cost(2);
567 let right = estimate_relational_access_cost(3);
568
569 let cost = estimate_relational_join_cost(
570 left,
571 right,
572 RelationalJoinCardinality::Inner,
573 RelationalJoinRightInput::Materialized,
574 RelationalJoinSelectivity::Unknown,
575 );
576
577 assert_eq!(
578 cost.as_plan_cost(),
579 PlanCost {
580 estimated_rows: 1,
581 cost: 11,
582 }
583 );
584 }
585
586 #[test]
587 fn materialized_equi_join_uses_the_larger_distinct_count() {
588 let left = estimate_relational_access_cost(10_000);
589 let right = estimate_relational_access_cost(20_000);
590
591 let cost = estimate_relational_join_cost(
592 left,
593 right,
594 RelationalJoinCardinality::Inner,
595 RelationalJoinRightInput::Materialized,
596 RelationalJoinSelectivity::equi_join(Some(10_000), Some(5_000)),
597 );
598
599 assert_eq!(cost.estimated_rows, 20_000);
600 assert_eq!(cost.cpu, 200_030_000);
601 }
602
603 #[test]
604 fn materialized_equi_join_uses_one_known_distinct_count() {
605 let left = estimate_relational_access_cost(10_000);
606 let right = estimate_relational_access_cost(20_000);
607
608 let cost = estimate_relational_join_cost(
609 left,
610 right,
611 RelationalJoinCardinality::Inner,
612 RelationalJoinRightInput::Materialized,
613 RelationalJoinSelectivity::equi_join(Some(10_000), None),
614 );
615
616 assert_eq!(cost.estimated_rows, 20_000);
617 }
618
619 #[test]
620 fn probe_join_scales_the_right_cost_components_by_outer_rows() {
621 let left = PlanCostBreakdown::new(2, 3, 5, 7, 11);
622 let right = PlanCostBreakdown::new(3, 13, 17, 19, 23);
623
624 let cost = estimate_relational_join_cost(
625 left,
626 right,
627 RelationalJoinCardinality::Inner,
628 RelationalJoinRightInput::Probe,
629 RelationalJoinSelectivity::equi_join(Some(2), Some(3)),
630 );
631
632 assert_eq!(cost.estimated_rows, 6);
633 assert_eq!(cost.cpu, 29);
634 assert_eq!(cost.random_io, 39);
635 assert_eq!(cost.sequential_io, 45);
636 assert_eq!(cost.output_rows, 57);
637 assert_eq!(cost.cost, 209);
638 }
639
640 #[test]
641 fn left_preserving_join_keeps_the_outer_cardinality_floor() {
642 let left = PlanCostBreakdown::new(4, 4, 0, 0, 0);
643 let empty_right = PlanCostBreakdown {
644 estimated_rows: 0,
645 cost: 0,
646 cpu: 0,
647 random_io: 0,
648 sequential_io: 0,
649 output_rows: 0,
650 };
651
652 let cost = estimate_relational_join_cost(
653 left,
654 empty_right,
655 RelationalJoinCardinality::PreserveLeft,
656 RelationalJoinRightInput::Probe,
657 RelationalJoinSelectivity::Unknown,
658 );
659
660 assert_eq!(cost.estimated_rows, 4);
661 assert_eq!(cost.as_plan_cost().cost, 4);
662 }
663}