1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// SPDX-License-Identifier: BUSL-1.1
//! Cost-based broadcast-vs-shuffle join selection.
//!
//! This is the automatic, ANALYZE-driven half of the distributed-join planner.
//! The manual override (`nodedb.force_shuffle_join`) always wins; when it is
//! off, [`cost_model_picks_shuffle`] decides whether a whole-join shuffle is
//! cheaper than the default broadcast plan, using the per-collection statistics
//! that `ANALYZE` persists into the system catalog.
//!
//! The decision mirrors Postgres' textbook flow: estimate each side's byte
//! size from `row_count * per_row_width`, then defer to
//! `nodedb_cluster::distributed_join::select_strategy`, which returns
//! `Shuffle` only when NEITHER side is small enough to broadcast under the
//! configured threshold.
//!
//! **Graceful fallback:** if EITHER side has never been analyzed (no stats),
//! the cost model returns `false` and the join keeps the default broadcast
//! plan — zero regression for un-analyzed collections.
use ;
use ConvertContext;
/// Per-column fallback width (bytes) used when a column has no recorded
/// `avg_value_len`. A small constant keeps the estimate conservative without
/// assuming wide payloads for un-measured columns.
const DEFAULT_COLUMN_WIDTH_BYTES: usize = 16;
/// Decide whether the cost model selects a shuffle join over a broadcast join.
///
/// Looks up BOTH sides' estimated byte sizes from ANALYZE column statistics.
/// Returns:
/// - `false` if either side has no statistics (never analyzed) — graceful
/// broadcast fallback, no regression for un-analyzed collections.
/// - `false` if either estimate is zero (empty collection) — broadcast is
/// trivially correct and cheaper.
/// - `select_strategy(left, right, threshold) == Shuffle` otherwise.
///
/// `left_collection` / `right_collection` are the RAW (non-db-qualified)
/// collection names, matching how `ANALYZE` keys its persisted stats.
pub
/// Estimate a collection's on-the-wire size in bytes from ANALYZE statistics.
///
/// Returns `None` when the collection has no statistics at all (never analyzed)
/// — the caller treats this as "broadcast". The estimate is:
///
/// ```text
/// per_row_width = Σ_columns (avg_value_len OR DEFAULT_COLUMN_WIDTH_BYTES)
/// estimated_bytes = row_count * per_row_width (saturating)
/// ```
///
/// `row_count` is identical across a collection's columns (it is the table's
/// row count at ANALYZE time), so any column's value is representative; we take
/// the maximum to be robust against partially-written stat rows.