1use crate::cardinality_defaults::JOIN_SELECTIVITY_DIVISOR;
18use crate::cost::PlanCostBreakdown;
19use crate::{RelationalAccessPathDescriptor, RelationalAccessPathKind};
20
21const FULL_SCAN_SETUP_CPU: u64 = 4;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum RelationalJoinCardinality {
25 Inner,
26 PreserveLeft,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum RelationalJoinRightInput {
31 Probe,
34 Materialized,
36 Hash,
38 Merge,
40}
41
42#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
43pub enum RelationalJoinSelectivity {
44 #[default]
48 Unknown,
49 EquiJoin {
54 left_distinct_values: Option<u64>,
55 right_distinct_values: Option<u64>,
56 },
57}
58
59impl RelationalJoinSelectivity {
60 pub const fn equi_join(
61 left_distinct_values: Option<u64>,
62 right_distinct_values: Option<u64>,
63 ) -> Self {
64 Self::EquiJoin {
65 left_distinct_values,
66 right_distinct_values,
67 }
68 }
69
70 fn materialized_divisor(self, left_rows: u64, right_rows: u64) -> u64 {
71 let cap = |distinct_values: u64, rows: u64| distinct_values.max(1).min(rows.max(1));
72 match self {
73 Self::Unknown => JOIN_SELECTIVITY_DIVISOR,
74 Self::EquiJoin {
75 left_distinct_values,
76 right_distinct_values,
77 } => left_distinct_values
78 .map(|distinct_values| cap(distinct_values, left_rows))
79 .into_iter()
80 .chain(
81 right_distinct_values.map(|distinct_values| cap(distinct_values, right_rows)),
82 )
83 .max()
84 .unwrap_or(JOIN_SELECTIVITY_DIVISOR),
85 }
86 }
87}
88
89pub fn estimate_relational_access_cost(estimated_rows: usize) -> PlanCostBreakdown {
90 let rows = u64::try_from(estimated_rows).unwrap_or(u64::MAX).max(1);
91 PlanCostBreakdown::new(rows, rows, 0, 0, 0)
92}
93
94pub fn estimate_relational_access_path_cost(
104 access: &RelationalAccessPathDescriptor,
105) -> PlanCostBreakdown {
106 let rows = u64::try_from(access.estimated_rows)
107 .unwrap_or(u64::MAX)
108 .max(1);
109 match access.kind {
110 RelationalAccessPathKind::FullScan => {
111 PlanCostBreakdown::new(
113 rows,
114 rows.saturating_add(FULL_SCAN_SETUP_CPU),
115 0,
116 rows,
117 rows,
118 )
119 }
120 RelationalAccessPathKind::PrimaryKey => PlanCostBreakdown::new(rows, rows, rows, 0, rows),
121 RelationalAccessPathKind::Index => {
122 let fetches = if access.requires_row_fetch { rows } else { 0 };
123 PlanCostBreakdown::new(
124 rows,
125 rows.saturating_add(fetches),
126 1_u64.saturating_add(fetches),
127 if access.unique_point { 0 } else { rows },
128 rows,
129 )
130 }
131 }
132}
133
134pub fn estimate_relational_probe_join_cost(
137 left: PlanCostBreakdown,
138 inner_estimated_rows: usize,
139 cardinality: RelationalJoinCardinality,
140) -> PlanCostBreakdown {
141 estimate_relational_join_cost(
142 left,
143 estimate_relational_access_cost(inner_estimated_rows),
144 cardinality,
145 RelationalJoinRightInput::Probe,
146 RelationalJoinSelectivity::Unknown,
147 )
148}
149
150pub fn estimate_relational_join_cost(
151 left: PlanCostBreakdown,
152 right: PlanCostBreakdown,
153 cardinality: RelationalJoinCardinality,
154 right_input: RelationalJoinRightInput,
155 selectivity: RelationalJoinSelectivity,
156) -> PlanCostBreakdown {
157 let candidate_pairs = left.estimated_rows.saturating_mul(right.estimated_rows);
158 let joined_rows = match right_input {
159 RelationalJoinRightInput::Probe => candidate_pairs,
160 RelationalJoinRightInput::Materialized
161 | RelationalJoinRightInput::Hash
162 | RelationalJoinRightInput::Merge => candidate_pairs
163 .div_ceil(selectivity.materialized_divisor(left.estimated_rows, right.estimated_rows)),
164 };
165 let estimated_rows = match cardinality {
166 RelationalJoinCardinality::Inner => joined_rows,
167 RelationalJoinCardinality::PreserveLeft => joined_rows.max(left.estimated_rows),
168 };
169 let (right_multiplier, join_cpu) = match right_input {
170 RelationalJoinRightInput::Probe => (left.estimated_rows, 0),
171 RelationalJoinRightInput::Materialized => (1, candidate_pairs),
172 RelationalJoinRightInput::Hash => (
173 1,
174 left.estimated_rows
175 .saturating_add(right.estimated_rows.saturating_mul(2))
176 .saturating_add(joined_rows),
177 ),
178 RelationalJoinRightInput::Merge => (
179 1,
180 left.estimated_rows
181 .saturating_add(right.estimated_rows)
182 .saturating_add(joined_rows),
183 ),
184 };
185 PlanCostBreakdown::new(
186 estimated_rows,
187 left.cpu
188 .saturating_add(right.cpu.saturating_mul(right_multiplier))
189 .saturating_add(join_cpu),
190 left.random_io
191 .saturating_add(right.random_io.saturating_mul(right_multiplier)),
192 left.sequential_io
193 .saturating_add(right.sequential_io.saturating_mul(right_multiplier)),
194 left.output_rows
195 .saturating_add(right.output_rows.saturating_mul(right_multiplier)),
196 )
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::PlanCost;
203
204 #[test]
205 fn equi_join_algorithms_charge_linear_work_and_preserve_component_costs() {
206 for input in [
207 RelationalJoinRightInput::Hash,
208 RelationalJoinRightInput::Merge,
209 ] {
210 let left = PlanCostBreakdown::new(100, 7, 11, 13, 17);
211 let right = PlanCostBreakdown::new(200, 19, 23, 29, 31);
212 let result = estimate_relational_join_cost(
213 left,
214 right,
215 RelationalJoinCardinality::Inner,
216 input,
217 RelationalJoinSelectivity::equi_join(Some(100), Some(200)),
218 );
219 assert_eq!(result.estimated_rows, 100);
220 assert_eq!(
221 result.cpu,
222 26 + if input == RelationalJoinRightInput::Hash {
223 600
224 } else {
225 400
226 }
227 );
228 assert_eq!(
229 (result.random_io, result.sequential_io, result.output_rows),
230 (34, 42, 48)
231 );
232 let empty = PlanCostBreakdown::new(0, 0, 0, 0, 0);
233 let outer = estimate_relational_join_cost(
234 left,
235 empty,
236 RelationalJoinCardinality::PreserveLeft,
237 input,
238 RelationalJoinSelectivity::Unknown,
239 );
240 assert_eq!(outer.estimated_rows, 100);
241 }
242 }
243
244 #[test]
245 fn probe_join_preserves_the_existing_scalar_cost() {
246 let left = estimate_relational_access_cost(5);
247 let cost = estimate_relational_probe_join_cost(left, 2, RelationalJoinCardinality::Inner);
248
249 assert_eq!(
250 cost.as_plan_cost(),
251 PlanCost {
252 estimated_rows: 10,
253 cost: 15,
254 }
255 );
256 }
257
258 #[test]
259 fn materialized_join_charges_the_right_plan_once() {
260 let left = estimate_relational_access_cost(2);
261 let right = estimate_relational_access_cost(3);
262
263 let cost = estimate_relational_join_cost(
264 left,
265 right,
266 RelationalJoinCardinality::Inner,
267 RelationalJoinRightInput::Materialized,
268 RelationalJoinSelectivity::Unknown,
269 );
270
271 assert_eq!(
272 cost.as_plan_cost(),
273 PlanCost {
274 estimated_rows: 1,
275 cost: 11,
276 }
277 );
278 }
279
280 #[test]
281 fn materialized_equi_join_uses_the_larger_distinct_count() {
282 let left = estimate_relational_access_cost(10_000);
283 let right = estimate_relational_access_cost(20_000);
284
285 let cost = estimate_relational_join_cost(
286 left,
287 right,
288 RelationalJoinCardinality::Inner,
289 RelationalJoinRightInput::Materialized,
290 RelationalJoinSelectivity::equi_join(Some(10_000), Some(5_000)),
291 );
292
293 assert_eq!(cost.estimated_rows, 20_000);
294 assert_eq!(cost.cpu, 200_030_000);
295 }
296
297 #[test]
298 fn materialized_equi_join_uses_one_known_distinct_count() {
299 let left = estimate_relational_access_cost(10_000);
300 let right = estimate_relational_access_cost(20_000);
301
302 let cost = estimate_relational_join_cost(
303 left,
304 right,
305 RelationalJoinCardinality::Inner,
306 RelationalJoinRightInput::Materialized,
307 RelationalJoinSelectivity::equi_join(Some(10_000), None),
308 );
309
310 assert_eq!(cost.estimated_rows, 20_000);
311 }
312
313 #[test]
314 fn probe_join_scales_the_right_cost_components_by_outer_rows() {
315 let left = PlanCostBreakdown::new(2, 3, 5, 7, 11);
316 let right = PlanCostBreakdown::new(3, 13, 17, 19, 23);
317
318 let cost = estimate_relational_join_cost(
319 left,
320 right,
321 RelationalJoinCardinality::Inner,
322 RelationalJoinRightInput::Probe,
323 RelationalJoinSelectivity::equi_join(Some(2), Some(3)),
324 );
325
326 assert_eq!(cost.estimated_rows, 6);
327 assert_eq!(cost.cpu, 29);
328 assert_eq!(cost.random_io, 39);
329 assert_eq!(cost.sequential_io, 45);
330 assert_eq!(cost.output_rows, 57);
331 assert_eq!(cost.cost, 209);
332 }
333
334 #[test]
335 fn left_preserving_join_keeps_the_outer_cardinality_floor() {
336 let left = PlanCostBreakdown::new(4, 4, 0, 0, 0);
337 let empty_right = PlanCostBreakdown {
338 estimated_rows: 0,
339 cost: 0,
340 cpu: 0,
341 random_io: 0,
342 sequential_io: 0,
343 output_rows: 0,
344 };
345
346 let cost = estimate_relational_join_cost(
347 left,
348 empty_right,
349 RelationalJoinCardinality::PreserveLeft,
350 RelationalJoinRightInput::Probe,
351 RelationalJoinSelectivity::Unknown,
352 );
353
354 assert_eq!(cost.estimated_rows, 4);
355 assert_eq!(cost.as_plan_cost().cost, 4);
356 }
357}