use std::{
collections::HashMap,
io::{self, Cursor},
sync::Arc,
};
use ahash::AHashMap;
use vibesql_storage::Row;
use vibesql_types::SqlValue;
use super::{
row_serialization::{deserialize_row, serialize_row},
MemoryController, MemoryReservation, SpillFile,
};
use crate::select::grouping::AggregateAccumulator;
#[derive(Debug, Clone)]
pub struct ExternalAggregateConfig {
pub num_partitions: usize,
pub max_groups_per_partition: usize,
}
impl Default for ExternalAggregateConfig {
fn default() -> Self {
Self {
num_partitions: 64, max_groups_per_partition: 10_000, }
}
}
#[derive(Debug, Clone)]
pub struct AggregateSpec {
pub function_name: String,
pub distinct: bool,
pub value_index: usize,
}
struct Partition {
groups: AHashMap<Vec<u8>, (Vec<SqlValue>, Vec<AggregateAccumulator>)>,
memory_bytes: usize,
spilled: bool,
spill_file: Option<SpillFile>,
rows_spilled: usize,
}
impl Partition {
fn new() -> Self {
Self {
groups: AHashMap::new(),
memory_bytes: 0,
spilled: false,
spill_file: None,
rows_spilled: 0,
}
}
fn estimate_group_memory(
key_values: &[SqlValue],
accumulators: &[AggregateAccumulator],
) -> usize {
let key_size: usize = key_values.iter().map(|v| v.estimated_size_bytes()).sum();
let acc_size = std::mem::size_of_val(accumulators)
+ accumulators.iter().map(estimate_accumulator_memory).sum::<usize>();
key_size + acc_size + 64 }
}
fn estimate_accumulator_memory(acc: &AggregateAccumulator) -> usize {
match acc {
AggregateAccumulator::Count { seen: Some(set), .. } => set.len() * 48,
AggregateAccumulator::Sum { seen: Some(set), .. } => set.len() * 48,
AggregateAccumulator::Avg { seen: Some(set), .. } => set.len() * 48,
AggregateAccumulator::Min { seen: Some(set), .. } => set.len() * 48,
AggregateAccumulator::Max { seen: Some(set), .. } => set.len() * 48,
_ => 0,
}
}
pub struct ExternalAggregate {
reservation: MemoryReservation,
#[allow(dead_code)]
config: ExternalAggregateConfig,
aggregate_specs: Vec<AggregateSpec>,
num_key_columns: usize,
partitions: Vec<Partition>,
partition_mask: usize,
total_groups: usize,
}
impl ExternalAggregate {
pub fn new(
controller: &Arc<MemoryController>,
num_key_columns: usize,
aggregate_specs: Vec<AggregateSpec>,
) -> Self {
Self::with_config(
controller,
num_key_columns,
aggregate_specs,
ExternalAggregateConfig::default(),
)
}
pub fn with_config(
controller: &Arc<MemoryController>,
num_key_columns: usize,
aggregate_specs: Vec<AggregateSpec>,
config: ExternalAggregateConfig,
) -> Self {
let num_partitions = config.num_partitions.next_power_of_two();
let mut partitions = Vec::with_capacity(num_partitions);
for _ in 0..num_partitions {
partitions.push(Partition::new());
}
Self {
reservation: controller.create_reservation(),
config: ExternalAggregateConfig { num_partitions, ..config },
aggregate_specs,
num_key_columns,
partitions,
partition_mask: num_partitions - 1,
total_groups: 0,
}
}
pub fn add_row(&mut self, row: &[SqlValue]) -> io::Result<()> {
let key_values = &row[..self.num_key_columns];
let partition_idx = self.compute_partition(key_values);
let partition = &mut self.partitions[partition_idx];
if partition.spilled {
self.spill_row_to_partition(partition_idx, row)?;
return Ok(());
}
let key_bytes = serialize_key(key_values);
if let Some((_, accumulators)) = partition.groups.get_mut(&key_bytes) {
for (spec, acc) in self.aggregate_specs.iter().zip(accumulators.iter_mut()) {
let value = &row[self.num_key_columns + spec.value_index];
acc.accumulate(value);
}
} else {
let accumulators = self.create_accumulators();
let group_memory = Partition::estimate_group_memory(key_values, &accumulators);
if !self.reservation.try_grow(group_memory) {
self.spill_largest_partition()?;
let partition = &mut self.partitions[partition_idx];
if partition.spilled {
self.spill_row_to_partition(partition_idx, row)?;
return Ok(());
}
if !self.reservation.try_grow(group_memory) {
return Err(io::Error::new(
io::ErrorKind::OutOfMemory,
"single group exceeds available memory budget",
));
}
}
let mut accumulators = self.create_accumulators();
for (spec, acc) in self.aggregate_specs.iter().zip(accumulators.iter_mut()) {
let value = &row[self.num_key_columns + spec.value_index];
acc.accumulate(value);
}
let partition = &mut self.partitions[partition_idx];
partition.groups.insert(key_bytes, (key_values.to_vec(), accumulators));
partition.memory_bytes += group_memory;
self.total_groups += 1;
}
Ok(())
}
fn create_accumulators(&self) -> Vec<AggregateAccumulator> {
self.aggregate_specs
.iter()
.map(|spec| {
AggregateAccumulator::new(&spec.function_name, spec.distinct)
.expect("aggregate spec should be valid")
})
.collect()
}
fn compute_partition(&self, key_values: &[SqlValue]) -> usize {
use std::hash::Hasher;
let mut hasher = ahash::AHasher::default();
for v in key_values {
hash_sql_value(v, &mut hasher);
}
(hasher.finish() as usize) & self.partition_mask
}
fn spill_row_to_partition(&mut self, partition_idx: usize, row: &[SqlValue]) -> io::Result<()> {
let partition = &mut self.partitions[partition_idx];
if partition.spill_file.is_none() {
let temp_dir = self.reservation.temp_directory();
partition.spill_file =
Some(SpillFile::with_suffix(temp_dir, &format!("agg_part_{}", partition_idx))?);
}
let spill_file = partition.spill_file.as_mut().unwrap();
let mut buf = Vec::new();
let row_to_serialize = Row::from_vec(row.to_vec());
serialize_row(&row_to_serialize, &mut buf)?;
let len = buf.len() as u32;
spill_file.write_all(&len.to_le_bytes())?;
spill_file.write_all(&buf)?;
partition.rows_spilled += 1;
self.reservation.record_spill(buf.len() + 4);
Ok(())
}
fn spill_largest_partition(&mut self) -> io::Result<()> {
let (largest_idx, largest_size) = self
.partitions
.iter()
.enumerate()
.filter(|(_, p)| !p.spilled && !p.groups.is_empty())
.max_by_key(|(_, p)| p.memory_bytes)
.map(|(i, p)| (i, p.memory_bytes))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::OutOfMemory, "no partition available to spill")
})?;
let temp_dir = self.reservation.temp_directory().clone();
let spill_file = SpillFile::with_suffix(&temp_dir, &format!("agg_part_{}", largest_idx))?;
let partition = &mut self.partitions[largest_idx];
let groups_lost = partition.groups.len();
partition.groups.clear();
self.reservation.shrink(largest_size);
self.total_groups = self.total_groups.saturating_sub(groups_lost);
partition.memory_bytes = 0;
partition.spilled = true;
partition.spill_file = Some(spill_file);
Ok(())
}
pub fn finish(mut self) -> io::Result<AggregateResultIterator> {
for partition in &mut self.partitions {
if let Some(ref mut file) = partition.spill_file {
file.flush()?;
}
}
let mut in_memory_results: Vec<(Vec<SqlValue>, Vec<SqlValue>)> = Vec::new();
for partition in &mut self.partitions {
if !partition.spilled {
for (_, (key_values, accumulators)) in partition.groups.drain() {
let agg_values: Vec<SqlValue> = accumulators
.iter()
.map(|a| {
a.finalize().map_err(|e| {
io::Error::new(io::ErrorKind::InvalidData, e.to_string())
})
})
.collect::<io::Result<Vec<_>>>()?;
in_memory_results.push((key_values, agg_values));
}
}
}
let spilled_partitions: Vec<_> = self
.partitions
.into_iter()
.enumerate()
.filter(|(_, p)| p.spilled && p.rows_spilled > 0)
.collect();
Ok(AggregateResultIterator {
in_memory_results: in_memory_results.into_iter(),
spilled_partitions,
current_spill_idx: 0,
aggregate_specs: self.aggregate_specs,
num_key_columns: self.num_key_columns,
partition_mask: self.partition_mask,
#[allow(dead_code)]
reservation: self.reservation,
})
}
pub fn num_groups(&self) -> usize {
self.total_groups
}
pub fn num_spilled_partitions(&self) -> usize {
self.partitions.iter().filter(|p| p.spilled).count()
}
}
pub struct AggregateResultIterator {
in_memory_results: std::vec::IntoIter<(Vec<SqlValue>, Vec<SqlValue>)>,
spilled_partitions: Vec<(usize, Partition)>,
current_spill_idx: usize,
aggregate_specs: Vec<AggregateSpec>,
num_key_columns: usize,
#[allow(dead_code)]
partition_mask: usize,
#[allow(dead_code)]
reservation: MemoryReservation,
}
impl Iterator for AggregateResultIterator {
type Item = io::Result<Vec<SqlValue>>;
fn next(&mut self) -> Option<Self::Item> {
if let Some((key_values, agg_values)) = self.in_memory_results.next() {
let mut result = key_values;
result.extend(agg_values);
return Some(Ok(result));
}
while self.current_spill_idx < self.spilled_partitions.len() {
let result = self.process_next_spilled_partition();
if result.is_some() {
return result;
}
self.current_spill_idx += 1;
}
None
}
}
impl AggregateResultIterator {
fn process_next_spilled_partition(&mut self) -> Option<io::Result<Vec<SqlValue>>> {
if self.current_spill_idx >= self.spilled_partitions.len() {
return None;
}
let (_, partition) = &mut self.spilled_partitions[self.current_spill_idx];
let rows_spilled = partition.rows_spilled;
let spill_file_opt = partition.spill_file.take();
partition.rows_spilled = 0;
if let Some(mut spill_file) = spill_file_opt {
match self.reload_and_aggregate_partition(&mut spill_file, rows_spilled) {
Ok(mut results) => {
if let Some((key_values, agg_values)) = results.pop() {
if !results.is_empty() {
self.in_memory_results = results.into_iter();
}
let mut result = key_values;
result.extend(agg_values);
return Some(Ok(result));
}
}
Err(e) => return Some(Err(e)),
}
}
None
}
fn reload_and_aggregate_partition(
&self,
spill_file: &mut SpillFile,
num_rows: usize,
) -> io::Result<Vec<(Vec<SqlValue>, Vec<SqlValue>)>> {
spill_file.prepare_for_read()?;
let mut groups: HashMap<Vec<u8>, (Vec<SqlValue>, Vec<AggregateAccumulator>)> =
HashMap::new();
for _ in 0..num_rows {
let mut len_buf = [0u8; 4];
spill_file.read_exact(&mut len_buf)?;
let len = u32::from_le_bytes(len_buf) as usize;
let mut row_buf = vec![0u8; len];
spill_file.read_exact(&mut row_buf)?;
let row = deserialize_row(&mut Cursor::new(row_buf))?;
let row_values: Vec<SqlValue> = row.values.into_iter().collect();
let key_values = &row_values[..self.num_key_columns];
let key_bytes = serialize_key(key_values);
if let Some((_, accumulators)) = groups.get_mut(&key_bytes) {
for (spec, acc) in self.aggregate_specs.iter().zip(accumulators.iter_mut()) {
let value = &row_values[self.num_key_columns + spec.value_index];
acc.accumulate(value);
}
} else {
let mut accumulators: Vec<AggregateAccumulator> = self
.aggregate_specs
.iter()
.map(|spec| {
AggregateAccumulator::new(&spec.function_name, spec.distinct)
.expect("valid spec")
})
.collect();
for (spec, acc) in self.aggregate_specs.iter().zip(accumulators.iter_mut()) {
let value = &row_values[self.num_key_columns + spec.value_index];
acc.accumulate(value);
}
groups.insert(key_bytes, (key_values.to_vec(), accumulators));
}
}
let mut results = Vec::with_capacity(groups.len());
for (key_values, accumulators) in groups.into_values() {
let agg_values: Vec<SqlValue> = accumulators
.iter()
.map(|a| {
a.finalize()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
})
.collect::<io::Result<Vec<_>>>()?;
results.push((key_values, agg_values));
}
Ok(results)
}
}
fn serialize_key(values: &[SqlValue]) -> Vec<u8> {
use super::row_serialization::serialize_value;
let mut buf = Vec::new();
for v in values {
serialize_value(v, &mut buf).expect("key serialization should not fail");
}
buf
}
fn hash_sql_value<H: std::hash::Hasher>(value: &SqlValue, hasher: &mut H) {
use std::hash::Hash;
std::mem::discriminant(value).hash(hasher);
match value {
SqlValue::Null => {}
SqlValue::Boolean(b) => b.hash(hasher),
SqlValue::Smallint(i) => i.hash(hasher),
SqlValue::Integer(i) => i.hash(hasher),
SqlValue::Bigint(i) => i.hash(hasher),
SqlValue::Unsigned(u) => u.hash(hasher),
SqlValue::Float(f) => f.to_bits().hash(hasher),
SqlValue::Real(f) => f.to_bits().hash(hasher),
SqlValue::Double(f) => f.to_bits().hash(hasher),
SqlValue::Numeric(f) => f.to_bits().hash(hasher),
SqlValue::Character(s) | SqlValue::Varchar(s) => s.hash(hasher),
SqlValue::Date(d) => (d.year, d.month, d.day).hash(hasher),
SqlValue::Time(t) => (t.hour, t.minute, t.second, t.nanosecond).hash(hasher),
SqlValue::Timestamp(ts) => {
(ts.date.year, ts.date.month, ts.date.day).hash(hasher);
(ts.time.hour, ts.time.minute, ts.time.second, ts.time.nanosecond).hash(hasher);
}
SqlValue::Interval(iv) => {
iv.value.hash(hasher);
}
SqlValue::Vector(v) => {
for f in v {
f.to_bits().hash(hasher);
}
}
SqlValue::Blob(b) => b.hash(hasher),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_controller() -> Arc<MemoryController> {
Arc::new(MemoryController::with_budget(1024 * 1024)) }
#[test]
fn test_simple_count() {
let controller = make_test_controller();
let specs = vec![AggregateSpec {
function_name: "COUNT".to_string(),
distinct: false,
value_index: 0,
}];
let mut agg = ExternalAggregate::new(&controller, 1, specs);
agg.add_row(&[SqlValue::Varchar("a".into()), SqlValue::Integer(1)]).unwrap();
agg.add_row(&[SqlValue::Varchar("a".into()), SqlValue::Integer(2)]).unwrap();
agg.add_row(&[SqlValue::Varchar("a".into()), SqlValue::Integer(3)]).unwrap();
agg.add_row(&[SqlValue::Varchar("b".into()), SqlValue::Integer(10)]).unwrap();
agg.add_row(&[SqlValue::Varchar("b".into()), SqlValue::Integer(20)]).unwrap();
let results: Vec<_> = agg.finish().unwrap().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 2);
let group_a = results.iter().find(|r| r[0] == SqlValue::Varchar("a".into())).unwrap();
let group_b = results.iter().find(|r| r[0] == SqlValue::Varchar("b".into())).unwrap();
assert_eq!(group_a[1], SqlValue::Integer(3)); assert_eq!(group_b[1], SqlValue::Integer(2)); }
#[test]
fn test_sum_and_avg() {
let controller = make_test_controller();
let specs = vec![
AggregateSpec { function_name: "SUM".to_string(), distinct: false, value_index: 0 },
AggregateSpec { function_name: "AVG".to_string(), distinct: false, value_index: 0 },
];
let mut agg = ExternalAggregate::new(&controller, 1, specs);
agg.add_row(&[SqlValue::Integer(1), SqlValue::Integer(10)]).unwrap();
agg.add_row(&[SqlValue::Integer(1), SqlValue::Integer(20)]).unwrap();
agg.add_row(&[SqlValue::Integer(1), SqlValue::Integer(30)]).unwrap();
let results: Vec<_> = agg.finish().unwrap().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 1);
assert_eq!(results[0][0], SqlValue::Integer(1)); assert_eq!(results[0][1], SqlValue::Integer(60)); assert_eq!(results[0][2], SqlValue::Double(20.0)); }
#[test]
fn test_min_max() {
let controller = make_test_controller();
let specs = vec![
AggregateSpec { function_name: "MIN".to_string(), distinct: false, value_index: 0 },
AggregateSpec { function_name: "MAX".to_string(), distinct: false, value_index: 0 },
];
let mut agg = ExternalAggregate::new(&controller, 1, specs);
agg.add_row(&[SqlValue::Varchar("x".into()), SqlValue::Integer(5)]).unwrap();
agg.add_row(&[SqlValue::Varchar("x".into()), SqlValue::Integer(15)]).unwrap();
agg.add_row(&[SqlValue::Varchar("x".into()), SqlValue::Integer(10)]).unwrap();
let results: Vec<_> = agg.finish().unwrap().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 1);
assert_eq!(results[0][1], SqlValue::Integer(5)); assert_eq!(results[0][2], SqlValue::Integer(15)); }
#[test]
fn test_multi_key_grouping() {
let controller = make_test_controller();
let specs = vec![AggregateSpec {
function_name: "COUNT".to_string(),
distinct: false,
value_index: 0,
}];
let mut agg = ExternalAggregate::new(&controller, 2, specs);
agg.add_row(&[SqlValue::Integer(1), SqlValue::Varchar("a".into()), SqlValue::Integer(100)])
.unwrap();
agg.add_row(&[SqlValue::Integer(1), SqlValue::Varchar("a".into()), SqlValue::Integer(200)])
.unwrap();
agg.add_row(&[SqlValue::Integer(1), SqlValue::Varchar("b".into()), SqlValue::Integer(300)])
.unwrap();
agg.add_row(&[SqlValue::Integer(2), SqlValue::Varchar("a".into()), SqlValue::Integer(400)])
.unwrap();
agg.add_row(&[SqlValue::Integer(2), SqlValue::Varchar("a".into()), SqlValue::Integer(500)])
.unwrap();
agg.add_row(&[SqlValue::Integer(2), SqlValue::Varchar("a".into()), SqlValue::Integer(600)])
.unwrap();
let results: Vec<_> = agg.finish().unwrap().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 3);
let g1a = results
.iter()
.find(|r| r[0] == SqlValue::Integer(1) && r[1] == SqlValue::Varchar("a".into()))
.unwrap();
let g1b = results
.iter()
.find(|r| r[0] == SqlValue::Integer(1) && r[1] == SqlValue::Varchar("b".into()))
.unwrap();
let g2a = results
.iter()
.find(|r| r[0] == SqlValue::Integer(2) && r[1] == SqlValue::Varchar("a".into()))
.unwrap();
assert_eq!(g1a[2], SqlValue::Integer(2)); assert_eq!(g1b[2], SqlValue::Integer(1)); assert_eq!(g2a[2], SqlValue::Integer(3)); }
#[test]
fn test_null_handling() {
let controller = make_test_controller();
let specs = vec![
AggregateSpec { function_name: "COUNT".to_string(), distinct: false, value_index: 0 },
AggregateSpec { function_name: "SUM".to_string(), distinct: false, value_index: 0 },
];
let mut agg = ExternalAggregate::new(&controller, 1, specs);
agg.add_row(&[SqlValue::Integer(1), SqlValue::Integer(10)]).unwrap();
agg.add_row(&[SqlValue::Integer(1), SqlValue::Null]).unwrap();
agg.add_row(&[SqlValue::Integer(1), SqlValue::Integer(20)]).unwrap();
agg.add_row(&[SqlValue::Integer(1), SqlValue::Null]).unwrap();
let results: Vec<_> = agg.finish().unwrap().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 1);
assert_eq!(results[0][1], SqlValue::Integer(2)); assert_eq!(results[0][2], SqlValue::Integer(30));
}
#[test]
fn test_empty_input() {
let controller = make_test_controller();
let specs = vec![AggregateSpec {
function_name: "COUNT".to_string(),
distinct: false,
value_index: 0,
}];
let agg = ExternalAggregate::new(&controller, 1, specs);
let results: Vec<_> = agg.finish().unwrap().map(|r| r.unwrap()).collect();
assert!(results.is_empty());
}
#[test]
fn test_spill_under_memory_pressure() {
let controller = Arc::new(MemoryController::with_budget(4096)); let config = ExternalAggregateConfig {
num_partitions: 4, max_groups_per_partition: 10,
};
let specs = vec![AggregateSpec {
function_name: "SUM".to_string(),
distinct: false,
value_index: 0,
}];
let mut agg = ExternalAggregate::with_config(&controller, 1, specs, config);
for i in 0..100 {
agg.add_row(&[SqlValue::Integer(i), SqlValue::Integer(i * 10)]).unwrap();
}
let _ = agg.num_spilled_partitions();
let results: Vec<_> = agg.finish().unwrap().map(|r| r.unwrap()).collect();
assert!(results.len() <= 100);
}
}