Skip to content

Commit 917f823

Browse files
authored
Merge pull request #64 from poyrazK/fix/vectorized-hash-join-first-match-bug
fix(vectorized): remove break to allow multiple matches per left row in hash join
2 parents 1429f4a + a48b17b commit 917f823

2 files changed

Lines changed: 69 additions & 9 deletions

File tree

‎include/executor/vectorized_operator.hpp‎

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,12 @@ class VectorizedHashJoinOperator : public VectorizedOperator {
577577
std::vector<bool> left_matched_in_batch_;
578578
std::vector<size_t> unmatched_indices_;
579579

580+
// Probe state for resumable bucket scanning (prevents batch overflow)
581+
bool resuming_bucket_scan_ = false; // True if we're resuming a mid-bucket scan
582+
size_t resumed_bucket_idx_ = 0; // Bucket index when resuming
583+
size_t resumed_entry_idx_ = 0; // Entry index within bucket when resuming
584+
common::Value resumed_key_val_; // Key value being probed when resuming
585+
580586
// Join type
581587
JoinType join_type_;
582588

@@ -711,10 +717,52 @@ class VectorizedHashJoinOperator : public VectorizedOperator {
711717
left_row_idx_ = 0;
712718
// Reset matched tracking for new batch
713719
std::fill(left_matched_in_batch_.begin(), left_matched_in_batch_.end(), false);
720+
// Clear resume state when advancing to new batch
721+
resuming_bucket_scan_ = false;
714722
}
715723

716724
// Process rows in current batch
717725
while (left_row_idx_ < left_batch_->row_count() && out_batch.row_count() < BATCH_SIZE) {
726+
// Check if we need to resume an interrupted bucket scan
727+
if (resuming_bucket_scan_) {
728+
// We were in the middle of scanning a bucket - resume from saved position
729+
const auto& key_val = resumed_key_val_;
730+
auto& bucket = buckets_[resumed_bucket_idx_];
731+
bool found_match = left_matched_in_batch_[left_row_idx_];
732+
733+
// Resume scanning bucket from resumed_entry_idx_
734+
for (size_t i = resumed_entry_idx_; i < bucket.key_values.size(); ++i) {
735+
if (out_batch.row_count() >= BATCH_SIZE) {
736+
// Batch full - save state and return
737+
resuming_bucket_scan_ = true;
738+
resumed_bucket_idx_ = resumed_bucket_idx_;
739+
resumed_entry_idx_ = i;
740+
resumed_key_val_ = key_val;
741+
return true; // Caller must consume batch before continuing
742+
}
743+
744+
const auto& bucket_key = bucket.key_values[i][right_key_col_idx_];
745+
if (bucket_key == key_val) {
746+
emit_joined_row(out_batch, left_row_idx_, bucket.payload_rows[i]);
747+
found_match = true;
748+
if (join_type_ == JoinType::Left) {
749+
left_matched_in_batch_[left_row_idx_] = true;
750+
}
751+
}
752+
}
753+
754+
// Finished scanning this bucket
755+
resuming_bucket_scan_ = false;
756+
757+
// Track unmatched for LEFT join
758+
if (join_type_ == JoinType::Left && !found_match) {
759+
unmatched_indices_.push_back(left_row_idx_);
760+
}
761+
762+
left_row_idx_++;
763+
continue;
764+
}
765+
718766
const auto& key_val = left_batch_->get_column(left_key_col_idx_).get(left_row_idx_);
719767

720768
if (key_val.is_null()) {
@@ -732,6 +780,15 @@ class VectorizedHashJoinOperator : public VectorizedOperator {
732780
// Search for match in this bucket
733781
bool found_match = false;
734782
for (size_t i = 0; i < bucket.key_values.size(); ++i) {
783+
if (out_batch.row_count() >= BATCH_SIZE) {
784+
// Batch full - save state and return
785+
resuming_bucket_scan_ = true;
786+
resumed_bucket_idx_ = bucket_idx;
787+
resumed_entry_idx_ = i;
788+
resumed_key_val_ = key_val;
789+
return true; // Caller must consume batch before continuing
790+
}
791+
735792
const auto& bucket_key = bucket.key_values[i][right_key_col_idx_];
736793
if (bucket_key == key_val) {
737794
// Match found - emit row
@@ -740,7 +797,7 @@ class VectorizedHashJoinOperator : public VectorizedOperator {
740797
if (join_type_ == JoinType::Left) {
741798
left_matched_in_batch_[left_row_idx_] = true;
742799
}
743-
break; // Each left row matches at most one right row
800+
// Continue scanning bucket for all matching right rows
744801
}
745802
}
746803

‎tests/vectorized_operator_tests.cpp‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,8 +1119,7 @@ TEST_F(VectorizedGroupByTests, VectorizedHashJoinEmptyLeft) {
11191119

11201120
TEST_F(VectorizedGroupByTests, VectorizedHashJoinMultipleMatches) {
11211121
// Test when right has duplicate keys: id=1 appears twice
1122-
// Current implementation limitation: each left row matches at most ONE right row
1123-
// This is a known issue - proper hash join should match ALL right rows with same key
1122+
// Each left row should match ALL right rows with the same key
11241123
Schema left_schema;
11251124
left_schema.add_column("id", common::ValueType::TYPE_INT64);
11261125

@@ -1164,13 +1163,17 @@ TEST_F(VectorizedGroupByTests, VectorizedHashJoinMultipleMatches) {
11641163
result->clear();
11651164
}
11661165

1167-
// NOTE: Current implementation uses "break" after first match per left row
1168-
// So each left row only matches ONE right row, even if duplicates exist
1169-
// This test documents the current behavior; proper hash join would return 3
1170-
EXPECT_EQ(left_ids.size(), 2); // left_id=1 matches once, left_id=2 matches once
1171-
EXPECT_EQ(right_ids.size(), 2);
1166+
// INNER: left_id=1 matches 2 right rows, left_id=2 matches 1 right row = 3 total
1167+
EXPECT_EQ(left_ids.size(), 3);
1168+
EXPECT_EQ(right_ids.size(), 3);
11721169
EXPECT_EQ(left_ids[0], 1);
1173-
EXPECT_EQ(left_ids[1], 2);
1170+
EXPECT_EQ(left_ids[1], 1); // Second match for left id=1
1171+
EXPECT_EQ(left_ids[2], 2);
1172+
// Right-side: two rows with id=1 (matches for left_id=1), then one row with id=2 (match for
1173+
// left_id=2)
1174+
EXPECT_EQ(right_ids[0], 1);
1175+
EXPECT_EQ(right_ids[1], 1); // Second right row with id=1
1176+
EXPECT_EQ(right_ids[2], 2);
11741177
}
11751178

11761179
TEST_F(VectorizedGroupByTests, VectorizedHashJoinLeftNullKeys) {

0 commit comments

Comments
 (0)