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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
//! Histogram-based selectivity estimation for non-uniform distributions
//!
//! Histograms provide more accurate selectivity estimates than assuming
//! uniform distribution, especially for skewed data.
use vibesql_types::SqlValue;
/// Histogram for tracking value distribution
#[derive(Debug, Clone)]
pub struct Histogram {
/// Histogram buckets
pub buckets: Vec<HistogramBucket>,
/// Strategy used to create buckets
pub bucket_strategy: BucketStrategy,
/// Total number of rows represented
pub total_rows: usize,
}
/// A single bucket in the histogram
#[derive(Debug, Clone)]
pub struct HistogramBucket {
/// Lower bound of this bucket (inclusive)
pub lower_bound: SqlValue,
/// Upper bound of this bucket (inclusive)
pub upper_bound: SqlValue,
/// Fraction of rows in this bucket (0.0 to 1.0)
pub frequency: f64,
/// Number of distinct values within this bucket
pub distinct_count: usize,
}
/// Strategy for dividing data into histogram buckets
#[derive(Debug, Clone, PartialEq)]
pub enum BucketStrategy {
/// Equal-width buckets: each bucket covers same value range
/// Good for uniformly distributed data
EqualWidth,
/// Equal-depth buckets: each bucket contains same number of rows
/// Good for skewed data
EqualDepth,
/// Hybrid: mix of equal-width and equal-depth
/// Balances accuracy for both uniform and skewed data
Hybrid,
}
impl Histogram {
/// Create a histogram from sample data
///
/// # Arguments
/// * `values` - Sample values (should not include NULLs)
/// * `num_buckets` - Number of buckets to create (default: 100)
/// * `strategy` - Bucketing strategy to use
pub fn create(values: &[SqlValue], num_buckets: usize, strategy: BucketStrategy) -> Self {
if values.is_empty() {
return Self { buckets: Vec::new(), bucket_strategy: strategy, total_rows: 0 };
}
let buckets = match strategy {
BucketStrategy::EqualWidth => Self::create_equal_width_buckets(values, num_buckets),
BucketStrategy::EqualDepth => Self::create_equal_depth_buckets(values, num_buckets),
BucketStrategy::Hybrid => Self::create_hybrid_buckets(values, num_buckets),
};
Self { buckets, bucket_strategy: strategy, total_rows: values.len() }
}
/// Create equal-width buckets
fn create_equal_width_buckets(values: &[SqlValue], num_buckets: usize) -> Vec<HistogramBucket> {
if values.is_empty() || num_buckets == 0 {
return Vec::new();
}
let mut sorted_values = values.to_vec();
sorted_values.sort();
let min_val = &sorted_values[0];
let max_val = &sorted_values[sorted_values.len() - 1];
// Can't create equal-width buckets if all values are the same
if min_val == max_val {
return vec![HistogramBucket {
lower_bound: min_val.clone(),
upper_bound: max_val.clone(),
frequency: 1.0,
distinct_count: 1,
}];
}
// For numeric types, we can calculate bucket boundaries
// For now, we'll use a simplified approach with equal-depth as fallback
Self::create_equal_depth_buckets(values, num_buckets)
}
/// Create equal-depth buckets (each bucket has roughly equal number of rows)
fn create_equal_depth_buckets(values: &[SqlValue], num_buckets: usize) -> Vec<HistogramBucket> {
if values.is_empty() || num_buckets == 0 {
return Vec::new();
}
let mut sorted_values = values.to_vec();
sorted_values.sort();
// Special case: if all values are the same, create a single bucket
if sorted_values[0] == sorted_values[sorted_values.len() - 1] {
return vec![HistogramBucket {
lower_bound: sorted_values[0].clone(),
upper_bound: sorted_values[0].clone(),
frequency: 1.0,
distinct_count: 1,
}];
}
let target_bucket_size = (values.len() as f64 / num_buckets as f64).ceil() as usize;
let mut buckets = Vec::new();
let mut i = 0;
while i < sorted_values.len() {
let start_idx = i;
let end_idx = (i + target_bucket_size).min(sorted_values.len());
let lower_bound = sorted_values[start_idx].clone();
let upper_bound = sorted_values[end_idx - 1].clone();
// Count distinct values in this range
let mut distinct_values = std::collections::HashSet::new();
for value in &sorted_values[start_idx..end_idx] {
distinct_values.insert(value.clone());
}
let bucket_rows = end_idx - start_idx;
let frequency = bucket_rows as f64 / values.len() as f64;
buckets.push(HistogramBucket {
lower_bound,
upper_bound,
frequency,
distinct_count: distinct_values.len(),
});
i = end_idx;
}
buckets
}
/// Create hybrid buckets (combination of equal-width and equal-depth)
fn create_hybrid_buckets(values: &[SqlValue], num_buckets: usize) -> Vec<HistogramBucket> {
// For now, use equal-depth as it works better for skewed data
// In the future, we could detect skew and choose strategy accordingly
Self::create_equal_depth_buckets(values, num_buckets)
}
/// Estimate selectivity for equality predicate: col = value
pub fn estimate_equality_selectivity(&self, value: &SqlValue) -> f64 {
if self.buckets.is_empty() {
return 0.0;
}
// Find the bucket containing this value
for bucket in &self.buckets {
if value >= &bucket.lower_bound && value <= &bucket.upper_bound {
// Assume uniform distribution within the bucket
if bucket.distinct_count > 0 {
return bucket.frequency / bucket.distinct_count as f64;
}
}
}
// Value not in any bucket
0.0
}
/// Estimate selectivity for range predicate: col > value, col >= value, col < value, col <=
/// value
pub fn estimate_range_selectivity(&self, operator: &str, value: &SqlValue) -> f64 {
if self.buckets.is_empty() {
return 0.0;
}
let mut selectivity = 0.0;
match operator {
">" | ">=" => {
// Sum frequency of all buckets above the value
for bucket in &self.buckets {
if &bucket.lower_bound > value {
// Entire bucket is above the value
selectivity += bucket.frequency;
} else if &bucket.upper_bound >= value {
// Value is within this bucket, estimate partial selectivity
// Assume uniform distribution within bucket
let partial = if operator == ">" {
// Exclude the equality case
bucket.frequency * 0.5 // Rough estimate
} else {
// Include the equality case
bucket.frequency * 0.5 + self.estimate_equality_selectivity(value)
};
selectivity += partial;
}
}
}
"<" | "<=" => {
// Sum frequency of all buckets below the value
for bucket in &self.buckets {
if &bucket.upper_bound < value {
// Entire bucket is below the value
selectivity += bucket.frequency;
} else if &bucket.lower_bound <= value {
// Value is within this bucket, estimate partial selectivity
let partial = if operator == "<" {
// Exclude the equality case
bucket.frequency * 0.5
} else {
// Include the equality case
bucket.frequency * 0.5 + self.estimate_equality_selectivity(value)
};
selectivity += partial;
}
}
}
_ => return 0.33, // Default fallback
}
selectivity.clamp(0.0, 1.0)
}
/// Estimate selectivity for BETWEEN predicate: col BETWEEN lower AND upper
pub fn estimate_between_selectivity(&self, lower: &SqlValue, upper: &SqlValue) -> f64 {
if self.buckets.is_empty() {
return 0.0;
}
let mut selectivity = 0.0;
for bucket in &self.buckets {
// Check if bucket overlaps with [lower, upper] range
if &bucket.upper_bound < lower || &bucket.lower_bound > upper {
// No overlap
continue;
}
if &bucket.lower_bound >= lower && &bucket.upper_bound <= upper {
// Bucket is fully contained in range
selectivity += bucket.frequency;
} else {
// Partial overlap - estimate fraction
// This is a simplified estimate; more sophisticated logic
// would calculate exact overlap based on value distribution
selectivity += bucket.frequency * 0.5;
}
}
selectivity.clamp(0.0, 1.0)
}
/// Get the number of buckets
pub fn bucket_count(&self) -> usize {
self.buckets.len()
}
/// Check if histogram is empty
pub fn is_empty(&self) -> bool {
self.buckets.is_empty()
}
}
#[cfg(test)]
mod tests {
use vibesql_types::SqlValue;
use super::*;
#[test]
fn test_histogram_equal_depth() {
// Create skewed data: many 1s, some 2s, few 3s
let mut values = Vec::new();
for _ in 0..70 {
values.push(SqlValue::Integer(1));
}
for _ in 0..20 {
values.push(SqlValue::Integer(2));
}
for _ in 0..10 {
values.push(SqlValue::Integer(3));
}
let histogram = Histogram::create(&values, 3, BucketStrategy::EqualDepth);
assert_eq!(histogram.bucket_count(), 3);
assert_eq!(histogram.total_rows, 100);
// Each bucket should have roughly equal frequency (33.3%)
for bucket in &histogram.buckets {
assert!((bucket.frequency - 0.33).abs() < 0.1);
}
}
#[test]
fn test_histogram_equality_selectivity() {
let values = vec![
SqlValue::Integer(1),
SqlValue::Integer(1),
SqlValue::Integer(1),
SqlValue::Integer(2),
SqlValue::Integer(3),
];
let histogram = Histogram::create(&values, 2, BucketStrategy::EqualDepth);
// Estimate selectivity for value 1 (appears 3 times out of 5)
let sel = histogram.estimate_equality_selectivity(&SqlValue::Integer(1));
// Should be roughly 60% (3/5), but depends on bucketing
assert!(sel > 0.0);
assert!(sel <= 1.0);
}
#[test]
fn test_histogram_range_selectivity() {
let values: Vec<SqlValue> = (1..=100).map(SqlValue::Integer).collect();
let histogram = Histogram::create(&values, 10, BucketStrategy::EqualDepth);
// Estimate selectivity for values > 50
let sel = histogram.estimate_range_selectivity(">", &SqlValue::Integer(50));
// Should be roughly 50%
assert!((sel - 0.5).abs() < 0.2);
}
#[test]
fn test_histogram_between_selectivity() {
let values: Vec<SqlValue> = (1..=100).map(SqlValue::Integer).collect();
let histogram = Histogram::create(&values, 10, BucketStrategy::EqualDepth);
// Estimate selectivity for BETWEEN 25 AND 75
let sel =
histogram.estimate_between_selectivity(&SqlValue::Integer(25), &SqlValue::Integer(75));
// Should be roughly 50% (50 values out of 100)
assert!((sel - 0.5).abs() < 0.3);
}
#[test]
fn test_empty_histogram() {
let histogram = Histogram::create(&[], 10, BucketStrategy::EqualDepth);
assert!(histogram.is_empty());
assert_eq!(histogram.bucket_count(), 0);
assert_eq!(histogram.estimate_equality_selectivity(&SqlValue::Integer(1)), 0.0);
}
#[test]
fn test_single_value_histogram() {
let values = vec![SqlValue::Integer(42); 100];
let histogram = Histogram::create(&values, 10, BucketStrategy::EqualDepth);
assert_eq!(histogram.bucket_count(), 1);
assert_eq!(histogram.buckets[0].distinct_count, 1);
assert_eq!(histogram.buckets[0].frequency, 1.0);
}
}