uqa_planner/cardinality/
stats.rs1use super::{BTreeMap, Value, VertexConstraint};
10
11pub const JACCARD_JOIN_SELECTIVITY: f64 = 0.05;
14
15pub const GRAPH_AVG_DEGREE_DEFAULT: f64 = 10.0;
18
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21pub enum AccessParadigm {
22 #[default]
23 Relational,
24 Text,
25 Vector,
26 Graph,
27 Hybrid,
28 CrossParadigm,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct LocalAccessEstimate {
34 pub output_rows: f64,
35 pub cost: f64,
36 pub paradigm: AccessParadigm,
37}
38
39#[derive(Debug, Clone, Default)]
45pub struct GraphStats {
46 pub num_vertices: u64,
47 pub num_edges: u64,
48 pub label_counts: BTreeMap<String, u64>,
49 pub avg_out_degree: f64,
50 pub degree_distribution: BTreeMap<u64, u64>,
51 pub min_timestamp: Option<f64>,
52 pub max_timestamp: Option<f64>,
53 pub graph_name: String,
54 pub vertex_label_counts: BTreeMap<String, u64>,
55 pub label_degree_map: BTreeMap<String, f64>,
56}
57
58impl GraphStats {
59 pub fn label_selectivity(&self, label: Option<&str>) -> f64 {
62 match label {
63 None => 1.0,
64 Some(_) if self.num_edges == 0 => 1.0,
65 Some(name) => {
66 let c = self.label_counts.get(name).copied().unwrap_or(0);
67 c as f64 / self.num_edges as f64
68 }
69 }
70 }
71
72 pub fn edge_density(&self) -> f64 {
74 if self.num_vertices <= 1 {
75 return 0.0;
76 }
77 let nv = self.num_vertices as f64;
78 self.num_edges as f64 / (nv * nv)
79 }
80}
81
82pub struct EdgeSample {
88 pub target_id: u64,
89 pub label: String,
90}
91
92pub trait GraphStoreSampler: Send + Sync {
95 fn vertex_ids(&self) -> Vec<u64>;
97
98 fn outgoing_edges(&self, vid: u64) -> Vec<EdgeSample>;
100
101 fn vertex_satisfies(&self, vid: u64, constraint: &VertexConstraint) -> bool;
104}
105
106#[derive(Debug, Clone, Default)]
111pub struct ColumnStats {
112 pub distinct_count: u64,
113 pub null_count: u64,
114 pub min_value: Option<Value>,
115 pub max_value: Option<Value>,
116 pub row_count: u64,
117 pub histogram: Vec<Value>,
120 pub mcv_values: Vec<Value>,
122 pub mcv_frequencies: Vec<f64>,
123}
124
125impl ColumnStats {
126 pub fn equality_selectivity(&self) -> f64 {
128 self.non_null_fraction() / self.distinct_count.max(1) as f64
129 }
130
131 pub fn non_null_fraction(&self) -> f64 {
132 if self.row_count == 0 {
133 1.0
134 } else {
135 (1.0 - self.null_count as f64 / self.row_count as f64).clamp(0.0, 1.0)
136 }
137 }
138
139 pub fn equality_selectivity_for(&self, value: &Value) -> f64 {
141 if matches!(value, Value::Null) {
142 return 0.0;
143 }
144 if let Some(frequency) = self.matches_mcv(value) {
145 return frequency.clamp(0.0, 1.0);
146 }
147 let remaining =
148 (self.non_null_fraction() - self.mcv_frequencies.iter().sum::<f64>()).max(0.0);
149 let distinct = self
150 .distinct_count
151 .saturating_sub(self.mcv_values.len() as u64)
152 .max(1);
153 let selectivity = remaining / distinct as f64;
154 self.mcv_frequencies
155 .iter()
156 .copied()
157 .reduce(f64::min)
158 .map_or(selectivity, |least_common| selectivity.min(least_common))
159 .clamp(0.0, 1.0)
160 }
161
162 pub fn matches_mcv(&self, value: &Value) -> Option<f64> {
163 for (mcv, freq) in self.mcv_values.iter().zip(self.mcv_frequencies.iter()) {
164 if mcv == value {
165 return Some(*freq);
166 }
167 }
168 None
169 }
170}
171
172#[derive(Debug, Clone, Default)]
173pub struct RelationStats {
174 pub row_count: u64,
175 pub columns: BTreeMap<String, ColumnStats>,
176}
177
178impl RelationStats {
179 pub fn new(row_count: u64) -> Self {
180 Self {
181 row_count,
182 columns: BTreeMap::new(),
183 }
184 }
185
186 pub fn with_column(mut self, name: impl Into<String>, stats: ColumnStats) -> Self {
187 self.columns.insert(name.into(), stats);
188 self
189 }
190
191 pub fn column(&self, name: &str) -> Option<&ColumnStats> {
192 self.columns.get(name)
193 }
194}
195
196#[derive(Debug, Clone, Copy)]
197pub struct Selectivity(pub f64);
198
199impl Selectivity {
200 pub fn clamp(self) -> Self {
201 Self(self.0.clamp(0.0, 1.0))
202 }
203
204 pub fn raw(self) -> f64 {
205 self.0
206 }
207}