Repository: duckdb/duckdb
File: src/execution/join_hashtable.cpp (C++)

Code before:
	ScanKeyMatches(keys);
	// then construct the result from all tuples with a match
	NextSemiOrAntiJoin<true>(keys, left, result);

	finished = true;
}

void ScanStructure::NextAntiJoin(DataChunk &keys, DataChunk &left, DataChunk &result) {
	// first scan for key matches
	ScanKeyMatches(keys);
	// then construct the result from all tuples that did not find a match
	NextSemiOrAntiJoin<false>(keys, left, result);

	finished = true;
}

void ScanStructure::NextRightSemiOrAntiJoin(DataChunk &keys) {
	const auto ptrs = FlatVector::GetData<data_ptr_t>(pointers);
	while (!PointersExhausted()) {
		// resolve the equality_predicates for this set of keys
		idx_t result_count = ResolvePredicates(keys, chain_match_sel_vector, nullptr);

		// for each match - mark the match as found
		for (idx_t i = 0; i < result_count; i++) {
			auto idx = chain_match_sel_vector.get_index(i);
			Store<bool>(true, ptrs[idx] + ht.tuple_size);
		}

		// check the next set of pointers
		AdvancePointers();
	}

	finished = true;
}

void ScanStructure::ConstructMarkJoinResult(DataChunk &join_keys, DataChunk &child, DataChunk &result) {
	// for the initial set of columns we just reference the left side
	result.SetCardinality(child);
	for (idx_t i = 0; i < child.ColumnCount(); i++) {
		result.data[i].Reference(child.data[i]);
	}
	auto &mark_vector = result.data.back();
	mark_vector.SetVectorType(VectorType::FLAT_VECTOR);
	// first we set the NULL values from the join keys
	// if there is any NULL in the keys, the result is NULL
	auto bool_result = FlatVector::GetData<bool>(mark_vector);
	auto &mask = FlatVector::Validity(mark_vector);
	for (idx_t col_idx = 0; col_idx < join_keys.ColumnCount(); col_idx++) {
		if (ht.null_values_are_equal[col_idx]) {
			continue;
		}

Review comment (performance):
Seems like the optimization is fully removed now. I added this optimization because RIGHT SEMI/ANTI became really slow if we have a build side with many duplicates (the original issue was a build side with 500k exact duplicates). Can we re-add the optimization for cases when there are only equality predicates?

Code after:
	ScanKeyMatches(keys);
	// then construct the result from all tuples with a match
	NextSemiOrAntiJoin<true>(keys, left, result);

	finished = true;
}

void ScanStructure::NextAntiJoin(DataChunk &keys, DataChunk &left, DataChunk &result) {
	// first scan for key matches
	ScanKeyMatches(keys);
	// then construct the result from all tuples that did not find a match
	NextSemiOrAntiJoin<false>(keys, left, result);

	finished = true;
}

void ScanStructure::NextRightSemiOrAntiJoin(DataChunk &keys) {
	const auto ptrs = FlatVector::GetData<data_ptr_t>(pointers);
	while (!PointersExhausted()) {
		// resolve the equality_predicates for this set of keys
		idx_t result_count = ResolvePredicates(keys, chain_match_sel_vector, nullptr);

		if (ht.non_equality_predicates.empty()) {
			// we only have equality predicates - the match is found for the entire chain
			for (idx_t i = 0; i < result_count; i++) {
				const auto idx = chain_match_sel_vector.get_index(i);
				auto &ptr = ptrs[idx];
				if (Load<bool>(ptr + ht.tuple_size)) { // Early out: chain has been fully marked as found before
					ptr = ht.dead_end.get();
					continue;
				}

				// Fully mark chain as found
				while (true) {
					// NOTE: threadsan reports this as a data race because this can be set concurrently by separate
					// threads Technically it is, but it does not matter, since the only value that can be written is
					// "true"
					Store<bool>(true, ptr + ht.tuple_size);
					auto next_ptr = LoadPointer(ptr + ht.pointer_offset);
					if (!next_ptr) {
						break;
					}
					ptr = next_ptr;
				}
			}
		} else {
			// we have non-equality predicates - we need to evaluate the join condition for every row
			// for each match found in the current pass - mark the match as found
			for (idx_t i = 0; i < result_count; i++) {
				auto idx = chain_match_sel_vector.get_index(i);
				Store<bool>(true, ptrs[idx] + ht.tuple_size);