1use std::collections::HashMap;
9use std::sync::{Arc, RwLock};
10
11use crate::{LogicalPlan, NodeOp};
12
13use super::CostModel;
14
15#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
17pub struct ColumnCboStats {
18 pub name: String,
19 pub ndv: Option<u64>,
21 pub min: Option<String>,
23 pub max: Option<String>,
25 pub null_count: Option<u64>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
30pub struct TableCboStats {
31 pub table: String,
32 pub row_count: Option<u64>,
33 pub ndv: Option<u64>,
34 pub avg_row_bytes: Option<u64>,
35 #[serde(default)]
37 pub columns: Vec<ColumnCboStats>,
38}
39
40impl TableCboStats {
41 pub fn new(table: impl Into<String>) -> Self {
42 Self {
43 table: table.into(),
44 ..Default::default()
45 }
46 }
47
48 #[must_use]
49 pub fn with_row_count(mut self, n: u64) -> Self {
50 self.row_count = Some(n);
51 self
52 }
53
54 #[must_use]
55 pub fn with_ndv(mut self, n: u64) -> Self {
56 self.ndv = Some(n);
57 self
58 }
59
60 #[must_use]
61 pub fn with_avg_row_bytes(mut self, n: u64) -> Self {
62 self.avg_row_bytes = Some(n);
63 self
64 }
65}
66
67#[derive(Debug, Default, Clone)]
68pub struct TableStatsRegistry {
69 inner: Arc<RwLock<HashMap<String, TableCboStats>>>,
70}
71
72pub fn global_table_stats() -> &'static TableStatsRegistry {
80 static GLOBAL: std::sync::OnceLock<TableStatsRegistry> = std::sync::OnceLock::new();
81 GLOBAL.get_or_init(TableStatsRegistry::new)
82}
83
84impl TableStatsRegistry {
85 pub fn new() -> Self {
86 Self::default()
87 }
88
89 pub fn put(&self, stats: TableCboStats) -> Option<TableCboStats> {
90 match self.inner.write() {
91 Ok(mut g) => g.insert(stats.table.clone(), stats),
92 Err(p) => p.into_inner().insert(stats.table.clone(), stats),
93 }
94 }
95
96 pub fn get(&self, table: &str) -> Option<TableCboStats> {
97 match self.inner.read() {
98 Ok(g) => g.get(table).cloned(),
99 Err(p) => p.into_inner().get(table).cloned(),
100 }
101 }
102
103 pub fn remove(&self, table: &str) -> Option<TableCboStats> {
104 match self.inner.write() {
105 Ok(mut g) => g.remove(table),
106 Err(p) => p.into_inner().remove(table),
107 }
108 }
109
110 pub fn len(&self) -> usize {
111 match self.inner.read() {
112 Ok(g) => g.len(),
113 Err(p) => p.into_inner().len(),
114 }
115 }
116
117 pub fn is_empty(&self) -> bool {
118 self.len() == 0
119 }
120
121 pub fn entries(&self) -> Vec<(String, TableCboStats)> {
122 match self.inner.read() {
123 Ok(g) => g.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
124 Err(p) => p
125 .into_inner()
126 .iter()
127 .map(|(k, v)| (k.clone(), v.clone()))
128 .collect(),
129 }
130 }
131}
132
133pub struct CboCostModel {
135 pub registry: TableStatsRegistry,
136}
137
138impl std::fmt::Debug for CboCostModel {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct("CboCostModel")
141 .field("registry_entries", &self.registry.len())
142 .finish()
143 }
144}
145
146impl Default for CboCostModel {
147 fn default() -> Self {
148 Self {
149 registry: TableStatsRegistry::new(),
150 }
151 }
152}
153
154impl CostModel for CboCostModel {
155 fn estimate(&self, plan: &LogicalPlan) -> super::Cost {
156 const DEFAULT_ROWS: u64 = 10_000;
157 let mut cpu_nanos: u64 = 0;
158 let mut memory_bytes: u64 = 0;
159 let mut network_bytes: u64 = 0;
160
161 for node in plan.nodes() {
162 let (rows, row_bytes) = match node.op() {
163 Some(NodeOp::Scan { table, .. }) => match self.registry.get(table) {
164 Some(stats) => (
165 stats.row_count.unwrap_or(DEFAULT_ROWS),
166 stats.avg_row_bytes.unwrap_or(64),
167 ),
168 None => (node.estimated_rows().unwrap_or(DEFAULT_ROWS), 64),
169 },
170 _ => (node.estimated_rows().unwrap_or(DEFAULT_ROWS), 64),
171 };
172
173 match node.op() {
174 Some(NodeOp::Scan { .. }) => {
175 cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(10));
176 memory_bytes = memory_bytes.saturating_add(rows.saturating_mul(row_bytes));
177 }
178 Some(NodeOp::Filter { .. }) => {
179 cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(5));
180 }
181 Some(NodeOp::Project { .. }) => {
182 cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(2));
183 }
184 Some(NodeOp::Aggregate { group_keys, .. }) => {
185 let ndv_proxy = group_keys.len() as u64;
186 cpu_nanos = cpu_nanos
187 .saturating_add(rows.saturating_mul(50))
188 .saturating_add(ndv_proxy.saturating_mul(20));
189 memory_bytes = memory_bytes.saturating_add(rows.saturating_mul(200));
190 }
191 Some(NodeOp::Join { .. }) | Some(NodeOp::SortMergeJoin { .. }) => {
192 let ndv_cost = rows.saturating_mul(100);
193 cpu_nanos = cpu_nanos.saturating_add(ndv_cost);
194 memory_bytes = memory_bytes.saturating_add(rows.saturating_mul(100));
195 }
196 Some(NodeOp::Exchange { .. }) => {
197 cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(20));
198 network_bytes = network_bytes.saturating_add(rows.saturating_mul(200));
199 }
200 _ => {
201 cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(15));
202 memory_bytes = memory_bytes.saturating_add(rows.saturating_mul(64));
203 }
204 }
205 }
206
207 super::Cost {
208 cpu_nanos,
209 memory_bytes,
210 network_bytes,
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::{ExecutionKind, PlanNode};
219
220 #[test]
221 fn empty_registry_estimate_is_zero() {
222 let model = CboCostModel::default();
223 let plan = LogicalPlan::new("p", ExecutionKind::Batch);
224 let cost = model.estimate(&plan);
225 assert_eq!(cost.cpu_nanos, 0);
226 assert_eq!(cost.memory_bytes, 0);
227 assert_eq!(cost.network_bytes, 0);
228 }
229
230 #[test]
231 fn registry_round_trip() {
232 let reg = TableStatsRegistry::new();
233 reg.put(
234 TableCboStats::new("orders")
235 .with_row_count(1_000_000)
236 .with_ndv(1_000_000)
237 .with_avg_row_bytes(128),
238 );
239 let got = reg.get("orders").expect("present");
240 assert_eq!(got.row_count, Some(1_000_000));
241 assert_eq!(got.ndv, Some(1_000_000));
242 assert_eq!(got.avg_row_bytes, Some(128));
243 assert_eq!(reg.get("unknown"), None);
244 assert_eq!(reg.len(), 1);
245 }
246
247 #[test]
248 fn registry_remove_returns_previous_value() {
249 let reg = TableStatsRegistry::new();
250 reg.put(TableCboStats::new("t1").with_row_count(100));
251 let prev = reg.remove("t1").expect("present");
252 assert_eq!(prev.row_count, Some(100));
253 assert!(reg.is_empty());
254 }
255
256 #[test]
257 fn cbo_cost_model_scan_charges_avg_row_bytes() {
258 let model = CboCostModel {
259 registry: {
260 let reg = TableStatsRegistry::new();
261 reg.put(
262 TableCboStats::new("lineitem")
263 .with_row_count(6_000_000)
264 .with_avg_row_bytes(256),
265 );
266 reg
267 },
268 };
269 let mut plan = LogicalPlan::new("q", ExecutionKind::Batch);
270 plan.add_node(
271 PlanNode::new("scan", "scan lineitem", ExecutionKind::Batch).with_op(NodeOp::Scan {
272 table: "lineitem".to_string(),
273 filters: vec![],
274 }),
275 );
276 let cost = model.estimate(&plan);
277 assert_eq!(cost.cpu_nanos, 60_000_000);
279 assert_eq!(cost.memory_bytes, 1_536_000_000);
280 }
281
282 #[test]
283 fn cbo_cost_model_aggregate_charges_group_key_proxy() {
284 let model = CboCostModel::default();
285 let mut plan = LogicalPlan::new("q", ExecutionKind::Batch);
286 plan.add_node(
287 PlanNode::new("agg", "aggregate", ExecutionKind::Batch).with_op(NodeOp::Aggregate {
288 group_keys: vec!["region".to_string(), "year".to_string()],
289 }),
290 );
291 let cost = model.estimate(&plan);
292 assert_eq!(cost.cpu_nanos, 500_040);
294 }
295}