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 {
121 RelationalAccessPathKind::FullScan => {
122 let pages = (u128::from(rows) * u128::from(snapshot.table_pages.get()))
123 .div_ceil(u128::from(snapshot.table_rows.get()))
124 .min(u128::from(snapshot.table_pages.get())) as u64;
125 SnapshotRootWork {
126 cpu: pages.saturating_mul(SNAPSHOT_ROOT_ARTIFACT_SETUP_CPU.saturating_add(1)),
127 random: 0,
128 sequential: pages.saturating_mul(SNAPSHOT_ROOT_DESCRIPTOR_READS),
129 }
130 }
131 RelationalAccessPathKind::PrimaryKey => snapshot.point_work(rows),
132 RelationalAccessPathKind::Index if access.requires_row_fetch => {
133 snapshot.point_work(rows)
134 }
135 RelationalAccessPathKind::Index => SnapshotRootWork::default(),
136 }
137 }
138
139 fn locator_fetch_work(self, rows: u64) -> SnapshotRootWork {
140 let Some(snapshot) = self.snapshot_rows else {
141 return SnapshotRootWork::default();
144 };
145 let root = snapshot.point_work(rows);
146 SnapshotRootWork {
147 cpu: rows.saturating_add(root.cpu),
148 random: rows.saturating_add(root.random),
149 sequential: 0,
150 }
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum RelationalJoinCardinality {
156 Inner,
157 PreserveLeft,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum RelationalJoinRightInput {
162 Probe,
165 Materialized,
167 Hash,
169 Merge,
171}
172
173#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
174pub enum RelationalJoinSelectivity {
175 #[default]
179 Unknown,
180 EquiJoin {
185 left_distinct_values: Option<u64>,
186 right_distinct_values: Option<u64>,
187 },
188}
189
190impl RelationalJoinSelectivity {
191 pub const fn equi_join(
192 left_distinct_values: Option<u64>,
193 right_distinct_values: Option<u64>,
194 ) -> Self {
195 Self::EquiJoin {
196 left_distinct_values,
197 right_distinct_values,
198 }
199 }
200
201 fn materialized_divisor(self, left_rows: u64, right_rows: u64) -> u64 {
202 let cap = |distinct_values: u64, rows: u64| distinct_values.max(1).min(rows.max(1));
203 match self {
204 Self::Unknown => JOIN_SELECTIVITY_DIVISOR,
205 Self::EquiJoin {
206 left_distinct_values,
207 right_distinct_values,
208 } => left_distinct_values
209 .map(|distinct_values| cap(distinct_values, left_rows))
210 .into_iter()
211 .chain(
212 right_distinct_values.map(|distinct_values| cap(distinct_values, right_rows)),
213 )
214 .max()
215 .unwrap_or(JOIN_SELECTIVITY_DIVISOR),
216 }
217 }
218}
219
220pub fn estimate_relational_access_cost(estimated_rows: usize) -> PlanCostBreakdown {
221 let rows = u64::try_from(estimated_rows).unwrap_or(u64::MAX).max(1);
222 PlanCostBreakdown::new(rows, rows, 0, 0, 0)
223}
224
225pub fn estimate_relational_access_path_cost(
235 access: &RelationalAccessPathDescriptor,
236) -> PlanCostBreakdown {
237 estimate_relational_access_path_cost_with_context(
238 access,
239 RelationalAccessCostContext::default(),
240 )
241}
242
243pub fn estimate_relational_access_path_cost_with_context(
248 access: &RelationalAccessPathDescriptor,
249 context: RelationalAccessCostContext,
250) -> PlanCostBreakdown {
251 let rows = u64::try_from(access.estimated_rows)
252 .unwrap_or(u64::MAX)
253 .max(1);
254 let (cpu, random, sequential) = match access.kind {
255 RelationalAccessPathKind::FullScan => (rows.saturating_add(FULL_SCAN_SETUP_CPU), 0, rows),
256 RelationalAccessPathKind::PrimaryKey => (rows, rows, 0),
257 RelationalAccessPathKind::Index => {
258 let fetches = if access.requires_row_fetch { rows } else { 0 };
259 (
260 rows.saturating_add(fetches),
261 1_u64.saturating_add(fetches),
262 if access.unique_point { 0 } else { rows },
263 )
264 }
265 };
266 let root_work = context.additional_work(access, rows);
267 PlanCostBreakdown::new(
268 rows,
269 cpu.saturating_add(root_work.cpu),
270 random.saturating_add(root_work.random),
271 sequential.saturating_add(root_work.sequential),
272 rows,
273 )
274}
275
276pub fn estimate_relational_probe_join_cost(
279 left: PlanCostBreakdown,
280 inner_estimated_rows: usize,
281 cardinality: RelationalJoinCardinality,
282) -> PlanCostBreakdown {
283 estimate_relational_join_cost(
284 left,
285 estimate_relational_access_cost(inner_estimated_rows),
286 cardinality,
287 RelationalJoinRightInput::Probe,
288 RelationalJoinSelectivity::Unknown,
289 )
290}
291
292pub fn estimate_relational_join_cost(
293 left: PlanCostBreakdown,
294 right: PlanCostBreakdown,
295 cardinality: RelationalJoinCardinality,
296 right_input: RelationalJoinRightInput,
297 selectivity: RelationalJoinSelectivity,
298) -> PlanCostBreakdown {
299 estimate_relational_join_cost_with_contexts(
300 left,
301 right,
302 cardinality,
303 right_input,
304 selectivity,
305 RelationalAccessCostContext::default(),
306 RelationalAccessCostContext::default(),
307 )
308}
309
310pub fn estimate_relational_join_cost_with_contexts(
322 left: PlanCostBreakdown,
323 right: PlanCostBreakdown,
324 cardinality: RelationalJoinCardinality,
325 right_input: RelationalJoinRightInput,
326 selectivity: RelationalJoinSelectivity,
327 left_context: RelationalAccessCostContext,
328 right_context: RelationalAccessCostContext,
329) -> PlanCostBreakdown {
330 let candidate_pairs = left.estimated_rows.saturating_mul(right.estimated_rows);
331 let joined_rows = match right_input {
332 RelationalJoinRightInput::Probe => candidate_pairs,
333 RelationalJoinRightInput::Materialized
334 | RelationalJoinRightInput::Hash
335 | RelationalJoinRightInput::Merge => candidate_pairs
336 .div_ceil(selectivity.materialized_divisor(left.estimated_rows, right.estimated_rows)),
337 };
338 let estimated_rows = match cardinality {
339 RelationalJoinCardinality::Inner => joined_rows,
340 RelationalJoinCardinality::PreserveLeft => joined_rows.max(left.estimated_rows),
341 };
342 let (right_multiplier, join_cpu) = match right_input {
343 RelationalJoinRightInput::Probe => (left.estimated_rows, 0),
344 RelationalJoinRightInput::Materialized => (1, candidate_pairs),
345 RelationalJoinRightInput::Hash => (
346 1,
347 left.estimated_rows
348 .saturating_add(right.estimated_rows.saturating_mul(2))
349 .saturating_add(joined_rows),
350 ),
351 RelationalJoinRightInput::Merge => (
352 1,
353 left.estimated_rows
354 .saturating_add(right.estimated_rows)
355 .saturating_add(joined_rows),
356 ),
357 };
358 let (left_replay, right_replay) = if right_input == RelationalJoinRightInput::Hash {
359 let left_fetches = joined_rows.saturating_add(match cardinality {
360 RelationalJoinCardinality::Inner => 0,
361 RelationalJoinCardinality::PreserveLeft => left.estimated_rows,
362 });
363 (
364 left_context.locator_fetch_work(left_fetches),
365 right_context.locator_fetch_work(joined_rows),
366 )
367 } else {
368 (SnapshotRootWork::default(), SnapshotRootWork::default())
369 };
370 PlanCostBreakdown::new(
371 estimated_rows,
372 left.cpu
373 .saturating_add(right.cpu.saturating_mul(right_multiplier))
374 .saturating_add(join_cpu)
375 .saturating_add(left_replay.cpu)
376 .saturating_add(right_replay.cpu),
377 left.random_io
378 .saturating_add(right.random_io.saturating_mul(right_multiplier))
379 .saturating_add(left_replay.random)
380 .saturating_add(right_replay.random),
381 left.sequential_io
382 .saturating_add(right.sequential_io.saturating_mul(right_multiplier)),
383 left.output_rows
384 .saturating_add(right.output_rows.saturating_mul(right_multiplier)),
385 )
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::PlanCost;
392
393 #[test]
394 fn snapshot_hash_locator_replay_preserves_mixed_sources_outer_floor_and_saturation() {
395 let snapshot = |rows, pages| {
396 RelationalAccessCostContext::for_snapshot_rows(
397 NonZeroU64::new(rows).unwrap(),
398 NonZeroU64::new(pages).unwrap(),
399 )
400 };
401 let empty = RelationalAccessCostContext::default();
402 let left_context = snapshot(10, 4);
403 let right_context = snapshot(4, 1);
404 let left = PlanCostBreakdown::new(10, 11, 13, 17, 19);
405 let right = PlanCostBreakdown::new(4, 23, 29, 31, 37);
406 let selectivity = RelationalJoinSelectivity::equi_join(Some(10), Some(4));
407 for (left_source, right_source, cpu, random) in [
411 (empty, empty, 56, 42),
412 (left_context, empty, 80, 82),
413 (empty, right_context, 72, 58),
414 (left_context, right_context, 96, 98),
415 ] {
416 let result = estimate_relational_join_cost_with_contexts(
417 left,
418 right,
419 RelationalJoinCardinality::Inner,
420 RelationalJoinRightInput::Hash,
421 selectivity,
422 left_source,
423 right_source,
424 );
425 assert_eq!(result, PlanCostBreakdown::new(4, cpu, random, 48, 56));
426 }
427 assert_eq!(
431 estimate_relational_join_cost_with_contexts(
432 left,
433 right,
434 RelationalJoinCardinality::PreserveLeft,
435 RelationalJoinRightInput::Hash,
436 selectivity,
437 left_context,
438 right_context,
439 ),
440 PlanCostBreakdown::new(10, 156, 198, 48, 56),
441 );
442 for cardinality in [
443 RelationalJoinCardinality::Inner,
444 RelationalJoinCardinality::PreserveLeft,
445 ] {
446 for input in [
447 RelationalJoinRightInput::Probe,
448 RelationalJoinRightInput::Materialized,
449 RelationalJoinRightInput::Merge,
450 ] {
451 assert_eq!(
452 estimate_relational_join_cost_with_contexts(
453 left,
454 right,
455 cardinality,
456 input,
457 selectivity,
458 left_context,
459 right_context
460 ),
461 estimate_relational_join_cost(left, right, cardinality, input, selectivity),
462 );
463 }
464 assert_eq!(
465 estimate_relational_join_cost_with_contexts(
466 left,
467 right,
468 cardinality,
469 RelationalJoinRightInput::Hash,
470 selectivity,
471 empty,
472 empty
473 ),
474 estimate_relational_join_cost(
475 left,
476 right,
477 cardinality,
478 RelationalJoinRightInput::Hash,
479 selectivity
480 ),
481 );
482 }
483 let max = PlanCostBreakdown::new(u64::MAX, u64::MAX, u64::MAX, u64::MAX, u64::MAX);
484 let max_context = snapshot(u64::MAX, u64::MAX);
485 assert_eq!(
486 estimate_relational_join_cost_with_contexts(
487 max,
488 max,
489 RelationalJoinCardinality::PreserveLeft,
490 RelationalJoinRightInput::Hash,
491 RelationalJoinSelectivity::Unknown,
492 max_context,
493 max_context
494 ),
495 max,
496 );
497 }
498
499 #[test]
500 fn equi_join_algorithms_charge_linear_work_and_preserve_component_costs() {
501 for input in [
502 RelationalJoinRightInput::Hash,
503 RelationalJoinRightInput::Merge,
504 ] {
505 let left = PlanCostBreakdown::new(100, 7, 11, 13, 17);
506 let right = PlanCostBreakdown::new(200, 19, 23, 29, 31);
507 let result = estimate_relational_join_cost(
508 left,
509 right,
510 RelationalJoinCardinality::Inner,
511 input,
512 RelationalJoinSelectivity::equi_join(Some(100), Some(200)),
513 );
514 assert_eq!(result.estimated_rows, 100);
515 assert_eq!(
516 result.cpu,
517 26 + if input == RelationalJoinRightInput::Hash {
518 600
519 } else {
520 400
521 }
522 );
523 assert_eq!(
524 (result.random_io, result.sequential_io, result.output_rows),
525 (34, 42, 48)
526 );
527 let empty = PlanCostBreakdown::new(0, 0, 0, 0, 0);
528 let outer = estimate_relational_join_cost(
529 left,
530 empty,
531 RelationalJoinCardinality::PreserveLeft,
532 input,
533 RelationalJoinSelectivity::Unknown,
534 );
535 assert_eq!(outer.estimated_rows, 100);
536 }
537 }
538
539 #[test]
540 fn probe_join_preserves_the_existing_scalar_cost() {
541 let left = estimate_relational_access_cost(5);
542 let cost = estimate_relational_probe_join_cost(left, 2, RelationalJoinCardinality::Inner);
543
544 assert_eq!(
545 cost.as_plan_cost(),
546 PlanCost {
547 estimated_rows: 10,
548 cost: 15,
549 }
550 );
551 }
552
553 #[test]
554 fn materialized_join_charges_the_right_plan_once() {
555 let left = estimate_relational_access_cost(2);
556 let right = estimate_relational_access_cost(3);
557
558 let cost = estimate_relational_join_cost(
559 left,
560 right,
561 RelationalJoinCardinality::Inner,
562 RelationalJoinRightInput::Materialized,
563 RelationalJoinSelectivity::Unknown,
564 );
565
566 assert_eq!(
567 cost.as_plan_cost(),
568 PlanCost {
569 estimated_rows: 1,
570 cost: 11,
571 }
572 );
573 }
574
575 #[test]
576 fn materialized_equi_join_uses_the_larger_distinct_count() {
577 let left = estimate_relational_access_cost(10_000);
578 let right = estimate_relational_access_cost(20_000);
579
580 let cost = estimate_relational_join_cost(
581 left,
582 right,
583 RelationalJoinCardinality::Inner,
584 RelationalJoinRightInput::Materialized,
585 RelationalJoinSelectivity::equi_join(Some(10_000), Some(5_000)),
586 );
587
588 assert_eq!(cost.estimated_rows, 20_000);
589 assert_eq!(cost.cpu, 200_030_000);
590 }
591
592 #[test]
593 fn materialized_equi_join_uses_one_known_distinct_count() {
594 let left = estimate_relational_access_cost(10_000);
595 let right = estimate_relational_access_cost(20_000);
596
597 let cost = estimate_relational_join_cost(
598 left,
599 right,
600 RelationalJoinCardinality::Inner,
601 RelationalJoinRightInput::Materialized,
602 RelationalJoinSelectivity::equi_join(Some(10_000), None),
603 );
604
605 assert_eq!(cost.estimated_rows, 20_000);
606 }
607
608 #[test]
609 fn probe_join_scales_the_right_cost_components_by_outer_rows() {
610 let left = PlanCostBreakdown::new(2, 3, 5, 7, 11);
611 let right = PlanCostBreakdown::new(3, 13, 17, 19, 23);
612
613 let cost = estimate_relational_join_cost(
614 left,
615 right,
616 RelationalJoinCardinality::Inner,
617 RelationalJoinRightInput::Probe,
618 RelationalJoinSelectivity::equi_join(Some(2), Some(3)),
619 );
620
621 assert_eq!(cost.estimated_rows, 6);
622 assert_eq!(cost.cpu, 29);
623 assert_eq!(cost.random_io, 39);
624 assert_eq!(cost.sequential_io, 45);
625 assert_eq!(cost.output_rows, 57);
626 assert_eq!(cost.cost, 209);
627 }
628
629 #[test]
630 fn left_preserving_join_keeps_the_outer_cardinality_floor() {
631 let left = PlanCostBreakdown::new(4, 4, 0, 0, 0);
632 let empty_right = PlanCostBreakdown {
633 estimated_rows: 0,
634 cost: 0,
635 cpu: 0,
636 random_io: 0,
637 sequential_io: 0,
638 output_rows: 0,
639 };
640
641 let cost = estimate_relational_join_cost(
642 left,
643 empty_right,
644 RelationalJoinCardinality::PreserveLeft,
645 RelationalJoinRightInput::Probe,
646 RelationalJoinSelectivity::Unknown,
647 );
648
649 assert_eq!(cost.estimated_rows, 4);
650 assert_eq!(cost.as_plan_cost().cost, 4);
651 }
652}