🧠
Using Hash Join Concept (Hash Map Based)
💡 Hash joins improve efficiency by indexing one table on the join key, reducing comparisons drastically compared to nested loops. It's like using a dictionary to quickly find matching entries instead of scanning all entries.
Intuition
Build a hash map from one table keyed by join column, then probe this map with rows from the other table to find matches quickly.
Algorithm
- Build a hash map from the smaller table with join key as map key
- Iterate over the larger table and look up matching rows in the hash map
- For matches, combine rows and add to result
- Handle unmatched rows depending on join type (left, right, full)
💡 Hashing avoids repeated scanning, making joins scalable for large tables.
def hash_join(table_a, table_b, join_type='inner'):
# Decide which table to hash (smaller one)
if len(table_a) <= len(table_b):
build_table, probe_table = table_a, table_b
build_key = 'id'
probe_key = 'id'
build_prefix = 'a_'
probe_prefix = 'b_'
build_is_a = True
else:
build_table, probe_table = table_b, table_a
build_key = 'id'
probe_key = 'id'
build_prefix = 'b_'
probe_prefix = 'a_'
build_is_a = False
hash_map = {}
for row in build_table:
key = row[build_key]
if key not in hash_map:
hash_map[key] = []
hash_map[key].append(row)
result = []
matched_probe_keys = set()
for row in probe_table:
key = row[probe_key]
if key in hash_map:
for matched_row in hash_map[key]:
combined = {}
# Prefix keys to avoid collision
for k, v in matched_row.items():
combined[build_prefix + k] = v
for k, v in row.items():
combined[probe_prefix + k] = v
result.append(combined)
matched_probe_keys.add(key)
else:
if (join_type == 'left' and build_is_a) or (join_type == 'right' and not build_is_a) or join_type == 'full':
combined = {}
for k, v in (build_table[0].items() if build_table else {}).items():
combined[build_prefix + k] = v if join_type == 'full' else None
for k, v in row.items():
combined[probe_prefix + k] = v
result.append(combined)
if join_type == 'full':
for key, rows in hash_map.items():
if key not in matched_probe_keys:
for r in rows:
combined = {}
for k, v in r.items():
combined[build_prefix + k] = v
for k in (probe_table[0].keys() if probe_table else []):
combined[probe_prefix + k] = None
result.append(combined)
return result
# Example usage
if __name__ == '__main__':
A = [{'id':1, 'name':'Alice'}, {'id':2, 'name':'Bob'}]
B = [{'id':2, 'city':'NY'}, {'id':3, 'city':'LA'}]
print('INNER JOIN:', hash_join(A, B, 'inner'))
print('LEFT JOIN:', hash_join(A, B, 'left'))
print('RIGHT JOIN:', hash_join(A, B, 'right'))
print('FULL OUTER JOIN:', hash_join(A, B, 'full'))
Line Notes
if len(table_a) <= len(table_b)Choose smaller table to build hash map for efficiency
hash_map = {}Initialize hash map to store rows keyed by join column
for row in build_tableBuild hash map from smaller table
for row in probe_tableProbe hash map with rows from other table
if key in hash_mapCheck if join key exists in hash map
combined[build_prefix + k] = vPrefix keys to avoid collisions in combined row
if (join_type == 'left' and build_is_a) or ...Handle unmatched rows based on join type
return resultReturn combined join results
import java.util.*;
public class HashJoin {
public static List<Map<String, Object>> hashJoin(List<Map<String, Object>> tableA, List<Map<String, Object>> tableB, String joinType) {
List<Map<String, Object>> buildTable, probeTable;
String buildPrefix, probePrefix;
boolean buildIsA;
if (tableA.size() <= tableB.size()) {
buildTable = tableA; probeTable = tableB;
buildPrefix = "a_"; probePrefix = "b_";
buildIsA = true;
} else {
buildTable = tableB; probeTable = tableA;
buildPrefix = "b_"; probePrefix = "a_";
buildIsA = false;
}
Map<Object, List<Map<String, Object>>> hashMap = new HashMap<>();
for (Map<String, Object> row : buildTable) {
Object key = row.get("id");
hashMap.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
}
List<Map<String, Object>> result = new ArrayList<>();
Set<Object> matchedProbeKeys = new HashSet<>();
for (Map<String, Object> row : probeTable) {
Object key = row.get("id");
if (hashMap.containsKey(key)) {
for (Map<String, Object> matchedRow : hashMap.get(key)) {
Map<String, Object> combined = new HashMap<>();
for (Map.Entry<String, Object> e : matchedRow.entrySet()) {
combined.put(buildPrefix + e.getKey(), e.getValue());
}
for (Map.Entry<String, Object> e : row.entrySet()) {
combined.put(probePrefix + e.getKey(), e.getValue());
}
result.add(combined);
}
matchedProbeKeys.add(key);
} else {
if ((joinType.equals("left") && buildIsA) || (joinType.equals("right") && !buildIsA) || joinType.equals("full")) {
Map<String, Object> combined = new HashMap<>();
if (!buildTable.isEmpty()) {
for (String k : buildTable.get(0).keySet()) {
combined.put(buildPrefix + k, joinType.equals("full") ? buildTable.get(0).get(k) : null);
}
}
for (Map.Entry<String, Object> e : row.entrySet()) {
combined.put(probePrefix + e.getKey(), e.getValue());
}
result.add(combined);
}
}
}
if (joinType.equals("full")) {
for (Map.Entry<Object, List<Map<String, Object>>> entry : hashMap.entrySet()) {
if (!matchedProbeKeys.contains(entry.getKey())) {
for (Map<String, Object> r : entry.getValue()) {
Map<String, Object> combined = new HashMap<>();
for (Map.Entry<String, Object> e : r.entrySet()) {
combined.put(buildPrefix + e.getKey(), e.getValue());
}
if (!probeTable.isEmpty()) {
for (String k : probeTable.get(0).keySet()) {
combined.put(probePrefix + k, null);
}
}
result.add(combined);
}
}
}
}
return result;
}
public static void main(String[] args) {
List<Map<String, Object>> A = new ArrayList<>();
A.add(new HashMap<>() {{ put("id", 1); put("name", "Alice"); }});
A.add(new HashMap<>() {{ put("id", 2); put("name", "Bob"); }});
List<Map<String, Object>> B = new ArrayList<>();
B.add(new HashMap<>() {{ put("id", 2); put("city", "NY"); }});
B.add(new HashMap<>() {{ put("id", 3); put("city", "LA"); }});
System.out.println("INNER JOIN: " + hashJoin(A, B, "inner"));
System.out.println("LEFT JOIN: " + hashJoin(A, B, "left"));
System.out.println("RIGHT JOIN: " + hashJoin(A, B, "right"));
System.out.println("FULL OUTER JOIN: " + hashJoin(A, B, "full"));
}
}
Line Notes
if (tableA.size() <= tableB.size())Choose smaller table to build hash map for efficiency
Map<Object, List<Map<String, Object>>> hashMap = new HashMap<>();Initialize hash map keyed by join column
hashMap.computeIfAbsent(key, k -> new ArrayList<>()).add(row);Group rows by join key
for (Map<String, Object> row : probeTable)Probe hash map with other table rows
if (hashMap.containsKey(key))Check for matching keys
combined.put(buildPrefix + e.getKey(), e.getValue());Prefix keys to avoid collisions
if (joinType.equals("full"))Handle unmatched rows for full outer join
return result;Return combined join results
#include <iostream>
#include <vector>
#include <unordered_map>
#include <string>
#include <set>
using namespace std;
using Row = unordered_map<string, string>;
using Table = vector<Row>;
Table hashJoin(const Table& tableA, const Table& tableB, const string& joinType) {
const Table *buildTable, *probeTable;
string buildPrefix, probePrefix;
bool buildIsA;
if (tableA.size() <= tableB.size()) {
buildTable = &tableA; probeTable = &tableB;
buildPrefix = "a_"; probePrefix = "b_";
buildIsA = true;
} else {
buildTable = &tableB; probeTable = &tableA;
buildPrefix = "b_"; probePrefix = "a_";
buildIsA = false;
}
unordered_map<string, vector<Row>> hashMap;
for (const auto& row : *buildTable) {
string key = row.at("id");
hashMap[key].push_back(row);
}
Table result;
set<string> matchedProbeKeys;
for (const auto& row : *probeTable) {
string key = row.at("id");
if (hashMap.find(key) != hashMap.end()) {
for (const auto& matchedRow : hashMap[key]) {
Row combined;
// Prefix build table keys first
for (const auto& [k, v] : matchedRow) {
combined[buildPrefix + k] = v;
}
// Then prefix probe table keys
for (const auto& [k, v] : row) {
combined[probePrefix + k] = v;
}
result.push_back(combined);
}
matchedProbeKeys.insert(key);
} else {
if ((joinType == "left" && buildIsA) || (joinType == "right" && !buildIsA) || joinType == "full") {
Row combined;
if (!buildTable->empty()) {
for (const auto& [k, v] : (*buildTable)[0]) {
combined[buildPrefix + k] = (joinType == "full") ? v : "";
}
}
for (const auto& [k, v] : row) {
combined[probePrefix + k] = v;
}
result.push_back(combined);
}
}
}
if (joinType == "full") {
for (const auto& [key, rows] : hashMap) {
if (matchedProbeKeys.find(key) == matchedProbeKeys.end()) {
for (const auto& r : rows) {
Row combined;
for (const auto& [k, v] : r) {
combined[buildPrefix + k] = v;
}
if (!probeTable->empty()) {
for (const auto& [k, v] : (*probeTable)[0]) {
combined[probePrefix + k] = "";
}
}
result.push_back(combined);
}
}
}
}
return result;
}
int main() {
Table A = {{{"id", "1"}, {"name", "Alice"}}, {{"id", "2"}, {"name", "Bob"}}};
Table B = {{{"id", "2"}, {"city", "NY"}}, {{"id", "3"}, {"city", "LA"}}};
auto inner = hashJoin(A, B, "inner");
cout << "INNER JOIN:" << endl;
for (auto& row : inner) {
for (auto& [k, v] : row) cout << k << ":" << v << " ";
cout << endl;
}
return 0;
}
Line Notes
Table hashJoin(const Table& tableA, const Table& tableB, const string& joinType)Function to perform hash join with join type
if (tableA.size() <= tableB.size())Choose smaller table to build hash map
unordered_map<string, vector<Row>> hashMap;Hash map keyed by join column
for (const auto& row : *buildTable)Build hash map from smaller table
for (const auto& row : *probeTable)Probe hash map with other table rows
if (hashMap.find(key) != hashMap.end())Check for matching keys
if (joinType == "full")Handle unmatched rows for full outer join
return result;Return combined join results
function hashJoin(tableA, tableB, joinType = 'inner') {
let buildTable, probeTable, buildPrefix, probePrefix, buildIsA;
if (tableA.length <= tableB.length) {
buildTable = tableA; probeTable = tableB;
buildPrefix = 'a_'; probePrefix = 'b_';
buildIsA = true;
} else {
buildTable = tableB; probeTable = tableA;
buildPrefix = 'b_'; probePrefix = 'a_';
buildIsA = false;
}
const hashMap = new Map();
for (const row of buildTable) {
const key = row.id;
if (!hashMap.has(key)) hashMap.set(key, []);
hashMap.get(key).push(row);
}
const result = [];
const matchedProbeKeys = new Set();
for (const row of probeTable) {
const key = row.id;
if (hashMap.has(key)) {
for (const matchedRow of hashMap.get(key)) {
const combined = {};
for (const k in matchedRow) combined[buildPrefix + k] = matchedRow[k];
for (const k in row) combined[probePrefix + k] = row[k];
result.push(combined);
}
matchedProbeKeys.add(key);
} else {
if ((joinType === 'left' && buildIsA) || (joinType === 'right' && !buildIsA) || joinType === 'full') {
const combined = {};
if (buildTable.length > 0) {
for (const k in buildTable[0]) {
combined[buildPrefix + k] = joinType === 'full' ? buildTable[0][k] : null;
}
}
for (const k in row) combined[probePrefix + k] = row[k];
result.push(combined);
}
}
}
if (joinType === 'full') {
for (const [key, rows] of hashMap.entries()) {
if (!matchedProbeKeys.has(key)) {
for (const r of rows) {
const combined = {};
for (const k in r) combined[buildPrefix + k] = r[k];
if (probeTable.length > 0) {
for (const k in probeTable[0]) combined[probePrefix + k] = null;
}
result.push(combined);
}
}
}
}
return result;
}
// Example usage
const A = [{id:1, name:'Alice'}, {id:2, name:'Bob'}];
const B = [{id:2, city:'NY'}, {id:3, city:'LA'}];
console.log('INNER JOIN:', hashJoin(A, B, 'inner'));
console.log('LEFT JOIN:', hashJoin(A, B, 'left'));
console.log('RIGHT JOIN:', hashJoin(A, B, 'right'));
console.log('FULL OUTER JOIN:', hashJoin(A, B, 'full'));
Line Notes
if (tableA.length <= tableB.length)Choose smaller table to build hash map for efficiency
const hashMap = new Map();Initialize hash map keyed by join column
for (const row of buildTable)Build hash map from smaller table
for (const row of probeTable)Probe hash map with other table rows
if (hashMap.has(key))Check for matching keys
combined[buildPrefix + k] = matchedRow[k];Prefix keys to avoid collisions
if (joinType === 'full')Handle unmatched rows for full outer join
return result;Return combined join results
TimeO(n + m) where n and m are sizes of the two tables
SpaceO(min(n, m)) for hash map storage
Building a hash map and probing it reduces comparisons drastically compared to nested loops
💡 For 1000 rows each, this approach does about 2000 operations instead of 1,000,000
Interview Verdict: Efficient and practical for large datasets; preferred over nested loops
This approach is what real database engines use internally for many joins