use std::{
io::{self, Read},
sync::Arc,
};
use ahash::AHashMap;
use vibesql_types::SqlValue;
use super::{
controller::{MemoryController, MemoryReservation},
row_serialization::{deserialize_value, serialize_value},
spill::SpillFile,
};
#[derive(Debug, Clone)]
pub struct ExternalHashJoinConfig {
pub num_partitions: usize,
pub max_rows_per_partition: usize,
}
impl Default for ExternalHashJoinConfig {
fn default() -> Self {
Self { num_partitions: 64, max_rows_per_partition: 10_000 }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JoinType {
Inner,
LeftOuter,
RightOuter,
}
struct BuildPartition {
rows: Vec<(Vec<SqlValue>, Vec<SqlValue>)>,
memory_bytes: usize,
spilled: bool,
spill_file: Option<SpillFile>,
spilled_row_count: usize,
}
impl BuildPartition {
fn new() -> Self {
Self {
rows: Vec::new(),
memory_bytes: 0,
spilled: false,
spill_file: None,
spilled_row_count: 0,
}
}
}
struct ProbePartition {
rows: Vec<(Vec<SqlValue>, Vec<SqlValue>)>,
memory_bytes: usize,
spilled: bool,
spill_file: Option<SpillFile>,
spilled_row_count: usize,
}
impl ProbePartition {
fn new() -> Self {
Self {
rows: Vec::new(),
memory_bytes: 0,
spilled: false,
spill_file: None,
spilled_row_count: 0,
}
}
}
pub struct ExternalHashJoin {
reservation: MemoryReservation,
#[allow(dead_code)]
config: ExternalHashJoinConfig,
build_partitions: Vec<BuildPartition>,
probe_partitions: Vec<ProbePartition>,
build_key_indices: Vec<usize>,
probe_key_indices: Vec<usize>,
partition_mask: usize,
join_type: JoinType,
build_row_count: usize,
probe_row_count: usize,
build_row_width: Option<usize>,
probe_row_width: Option<usize>,
}
impl ExternalHashJoin {
pub fn new(
controller: Arc<MemoryController>,
config: ExternalHashJoinConfig,
build_key_indices: Vec<usize>,
probe_key_indices: Vec<usize>,
join_type: JoinType,
) -> Self {
assert!(config.num_partitions.is_power_of_two(), "num_partitions must be power of 2");
assert_eq!(
build_key_indices.len(),
probe_key_indices.len(),
"build and probe must have same number of key columns"
);
let partition_mask = config.num_partitions - 1;
let mut build_partitions = Vec::with_capacity(config.num_partitions);
let mut probe_partitions = Vec::with_capacity(config.num_partitions);
for _ in 0..config.num_partitions {
build_partitions.push(BuildPartition::new());
probe_partitions.push(ProbePartition::new());
}
Self {
reservation: controller.create_reservation(),
config,
build_partitions,
probe_partitions,
build_key_indices,
probe_key_indices,
partition_mask,
join_type,
build_row_count: 0,
probe_row_count: 0,
build_row_width: None,
probe_row_width: None,
}
}
pub fn add_build_row(&mut self, row: &[SqlValue]) -> io::Result<()> {
if self.build_row_width.is_none() {
self.build_row_width = Some(row.len());
}
let key_values: Vec<SqlValue> = self
.build_key_indices
.iter()
.map(|&idx| row.get(idx).cloned().unwrap_or(SqlValue::Null))
.collect();
if key_values.iter().any(|v| v == &SqlValue::Null) {
return Ok(());
}
let partition_idx = self.compute_partition(&key_values);
let row_size = estimate_row_size(&key_values) + estimate_row_size(row);
if self.reservation.should_spill() {
self.spill_largest_build_partition()?;
}
let partition = &mut self.build_partitions[partition_idx];
if partition.spilled {
self.write_to_build_spill(partition_idx, &key_values, row)?;
} else {
if !self.reservation.try_grow(row_size) {
self.spill_build_partition(partition_idx)?;
self.write_to_build_spill(partition_idx, &key_values, row)?;
} else {
partition.rows.push((key_values, row.to_vec()));
partition.memory_bytes += row_size;
}
}
self.build_row_count += 1;
Ok(())
}
pub fn add_probe_row(&mut self, row: &[SqlValue]) -> io::Result<()> {
if self.probe_row_width.is_none() {
self.probe_row_width = Some(row.len());
}
let key_values: Vec<SqlValue> = self
.probe_key_indices
.iter()
.map(|&idx| row.get(idx).cloned().unwrap_or(SqlValue::Null))
.collect();
if self.join_type == JoinType::Inner && key_values.iter().any(|v| v == &SqlValue::Null) {
return Ok(());
}
let partition_idx = self.compute_partition(&key_values);
let row_size = estimate_row_size(&key_values) + estimate_row_size(row);
if self.reservation.should_spill() {
self.spill_largest_probe_partition()?;
}
let partition = &mut self.probe_partitions[partition_idx];
if partition.spilled {
self.write_to_probe_spill(partition_idx, &key_values, row)?;
} else {
if !self.reservation.try_grow(row_size) {
self.spill_probe_partition(partition_idx)?;
self.write_to_probe_spill(partition_idx, &key_values, row)?;
} else {
partition.rows.push((key_values, row.to_vec()));
partition.memory_bytes += row_size;
}
}
self.probe_row_count += 1;
Ok(())
}
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_largest_build_partition(&mut self) -> io::Result<()> {
let largest_idx = self
.build_partitions
.iter()
.enumerate()
.filter(|(_, p)| !p.spilled && !p.rows.is_empty())
.max_by_key(|(_, p)| p.memory_bytes)
.map(|(i, _)| i);
if let Some(idx) = largest_idx {
self.spill_build_partition(idx)?;
}
Ok(())
}
fn spill_build_partition(&mut self, idx: usize) -> io::Result<()> {
let partition = &mut self.build_partitions[idx];
if partition.spilled {
return Ok(());
}
let temp_dir = self.reservation.temp_directory().clone();
let mut spill_file = SpillFile::with_suffix(&temp_dir, &format!("build_part_{}", idx))?;
let rows = std::mem::take(&mut partition.rows);
for (key, row) in rows {
write_keyed_row(&mut spill_file, &key, &row)?;
}
spill_file.flush()?;
self.reservation.shrink(partition.memory_bytes);
partition.memory_bytes = 0;
partition.spilled = true;
partition.spill_file = Some(spill_file);
Ok(())
}
fn write_to_build_spill(
&mut self,
idx: usize,
key_values: &[SqlValue],
row: &[SqlValue],
) -> io::Result<()> {
let partition = &mut self.build_partitions[idx];
let spill_file = partition.spill_file.as_mut().expect("spill file should exist");
write_keyed_row(spill_file, key_values, row)?;
spill_file.flush()?;
partition.spilled_row_count += 1;
Ok(())
}
fn spill_largest_probe_partition(&mut self) -> io::Result<()> {
let largest_idx = self
.probe_partitions
.iter()
.enumerate()
.filter(|(_, p)| !p.spilled && !p.rows.is_empty())
.max_by_key(|(_, p)| p.memory_bytes)
.map(|(i, _)| i);
if let Some(idx) = largest_idx {
self.spill_probe_partition(idx)?;
}
Ok(())
}
fn spill_probe_partition(&mut self, idx: usize) -> io::Result<()> {
let partition = &mut self.probe_partitions[idx];
if partition.spilled {
return Ok(());
}
let temp_dir = self.reservation.temp_directory().clone();
let mut spill_file = SpillFile::with_suffix(&temp_dir, &format!("probe_part_{}", idx))?;
let rows = std::mem::take(&mut partition.rows);
for (key, row) in rows {
write_keyed_row(&mut spill_file, &key, &row)?;
}
spill_file.flush()?;
self.reservation.shrink(partition.memory_bytes);
partition.memory_bytes = 0;
partition.spilled = true;
partition.spill_file = Some(spill_file);
Ok(())
}
fn write_to_probe_spill(
&mut self,
idx: usize,
key_values: &[SqlValue],
row: &[SqlValue],
) -> io::Result<()> {
let partition = &mut self.probe_partitions[idx];
let spill_file = partition.spill_file.as_mut().expect("spill file should exist");
write_keyed_row(spill_file, key_values, row)?;
spill_file.flush()?;
partition.spilled_row_count += 1;
Ok(())
}
pub fn num_spilled_build_partitions(&self) -> usize {
self.build_partitions.iter().filter(|p| p.spilled).count()
}
pub fn num_spilled_probe_partitions(&self) -> usize {
self.probe_partitions.iter().filter(|p| p.spilled).count()
}
pub fn finish(mut self) -> io::Result<HashJoinResultIterator> {
let mut results = Vec::new();
for partition_idx in 0..self.build_partitions.len() {
let partition_results = self.process_partition(partition_idx)?;
results.extend(partition_results);
}
Ok(HashJoinResultIterator { results: results.into_iter(), _reservation: self.reservation })
}
fn process_partition(&mut self, partition_idx: usize) -> io::Result<Vec<Vec<SqlValue>>> {
let build_rows = self.load_build_partition(partition_idx)?;
let mut hash_table: AHashMap<Vec<SqlValue>, Vec<Vec<SqlValue>>> = AHashMap::new();
for (key, row) in build_rows {
hash_table.entry(key).or_default().push(row);
}
let probe_rows = self.load_probe_partition(partition_idx)?;
let mut results = Vec::new();
match self.join_type {
JoinType::Inner => {
for (key, probe_row) in probe_rows {
if let Some(build_rows) = hash_table.get(&key) {
for build_row in build_rows {
let mut result = build_row.clone();
result.extend(probe_row.clone());
results.push(result);
}
}
}
}
JoinType::LeftOuter => {
let build_row_width = self.build_row_width.unwrap_or_else(|| {
hash_table.values().next().and_then(|v| v.first()).map(|r| r.len()).unwrap_or(0)
});
for (key, probe_row) in probe_rows {
if let Some(build_rows) = hash_table.get(&key) {
for build_row in build_rows {
let mut result = build_row.clone();
result.extend(probe_row.clone());
results.push(result);
}
} else {
let mut result = vec![SqlValue::Null; build_row_width];
result.extend(probe_row);
results.push(result);
}
}
}
JoinType::RightOuter => {
let mut matched: std::collections::HashSet<usize> =
std::collections::HashSet::new();
let probe_row_width = self
.probe_row_width
.unwrap_or_else(|| probe_rows.first().map(|(_, r)| r.len()).unwrap_or(0));
let build_rows_vec: Vec<_> = hash_table
.iter()
.flat_map(|(k, rows)| rows.iter().map(move |r| (k.clone(), r.clone())))
.collect();
for (key, probe_row) in &probe_rows {
if let Some(build_rows) = hash_table.get(key) {
for build_row in build_rows {
let mut result = build_row.clone();
result.extend(probe_row.clone());
results.push(result);
}
for (i, (k, _)) in build_rows_vec.iter().enumerate() {
if k == key {
matched.insert(i);
}
}
}
}
for (i, (_, build_row)) in build_rows_vec.iter().enumerate() {
if !matched.contains(&i) {
let mut result = build_row.clone();
result.extend(vec![SqlValue::Null; probe_row_width]);
results.push(result);
}
}
}
}
Ok(results)
}
fn load_build_partition(
&mut self,
idx: usize,
) -> io::Result<Vec<(Vec<SqlValue>, Vec<SqlValue>)>> {
let partition = &mut self.build_partitions[idx];
if !partition.spilled {
return Ok(std::mem::take(&mut partition.rows));
}
let spill_file = partition.spill_file.as_mut().expect("spill file should exist");
spill_file.prepare_for_read()?;
let mut rows = Vec::new();
loop {
match read_keyed_row(spill_file) {
Ok(Some((key, row))) => rows.push((key, row)),
Ok(None) => break,
Err(e) => return Err(e),
}
}
Ok(rows)
}
fn load_probe_partition(
&mut self,
idx: usize,
) -> io::Result<Vec<(Vec<SqlValue>, Vec<SqlValue>)>> {
let partition = &mut self.probe_partitions[idx];
if !partition.spilled {
return Ok(std::mem::take(&mut partition.rows));
}
let spill_file = partition.spill_file.as_mut().expect("spill file should exist");
spill_file.prepare_for_read()?;
let mut rows = Vec::new();
loop {
match read_keyed_row(spill_file) {
Ok(Some((key, row))) => rows.push((key, row)),
Ok(None) => break,
Err(e) => return Err(e),
}
}
Ok(rows)
}
}
fn write_keyed_row(
spill_file: &mut SpillFile,
key: &[SqlValue],
row: &[SqlValue],
) -> io::Result<()> {
let key_len = key.len() as u16;
spill_file.write_all(&key_len.to_le_bytes())?;
let mut buf = Vec::new();
for v in key {
serialize_value(v, &mut buf)?;
}
spill_file.write_all(&buf)?;
let row_len = row.len() as u16;
spill_file.write_all(&row_len.to_le_bytes())?;
buf.clear();
for v in row {
serialize_value(v, &mut buf)?;
}
spill_file.write_all(&buf)?;
Ok(())
}
fn read_keyed_row(
spill_file: &mut SpillFile,
) -> io::Result<Option<(Vec<SqlValue>, Vec<SqlValue>)>> {
let mut len_buf = [0u8; 2];
match spill_file.read_exact(&mut len_buf) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
}
let key_len = u16::from_le_bytes(len_buf) as usize;
let mut key = Vec::with_capacity(key_len);
for _ in 0..key_len {
key.push(deserialize_value_from_spill(spill_file)?);
}
spill_file.read_exact(&mut len_buf)?;
let row_len = u16::from_le_bytes(len_buf) as usize;
let mut row = Vec::with_capacity(row_len);
for _ in 0..row_len {
row.push(deserialize_value_from_spill(spill_file)?);
}
Ok(Some((key, row)))
}
fn deserialize_value_from_spill(spill_file: &mut SpillFile) -> io::Result<SqlValue> {
struct SpillFileReader<'a>(&'a mut SpillFile);
impl Read for SpillFileReader<'_> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
}
let mut reader = SpillFileReader(spill_file);
deserialize_value(&mut reader)
}
fn hash_sql_value<H: std::hash::Hasher>(value: &SqlValue, hasher: &mut H) {
use std::hash::Hash;
match value {
SqlValue::Null => 0u8.hash(hasher),
SqlValue::Integer(i) => {
1u8.hash(hasher);
i.hash(hasher);
}
SqlValue::Smallint(i) => {
2u8.hash(hasher);
i.hash(hasher);
}
SqlValue::Bigint(i) => {
3u8.hash(hasher);
i.hash(hasher);
}
SqlValue::Unsigned(u) => {
4u8.hash(hasher);
u.hash(hasher);
}
SqlValue::Numeric(d) => {
5u8.hash(hasher);
d.to_string().hash(hasher);
}
SqlValue::Float(f) => {
6u8.hash(hasher);
f.to_bits().hash(hasher);
}
SqlValue::Real(f) => {
7u8.hash(hasher);
f.to_bits().hash(hasher);
}
SqlValue::Double(f) => {
8u8.hash(hasher);
f.to_bits().hash(hasher);
}
SqlValue::Character(s) | SqlValue::Varchar(s) => {
9u8.hash(hasher);
s.hash(hasher);
}
SqlValue::Boolean(b) => {
10u8.hash(hasher);
b.hash(hasher);
}
SqlValue::Date(d) => {
11u8.hash(hasher);
d.hash(hasher);
}
SqlValue::Time(t) => {
12u8.hash(hasher);
t.hash(hasher);
}
SqlValue::Timestamp(ts) => {
13u8.hash(hasher);
ts.hash(hasher);
}
SqlValue::Interval(iv) => {
14u8.hash(hasher);
iv.value.hash(hasher);
}
SqlValue::Vector(v) => {
15u8.hash(hasher);
for f in v {
f.to_bits().hash(hasher);
}
}
SqlValue::Blob(b) => {
16u8.hash(hasher);
b.hash(hasher);
}
}
}
fn estimate_row_size(row: &[SqlValue]) -> usize {
let base_size = std::mem::size_of::<Vec<SqlValue>>() + std::mem::size_of_val(row);
let value_size: usize = row
.iter()
.map(|v| match v {
SqlValue::Varchar(s) | SqlValue::Character(s) => s.len(),
SqlValue::Vector(vec) => vec.len() * std::mem::size_of::<f32>(),
_ => 0,
})
.sum();
base_size + value_size
}
pub struct HashJoinResultIterator {
results: std::vec::IntoIter<Vec<SqlValue>>,
#[allow(dead_code)]
_reservation: MemoryReservation,
}
impl Iterator for HashJoinResultIterator {
type Item = io::Result<Vec<SqlValue>>;
fn next(&mut self) -> Option<Self::Item> {
self.results.next().map(Ok)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_controller() -> Arc<MemoryController> {
Arc::new(MemoryController::with_budget(1024 * 1024)) }
#[test]
fn test_inner_join_basic() {
let controller = make_test_controller();
let config = ExternalHashJoinConfig { num_partitions: 4, max_rows_per_partition: 100 };
let mut join = ExternalHashJoin::new(
controller,
config,
vec![0], vec![0], JoinType::Inner,
);
join.add_build_row(&[SqlValue::Integer(1), SqlValue::Varchar("Alice".into())]).unwrap();
join.add_build_row(&[SqlValue::Integer(2), SqlValue::Varchar("Bob".into())]).unwrap();
join.add_build_row(&[SqlValue::Integer(3), SqlValue::Varchar("Charlie".into())]).unwrap();
join.add_probe_row(&[SqlValue::Integer(1), SqlValue::Varchar("NYC".into())]).unwrap();
join.add_probe_row(&[SqlValue::Integer(2), SqlValue::Varchar("LA".into())]).unwrap();
join.add_probe_row(&[SqlValue::Integer(4), SqlValue::Varchar("Chicago".into())]).unwrap();
let results: Vec<_> = join.finish().unwrap().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 2);
let alice_match = results.iter().find(|r| r[1] == SqlValue::Varchar("Alice".into()));
assert!(alice_match.is_some());
assert_eq!(alice_match.unwrap()[3], SqlValue::Varchar("NYC".into()));
let bob_match = results.iter().find(|r| r[1] == SqlValue::Varchar("Bob".into()));
assert!(bob_match.is_some());
assert_eq!(bob_match.unwrap()[3], SqlValue::Varchar("LA".into()));
}
#[test]
fn test_left_outer_join() {
let controller = make_test_controller();
let config = ExternalHashJoinConfig { num_partitions: 4, max_rows_per_partition: 100 };
let mut join =
ExternalHashJoin::new(controller, config, vec![0], vec![0], JoinType::LeftOuter);
join.add_build_row(&[SqlValue::Integer(1), SqlValue::Varchar("A".into())]).unwrap();
join.add_probe_row(&[SqlValue::Integer(1), SqlValue::Varchar("X".into())]).unwrap();
join.add_probe_row(&[SqlValue::Integer(2), SqlValue::Varchar("Y".into())]).unwrap();
let results: Vec<_> = join.finish().unwrap().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 2);
let matched = results.iter().filter(|r| r[0] != SqlValue::Null).count();
let unmatched = results.iter().filter(|r| r[0] == SqlValue::Null).count();
assert_eq!(matched, 1);
assert_eq!(unmatched, 1);
}
#[test]
fn test_multi_key_join() {
let controller = make_test_controller();
let config = ExternalHashJoinConfig { num_partitions: 4, max_rows_per_partition: 100 };
let mut join = ExternalHashJoin::new(
controller,
config,
vec![0, 1], vec![0, 1],
JoinType::Inner,
);
join.add_build_row(&[
SqlValue::Integer(1),
SqlValue::Integer(10),
SqlValue::Varchar("X".into()),
])
.unwrap();
join.add_build_row(&[
SqlValue::Integer(1),
SqlValue::Integer(20),
SqlValue::Varchar("Y".into()),
])
.unwrap();
join.add_probe_row(&[
SqlValue::Integer(1),
SqlValue::Integer(10),
SqlValue::Varchar("P".into()),
])
.unwrap();
join.add_probe_row(&[
SqlValue::Integer(1),
SqlValue::Integer(30),
SqlValue::Varchar("Q".into()),
])
.unwrap();
let results: Vec<_> = join.finish().unwrap().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 1);
assert_eq!(results[0][2], SqlValue::Varchar("X".into()));
assert_eq!(results[0][5], SqlValue::Varchar("P".into()));
}
#[test]
fn test_null_handling() {
let controller = make_test_controller();
let config = ExternalHashJoinConfig { num_partitions: 4, max_rows_per_partition: 100 };
let mut join = ExternalHashJoin::new(controller, config, vec![0], vec![0], JoinType::Inner);
join.add_build_row(&[SqlValue::Null, SqlValue::Varchar("A".into())]).unwrap();
join.add_build_row(&[SqlValue::Integer(1), SqlValue::Varchar("B".into())]).unwrap();
join.add_probe_row(&[SqlValue::Null, SqlValue::Varchar("X".into())]).unwrap();
join.add_probe_row(&[SqlValue::Integer(1), SqlValue::Varchar("Y".into())]).unwrap();
let results: Vec<_> = join.finish().unwrap().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 1);
assert_eq!(results[0][1], SqlValue::Varchar("B".into()));
}
#[test]
fn test_empty_inputs() {
let controller = make_test_controller();
let config = ExternalHashJoinConfig::default();
let join = ExternalHashJoin::new(controller, config, vec![0], vec![0], JoinType::Inner);
let results: Vec<_> = join.finish().unwrap().map(|r| r.unwrap()).collect();
assert!(results.is_empty());
}
#[test]
fn test_many_partitions() {
let controller = make_test_controller();
let config = ExternalHashJoinConfig { num_partitions: 16, max_rows_per_partition: 10 };
let mut join = ExternalHashJoin::new(controller, config, vec![0], vec![0], JoinType::Inner);
for i in 0..100 {
join.add_build_row(&[SqlValue::Integer(i), SqlValue::Integer(i * 10)]).unwrap();
join.add_probe_row(&[SqlValue::Integer(i), SqlValue::Integer(i * 100)]).unwrap();
}
let results: Vec<_> = join.finish().unwrap().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 100);
}
#[test]
fn test_duplicate_keys() {
let controller = make_test_controller();
let config = ExternalHashJoinConfig { num_partitions: 4, max_rows_per_partition: 100 };
let mut join = ExternalHashJoin::new(controller, config, vec![0], vec![0], JoinType::Inner);
join.add_build_row(&[SqlValue::Integer(1), SqlValue::Varchar("A".into())]).unwrap();
join.add_build_row(&[SqlValue::Integer(1), SqlValue::Varchar("B".into())]).unwrap();
join.add_probe_row(&[SqlValue::Integer(1), SqlValue::Varchar("X".into())]).unwrap();
join.add_probe_row(&[SqlValue::Integer(1), SqlValue::Varchar("Y".into())]).unwrap();
let results: Vec<_> = join.finish().unwrap().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 4);
}
}