linkrs_query/optimizer/stats/
histogram.rs1use linkrs_core::value::Value;
7use std::time::Instant;
8
9#[derive(Debug, Clone)]
11pub struct ValueHistogramBucket {
12 pub upper_bound: Value,
14 pub count: u64,
16 pub distinct_values: u64,
18}
19
20impl ValueHistogramBucket {
21 pub fn new(upper_bound: Value, count: u64, distinct_values: u64) -> Self {
23 Self {
24 upper_bound,
25 count,
26 distinct_values: distinct_values.max(1),
27 }
28 }
29}
30
31#[derive(Debug, Clone)]
33pub enum RangeCondition {
34 Lt(Value),
36 Le(Value),
38 Gt(Value),
40 Ge(Value),
42 Range { low: Value, high: Value },
44}
45
46#[derive(Debug, Clone)]
48pub struct ValueHistogram {
49 buckets: Vec<ValueHistogramBucket>,
51 null_fraction: f64,
53 total_distinct_values: u64,
55 total_count: u64,
57 last_updated: Instant,
59}
60
61impl ValueHistogram {
62 pub fn empty() -> Self {
64 Self {
65 buckets: Vec::new(),
66 null_fraction: 0.0,
67 total_distinct_values: 0,
68 total_count: 0,
69 last_updated: Instant::now(),
70 }
71 }
72
73 pub fn from_samples(samples: Vec<Value>, num_buckets: usize, total_count: u64) -> Self {
83 if samples.is_empty() || num_buckets == 0 {
84 return Self::empty();
85 }
86
87 let mut sorted_samples = samples;
88 let null_count = sorted_samples.iter().filter(|v| v.is_null()).count();
90 sorted_samples.retain(|v| !v.is_null());
91
92 if sorted_samples.is_empty() {
93 return Self {
94 buckets: Vec::new(),
95 null_fraction: null_count as f64 / total_count.max(1) as f64,
96 total_distinct_values: 0,
97 total_count,
98 last_updated: Instant::now(),
99 };
100 }
101
102 sorted_samples.sort_by(compare_values);
104
105 let distinct_values = calculate_distinct_values(&sorted_samples);
107
108 let non_null_count = sorted_samples.len();
110 let bucket_size = non_null_count / num_buckets;
111 let remainder = non_null_count % num_buckets;
112
113 let mut buckets = Vec::with_capacity(num_buckets);
114 let mut start_idx = 0;
115
116 for i in 0..num_buckets {
117 let current_bucket_size = bucket_size + if i < remainder { 1 } else { 0 };
118 if current_bucket_size == 0 {
119 continue;
120 }
121
122 let end_idx = (start_idx + current_bucket_size).min(sorted_samples.len());
123 let bucket_samples = &sorted_samples[start_idx..end_idx];
124
125 if let Some(upper_bound) = bucket_samples.last().cloned() {
126 let count = bucket_samples.len() as u64;
127 let bucket_distinct = calculate_distinct_values(bucket_samples);
128 buckets.push(ValueHistogramBucket::new(
129 upper_bound,
130 count,
131 bucket_distinct,
132 ));
133 }
134
135 start_idx = end_idx;
136 }
137
138 Self {
139 buckets,
140 null_fraction: null_count as f64 / total_count.max(1) as f64,
141 total_distinct_values: distinct_values,
142 total_count,
143 last_updated: Instant::now(),
144 }
145 }
146
147 pub fn estimate_equality_selectivity(&self, value: &Value) -> f64 {
152 if value.is_null() {
153 return self.null_fraction;
154 }
155
156 if self.buckets.is_empty() {
157 return 0.1; }
159
160 match self.find_bucket(value) {
162 Some(bucket_idx) => {
163 let bucket = &self.buckets[bucket_idx];
164 let bucket_selectivity = 1.0 / bucket.distinct_values.max(1) as f64;
166 let bucket_ratio = bucket.count as f64 / self.total_count.max(1) as f64;
168 bucket_selectivity * bucket_ratio
169 }
170 None => {
171 1.0 / self.total_distinct_values.max(1) as f64
173 }
174 }
175 }
176
177 pub fn estimate_range_selectivity(&self, range: &RangeCondition) -> f64 {
179 match range {
180 RangeCondition::Lt(value) | RangeCondition::Le(value) => {
181 self.estimate_less_than_selectivity(value)
182 }
183 RangeCondition::Gt(value) | RangeCondition::Ge(value) => {
184 1.0 - self.estimate_less_than_selectivity(value)
185 }
186 RangeCondition::Range { low, high } => {
187 let high_selectivity = self.estimate_less_than_selectivity(high);
188 let low_selectivity = self.estimate_less_than_selectivity(low);
189 (high_selectivity - low_selectivity).max(0.0)
190 }
191 }
192 }
193
194 fn estimate_less_than_selectivity(&self, value: &Value) -> f64 {
196 if self.buckets.is_empty() {
197 return 0.333; }
199
200 let mut selectivity = 0.0;
201 let mut found = false;
202
203 for bucket in &self.buckets {
204 if compare_values(value, &bucket.upper_bound) != std::cmp::Ordering::Greater {
205 let bucket_ratio = bucket.count as f64 / self.total_count.max(1) as f64;
208 selectivity += bucket_ratio * 0.5; found = true;
210 break;
211 } else {
212 selectivity += bucket.count as f64 / self.total_count.max(1) as f64;
214 }
215 }
216
217 if !found {
218 selectivity = 1.0 - self.null_fraction;
220 }
221
222 selectivity.min(1.0 - self.null_fraction)
223 }
224
225 fn find_bucket(&self, value: &Value) -> Option<usize> {
227 for (idx, bucket) in self.buckets.iter().enumerate() {
228 if compare_values(value, &bucket.upper_bound) != std::cmp::Ordering::Greater {
229 return Some(idx);
230 }
231 }
232 None
233 }
234
235 pub fn bucket_count(&self) -> usize {
237 self.buckets.len()
238 }
239
240 pub fn null_fraction(&self) -> f64 {
242 self.null_fraction
243 }
244
245 pub fn total_count(&self) -> u64 {
247 self.total_count
248 }
249
250 pub fn distinct_values(&self) -> u64 {
252 self.total_distinct_values
253 }
254
255 pub fn last_updated(&self) -> Instant {
257 self.last_updated
258 }
259}
260
261fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering {
266 use std::cmp::Ordering;
267
268 fn as_i64(value: &Value) -> Option<i64> {
269 match value {
270 Value::SmallInt(v) => Some(*v as i64),
271 Value::Int(v) => Some(*v as i64),
272 Value::BigInt(v) => Some(*v),
273 _ => None,
274 }
275 }
276 fn as_f64(value: &Value) -> Option<f64> {
277 match value {
278 Value::SmallInt(v) => Some(*v as f64),
279 Value::Int(v) => Some(*v as f64),
280 Value::BigInt(v) => Some(*v as f64),
281 Value::Float(v) => Some(*v as f64),
282 Value::Double(v) => Some(*v),
283 _ => None,
284 }
285 }
286 if let (Some(x), Some(y)) = (as_i64(a), as_i64(b)) {
287 return x.cmp(&y);
288 }
289 if let (Some(x), Some(y)) = (as_f64(a), as_f64(b)) {
290 return x.partial_cmp(&y).unwrap_or(Ordering::Equal);
291 }
292 match (a, b) {
293 (Value::SmallInt(a), Value::SmallInt(b)) => a.cmp(b),
294 (Value::Int(a), Value::Int(b)) => a.cmp(b),
295 (Value::BigInt(a), Value::BigInt(b)) => a.cmp(b),
296 (Value::Float(a), Value::Float(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
297 (Value::Double(a), Value::Double(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
298 (Value::String(a), Value::String(b)) => a.cmp(b),
299 (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
300 _ => Value::cmp(a, b),
301 }
302}
303
304fn calculate_distinct_values(values: &[Value]) -> u64 {
306 use std::collections::HashSet;
307
308 let mut seen = HashSet::new();
309 for value in values {
310 let key = format!("{:?}", value);
312 seen.insert(key);
313 }
314 seen.len() as u64
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_histogram_empty() {
323 let hist = ValueHistogram::empty();
324 assert_eq!(hist.bucket_count(), 0);
325 assert_eq!(hist.null_fraction(), 0.0);
326 }
327
328 #[test]
329 fn test_histogram_from_samples() {
330 let samples: Vec<Value> = (1..=100).map(Value::Int).collect();
331 let hist = ValueHistogram::from_samples(samples, 10, 100);
332
333 assert_eq!(hist.bucket_count(), 10);
334 assert_eq!(hist.total_count(), 100);
335 }
336
337 #[test]
338 fn test_estimate_equality_selectivity() {
339 let samples: Vec<Value> = (1..=100).map(Value::Int).collect();
340 let hist = ValueHistogram::from_samples(samples, 10, 100);
341
342 let selectivity = hist.estimate_equality_selectivity(&Value::Int(50));
344 assert!(selectivity > 0.0 && selectivity < 0.1);
345 }
346
347 #[test]
348 fn test_estimate_range_selectivity() {
349 let samples: Vec<Value> = (1..=100).map(Value::Int).collect();
350 let hist = ValueHistogram::from_samples(samples, 10, 100);
351
352 let range = RangeCondition::Lt(Value::Int(50));
354 let selectivity = hist.estimate_range_selectivity(&range);
355 assert!(selectivity > 0.3 && selectivity < 0.7);
356 }
357
358 #[test]
359 fn test_null_handling() {
360 let mut samples: Vec<Value> = (1..=90).map(Value::Int).collect();
361 for _ in 0..10 {
363 samples.push(Value::Null(linkrs_core::value::NullType::Null));
364 }
365
366 let hist = ValueHistogram::from_samples(samples, 10, 100);
367 assert!((hist.null_fraction() - 0.1).abs() < 0.01);
368
369 let null_selectivity =
370 hist.estimate_equality_selectivity(&Value::Null(linkrs_core::value::NullType::Null));
371 assert!((null_selectivity - 0.1).abs() < 0.01);
372 }
373}