Bird
Raised Fist0
Interview Prepdatabase-management-systemseasyAmazonGoogleMicrosoftFlipkartSwiggyRazorpay

SQL Joins - INNER, LEFT, RIGHT, FULL OUTER, CROSS, SELF

Choose your preparation mode3 modes available
🎯
SQL Joins - INNER, LEFT, RIGHT, FULL OUTER, CROSS, SELF
easyDBMSAmazonGoogleMicrosoft

Imagine you work at an e-commerce company where you need to combine customer and order data to generate reports. Understanding how different SQL joins work is crucial to get accurate results.

💡 SQL joins are fundamental to combining data from multiple tables. Beginners often struggle because each join type returns different results depending on how tables relate, and it's easy to confuse their outputs without concrete examples. Think of joins as ways to connect puzzle pieces based on matching edges (keys).
📋
Problem Statement

Given two tables, write SQL queries to demonstrate the behavior of INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN, CROSS JOIN, and SELF JOIN. For each join type, explain how rows from the two tables are combined and what rows appear in the result.

Tables can have up to 10^5 rowsColumns used for joining have matching data typesNULL values may appear in join columnsAssume standard SQL syntax
💡
Example
Input"Table A: {id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}\nTable B: {id: 2, city: 'NY'}, {id: 3, city: 'LA'}"
OutputINNER JOIN result: {id: 2, name: 'Bob', city: 'NY'} LEFT JOIN result: {id: 1, name: 'Alice', city: NULL}, {id: 2, name: 'Bob', city: 'NY'} RIGHT JOIN result: {id: 2, name: 'Bob', city: 'NY'}, {id: 3, name: NULL, city: 'LA'} FULL OUTER JOIN result: {id: 1, name: 'Alice', city: NULL}, {id: 2, name: 'Bob', city: 'NY'}, {id: 3, name: NULL, city: 'LA'} CROSS JOIN result: all combinations of rows from A and B SELF JOIN example: joining Table A with itself on id

INNER JOIN returns rows with matching ids; LEFT JOIN returns all from A with matches or NULLs; RIGHT JOIN returns all from B with matches or NULLs; FULL OUTER JOIN returns all rows from both with NULLs where no match; CROSS JOIN returns Cartesian product; SELF JOIN joins table with itself.

  • One or both tables are empty → result depends on join type
  • Join columns contain NULL values → affects matching rows
  • Multiple matching rows in one or both tables → duplicates in result
  • Self join with aliasing → avoid ambiguous column references
⚠️
Common Mistakes
Confusing LEFT JOIN with RIGHT JOIN

Results include unexpected NULLs or missing rows

✅ Remember LEFT JOIN keeps all rows from left table; RIGHT JOIN keeps all from right

Forgetting to alias tables in SELF JOIN

SQL error due to ambiguous column references

✅ Always alias tables when joining a table with itself

Using CROSS JOIN when INNER JOIN is intended

Result is Cartesian product, causing huge output

✅ Use INNER JOIN with ON clause to restrict matches

Assuming FULL OUTER JOIN is supported everywhere

Query fails on some databases like MySQL

✅ Use UNION of LEFT and RIGHT JOINs as workaround

Not handling NULLs in join columns

Rows with NULL join keys are excluded unexpectedly

✅ Be aware NULLs do not match any value; use IS NULL checks if needed

🧠
Brute Force (Nested Loop Join Simulation)
💡 This approach simulates how joins work internally by checking every pair of rows. It helps beginners understand the fundamental mechanics behind joins before using SQL syntax. Imagine checking every possible pair of customers and orders to find matches.

Intuition

For each row in the first table, compare it with every row in the second table to find matching rows based on join conditions.

Algorithm

  1. Iterate over each row in Table A
  2. For each row in Table A, iterate over each row in Table B
  3. Check if the join condition is satisfied
  4. Depending on join type, decide whether to include the combined row or handle unmatched rows
💡 This nested iteration is straightforward but inefficient; it reveals the core logic behind all join types.
</>
Code
def nested_loop_join(table_a, table_b, join_type='inner'):
    result = []
    matched_b_indices = set()
    for i, row_a in enumerate(table_a):
        matched = False
        for j, row_b in enumerate(table_b):
            if row_a['id'] == row_b['id']:
                matched = True
                matched_b_indices.add(j)
                combined = {**row_a, **row_b}
                result.append(combined)
        if not matched and join_type in ('left', 'full'):
            combined = {**row_a}
            for key in table_b[0].keys():
                if key != 'id':
                    combined[key] = None
            result.append(combined)
    if join_type in ('right', 'full'):
        for j, row_b in enumerate(table_b):
            if j not in matched_b_indices:
                combined = {**row_b}
                for key in table_a[0].keys():
                    if key != 'id':
                        combined[key] = 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:', nested_loop_join(A, B, 'inner'))
    print('LEFT JOIN:', nested_loop_join(A, B, 'left'))
    print('RIGHT JOIN:', nested_loop_join(A, B, 'right'))
    print('FULL OUTER JOIN:', nested_loop_join(A, B, 'full'))
Line Notes
def nested_loop_join(table_a, table_b, join_type='inner')Defines a function to simulate joins with a parameter for join type
for i, row_a in enumerate(table_a):Outer loop iterates over each row in first table
for j, row_b in enumerate(table_b):Inner loop iterates over each row in second table to find matches
if row_a['id'] == row_b['id']Checks join condition on 'id' column
matched_b_indices.add(j)Tracks matched rows in second table for right/full joins
if not matched and join_type in ('left', 'full')Handles unmatched rows from first table for left/full joins by adding NULLs
return resultReturns the combined result list
import java.util.*;
public class NestedLoopJoin {
    public static List<Map<String, Object>> nestedLoopJoin(List<Map<String, Object>> tableA, List<Map<String, Object>> tableB, String joinType) {
        List<Map<String, Object>> result = new ArrayList<>();
        Set<Integer> matchedBIndices = new HashSet<>();
        for (int i = 0; i < tableA.size(); i++) {
            Map<String, Object> rowA = tableA.get(i);
            boolean matched = false;
            for (int j = 0; j < tableB.size(); j++) {
                Map<String, Object> rowB = tableB.get(j);
                if (rowA.get("id").equals(rowB.get("id"))) {
                    matched = true;
                    matchedBIndices.add(j);
                    Map<String, Object> combined = new HashMap<>();
                    combined.putAll(rowA);
                    combined.putAll(rowB);
                    result.add(combined);
                }
            }
            if (!matched && (joinType.equals("left") || joinType.equals("full"))) {
                Map<String, Object> combined = new HashMap<>(rowA);
                for (String key : tableB.get(0).keySet()) {
                    if (!key.equals("id")) {
                        combined.put(key, null);
                    }
                }
                result.add(combined);
            }
        }
        if (joinType.equals("right") || joinType.equals("full")) {
            for (int j = 0; j < tableB.size(); j++) {
                if (!matchedBIndices.contains(j)) {
                    Map<String, Object> rowB = tableB.get(j);
                    Map<String, Object> combined = new HashMap<>(rowB);
                    for (String key : tableA.get(0).keySet()) {
                        if (!key.equals("id")) {
                            combined.put(key, 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: " + nestedLoopJoin(A, B, "inner"));
        System.out.println("LEFT JOIN: " + nestedLoopJoin(A, B, "left"));
        System.out.println("RIGHT JOIN: " + nestedLoopJoin(A, B, "right"));
        System.out.println("FULL OUTER JOIN: " + nestedLoopJoin(A, B, "full"));
    }
}
Line Notes
public static List<Map<String, Object>> nestedLoopJoinDefines method to simulate joins with join type parameter
for (int i = 0; i < tableA.size(); i++)Outer loop over first table rows
for (int j = 0; j < tableB.size(); j++)Inner loop over second table rows
if (rowA.get("id").equals(rowB.get("id")))Checks join condition on 'id'
matchedBIndices.add(j)Records matched indices in second table
if (!matched && (joinType.equals("left") || joinType.equals("full")))Handles unmatched rows from first table
if (joinType.equals("right") || joinType.equals("full"))Handles unmatched rows from second table
return result;Returns combined join result
#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 nestedLoopJoin(const Table& tableA, const Table& tableB, const string& joinType) {
    Table result;
    set<int> matchedBIndices;
    for (const auto& rowA : tableA) {
        bool matched = false;
        for (int j = 0; j < (int)tableB.size(); j++) {
            const auto& rowB = tableB[j];
            if (rowA.at("id") == rowB.at("id")) {
                matched = true;
                matchedBIndices.insert(j);
                Row combined = rowA;
                for (const auto& [k, v] : rowB) {
                    combined[k] = v;
                }
                result.push_back(combined);
            }
        }
        if (!matched && (joinType == "left" || joinType == "full")) {
            Row combined = rowA;
            for (const auto& [k, v] : tableB[0]) {
                if (k != "id") {
                    combined[k] = "";  // Use empty string to represent NULL
                }
            }
            result.push_back(combined);
        }
    }
    if (joinType == "right" || joinType == "full") {
        for (int j = 0; j < (int)tableB.size(); j++) {
            if (matchedBIndices.find(j) == matchedBIndices.end()) {
                Row combined = tableB[j];
                for (const auto& [k, v] : tableA[0]) {
                    if (k != "id") {
                        combined[k] = "";  // Use empty string to represent NULL
                    }
                }
                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 = nestedLoopJoin(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 nestedLoopJoin(const Table& tableA, const Table& tableB, const string& joinType)Function simulating join with join type parameter
for (const auto& rowA : tableA)Outer loop over first table rows
for (int j = 0; j < (int)tableB.size(); j++)Inner loop over second table rows
if (rowA.at("id") == rowB.at("id"))Checks join condition on 'id'
matchedBIndices.insert(j)Tracks matched rows in second table
if (!matched && (joinType == "left" || joinType == "full"))Handles unmatched rows from first table
if (joinType == "right" || joinType == "full")Handles unmatched rows from second table
return result;Returns combined join result
function nestedLoopJoin(tableA, tableB, joinType = 'inner') {
    const result = [];
    const matchedBIndices = new Set();
    for (const rowA of tableA) {
        let matched = false;
        for (let j = 0; j < tableB.length; j++) {
            const rowB = tableB[j];
            if (rowA.id === rowB.id) {
                matched = true;
                matchedBIndices.add(j);
                result.push({...rowA, ...rowB});
            }
        }
        if (!matched && (joinType === 'left' || joinType === 'full')) {
            const combined = {...rowA};
            for (const key in tableB[0]) {
                if (key !== 'id') combined[key] = null;
            }
            result.push(combined);
        }
    }
    if (joinType === 'right' || joinType === 'full') {
        for (let j = 0; j < tableB.length; j++) {
            if (!matchedBIndices.has(j)) {
                const combined = {...tableB[j]};
                for (const key in tableA[0]) {
                    if (key !== 'id') combined[key] = 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:', nestedLoopJoin(A, B, 'inner'));
console.log('LEFT JOIN:', nestedLoopJoin(A, B, 'left'));
console.log('RIGHT JOIN:', nestedLoopJoin(A, B, 'right'));
console.log('FULL OUTER JOIN:', nestedLoopJoin(A, B, 'full'));
Line Notes
function nestedLoopJoin(tableA, tableB, joinType = 'inner')Defines function to simulate joins with join type parameter
for (const rowA of tableA)Outer loop over first table rows
for (let j = 0; j < tableB.length; j++)Inner loop over second table rows
if (rowA.id === rowB.id)Checks join condition on 'id'
matchedBIndices.add(j)Tracks matched rows in second table
if (!matched && (joinType === 'left' || joinType === 'full'))Handles unmatched rows from first table
if (joinType === 'right' || joinType === 'full')Handles unmatched rows from second table
return result;Returns combined join result
⏱
Complexity
TimeO(n*m) where n and m are sizes of the two tables
SpaceO(n*m) in worst case for storing results

Every row in table A is compared with every row in table B, leading to quadratic time complexity

💡 For 1000 rows in each table, this means 1,000,000 comparisons, which is inefficient for large data
Interview Verdict: Use only to introduce join concepts; not practical for large datasets

This approach is too slow for real databases but is invaluable for understanding join mechanics

🧠
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

  1. Build a hash map from the smaller table with join key as map key
  2. Iterate over the larger table and look up matching rows in the hash map
  3. For matches, combine rows and add to result
  4. Handle unmatched rows depending on join type (left, right, full)
💡 Hashing avoids repeated scanning, making joins scalable for large tables.
</>
Code
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
⏱
Complexity
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

🧠
SQL Query Syntax with Examples
💡 After understanding join mechanics, writing correct SQL queries is essential for interviews and real work. Think of SQL join keywords as instructions to the database engine on how to combine tables.

Intuition

Use SQL keywords INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN, CROSS JOIN, and aliasing for SELF JOIN to express different join types.

Algorithm

  1. Write SELECT statement with columns from both tables
  2. Use appropriate JOIN keyword and ON clause for join condition
  3. For SELF JOIN, alias the table to join it with itself
  4. Run queries and interpret results to understand join behavior
💡 SQL syntax abstracts the join logic, so knowing keywords and clauses is key
</>
Code
def print_sql_queries():
    queries = {
        'INNER JOIN': "SELECT A.id, A.name, B.city FROM A INNER JOIN B ON A.id = B.id;",
        'LEFT JOIN': "SELECT A.id, A.name, B.city FROM A LEFT JOIN B ON A.id = B.id;",
        'RIGHT JOIN': "SELECT A.id, A.name, B.city FROM A RIGHT JOIN B ON A.id = B.id;",
        'FULL OUTER JOIN': "SELECT A.id, A.name, B.city FROM A FULL OUTER JOIN B ON A.id = B.id;",
        'CROSS JOIN': "SELECT A.id, A.name, B.city FROM A CROSS JOIN B;",
        'SELF JOIN': "SELECT A1.id, A1.name, A2.name AS manager_name FROM A AS A1 JOIN A AS A2 ON A1.manager_id = A2.id;"
    }
    for join_type, query in queries.items():
        print(f"-- {join_type} --")
        print(query)
        print()

if __name__ == '__main__':
    print_sql_queries()
Line Notes
queries = {Dictionary storing example SQL queries for each join type
'INNER JOIN': "SELECT A.id, A.name, B.city FROM A INNER JOIN B ON A.id = B.id;"Example of INNER JOIN syntax
'FULL OUTER JOIN': "SELECT A.id, A.name, B.city FROM A FULL OUTER JOIN B ON A.id = B.id;"Example of FULL OUTER JOIN syntax
for join_type, query in queries.items()Iterate and print each join type query
public class SQLQueries {
    public static void printSQLQueries() {
        String[] joinTypes = {"INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL OUTER JOIN", "CROSS JOIN", "SELF JOIN"};
        String[] queries = {
            "SELECT A.id, A.name, B.city FROM A INNER JOIN B ON A.id = B.id;",
            "SELECT A.id, A.name, B.city FROM A LEFT JOIN B ON A.id = B.id;",
            "SELECT A.id, A.name, B.city FROM A RIGHT JOIN B ON A.id = B.id;",
            "SELECT A.id, A.name, B.city FROM A FULL OUTER JOIN B ON A.id = B.id;",
            "SELECT A.id, A.name, B.city FROM A CROSS JOIN B;",
            "SELECT A1.id, A1.name, A2.name AS manager_name FROM A AS A1 JOIN A AS A2 ON A1.manager_id = A2.id;"
        };
        for (int i = 0; i < joinTypes.length; i++) {
            System.out.println("-- " + joinTypes[i] + " --");
            System.out.println(queries[i]);
            System.out.println();
        }
    }

    public static void main(String[] args) {
        printSQLQueries();
    }
}
Line Notes
String[] joinTypes = {...};Array of join type names
String[] queries = {...};Array of example SQL queries for each join type
for (int i = 0; i < joinTypes.length; i++)Loop to print each join type and query
System.out.println(queries[i]);Print the SQL query string
#include <iostream>
#include <string>
using namespace std;

void printSQLQueries() {
    string joinTypes[] = {"INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL OUTER JOIN", "CROSS JOIN", "SELF JOIN"};
    string queries[] = {
        "SELECT A.id, A.name, B.city FROM A INNER JOIN B ON A.id = B.id;",
        "SELECT A.id, A.name, B.city FROM A LEFT JOIN B ON A.id = B.id;",
        "SELECT A.id, A.name, B.city FROM A RIGHT JOIN B ON A.id = B.id;",
        "SELECT A.id, A.name, B.city FROM A FULL OUTER JOIN B ON A.id = B.id;",
        "SELECT A.id, A.name, B.city FROM A CROSS JOIN B;",
        "SELECT A1.id, A1.name, A2.name AS manager_name FROM A AS A1 JOIN A AS A2 ON A1.manager_id = A2.id;"
    };
    for (int i = 0; i < 6; i++) {
        cout << "-- " << joinTypes[i] << " --" << endl;
        cout << queries[i] << endl << endl;
    }
}

int main() {
    printSQLQueries();
    return 0;
}
Line Notes
string joinTypes[] = {...};Array of join type names
string queries[] = {...};Array of example SQL queries
for (int i = 0; i < 6; i++)Loop to print each join type and query
cout << queries[i] << endl << endl;Print the SQL query string
function printSQLQueries() {
    const joinTypes = ['INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'FULL OUTER JOIN', 'CROSS JOIN', 'SELF JOIN'];
    const queries = [
        'SELECT A.id, A.name, B.city FROM A INNER JOIN B ON A.id = B.id;',
        'SELECT A.id, A.name, B.city FROM A LEFT JOIN B ON A.id = B.id;',
        'SELECT A.id, A.name, B.city FROM A RIGHT JOIN B ON A.id = B.id;',
        'SELECT A.id, A.name, B.city FROM A FULL OUTER JOIN B ON A.id = B.id;',
        'SELECT A.id, A.name, B.city FROM A CROSS JOIN B;',
        'SELECT A1.id, A1.name, A2.name AS manager_name FROM A AS A1 JOIN A AS A2 ON A1.manager_id = A2.id;'
    ];
    for (let i = 0; i < joinTypes.length; i++) {
        console.log(`-- ${joinTypes[i]} --`);
        console.log(queries[i]);
        console.log();
    }
}

printSQLQueries();
Line Notes
const joinTypes = [...]Array of join type names
const queries = [...]Array of example SQL queries
for (let i = 0; i < joinTypes.length; i++)Loop to print each join type and query
console.log(queries[i]);Print the SQL query string
⏱
Complexity
TimeO(1) since queries are static strings
SpaceO(1) for storing queries

This approach is about writing correct SQL syntax, not algorithmic complexity

💡 Knowing these queries by heart is crucial for interviews and practical SQL work
Interview Verdict: Must know and write correctly in interviews

Interviewers expect you to write these queries fluently and explain their differences

📊
All Approaches - One-Glance Tradeoffs
💡 In interviews, explain brute force to show understanding, then code hash join or write SQL queries depending on question.
ApproachTimeSpaceStack RiskReconstructUse In Interview
1. Brute Force (Nested Loop)O(n*m)O(n*m)NoN/AMention only - never code for large data
2. Hash Join (Hash Map Based)O(n + m)O(min(n, m))NoN/ACode if asked for efficient join simulation
3. SQL Query SyntaxO(1) for writing queriesO(1)NoN/AMust know and write fluently
💼
Interview Strategy
💡 Use this guide to understand SQL joins from basics to writing queries. Practice explaining join types clearly and writing correct SQL syntax before interviews.

How to Present

Step 1: Clarify the problem and tables involvedStep 2: Explain the brute force nested loop join to show join mechanicsStep 3: Introduce hash join for efficiencyStep 4: Write and explain SQL queries for each join typeStep 5: Discuss edge cases and test with examples

Time Allocation

Clarify: 2min → Approach: 5min → Code: 5min → Test: 3min. Total ~15min

What the Interviewer Tests

Understanding of join types, ability to write correct SQL, and knowledge of performance implications

Common Follow-ups

  • How do NULL values affect join results? → NULLs do not match any value including other NULLs
  • Explain differences between INNER and OUTER joins → INNER returns only matches; OUTER includes unmatched rows
💡 These follow-ups test deeper understanding of join behavior and edge cases
🔍
Pattern Recognition

When to Use

1) Need to combine rows from two or more tables 2) Problem mentions matching keys or columns 3) Output requires rows with matching or all rows with NULLs 4) Keywords like INNER JOIN, LEFT JOIN, or CROSS JOIN appear

Signature Phrases

JOIN ONLEFT JOININNER JOINFULL OUTER JOINSELF JOIN

NOT This Pattern When

Problems about only single table queries or no table combinations

Similar Problems

SQL Aggregations - often combined with joinsSQL Subqueries - alternative to some joinsSQL Window Functions - advanced row-wise computations

Practice

(1/5)
1. You have a large table frequently queried by range conditions on a date column and occasionally updated. Which indexing strategy is most appropriate to optimize query performance?
easy
A. Use a clustered index on the date column to store rows physically sorted by date.
B. Use a non-clustered index on the date column to avoid physical reordering on updates.
C. Use no index and rely on full table scans since updates are frequent.
D. Use a clustered index on a unique ID column and a non-clustered index on the date column.

Solution

  1. Step 1: Understand query pattern for range scans on date

    Range queries on the date column benefit from data physically sorted by date, enabling efficient range scans.
  2. Step 2: Consider clustered index effect on physical ordering

    A clustered index stores rows physically ordered by the indexed column, making range queries faster.
  3. Step 3: Evaluate update impact tradeoff

    Although updates cause some overhead due to physical reordering, occasional updates are an acceptable tradeoff for query speed.
  4. Step 4: Analyze other options

    Non-clustered index avoids physical reordering but range queries require extra lookups; no index leads to slow scans; clustered on unique ID doesn't optimize date range queries.
  5. Final Answer:

    Option A -> Option A
  6. Quick Check:

    Range queries -> clustered index on date column [OK]
Hint: Range queries -> clustered index on that column [OK]
Common Mistakes:
  • Assuming non-clustered index always better for updates
  • Ignoring physical data order benefits for range queries
  • Believing no index is better if updates exist
2. You have a relation R with attributes ABCD and functional dependencies F = {A -> B, B -> C}. You want to decompose R into smaller relations while ensuring no data loss. In which scenario would you prioritize a lossless join decomposition over dependency preservation?
easy
A. When all functional dependencies must be preserved at the cost of possible data loss during joins
B. When the schema is already in BCNF and no further decomposition is needed
C. When the primary goal is to avoid redundant data and anomalies, even if some dependencies are not preserved
D. When the relation has no functional dependencies and normalization is unnecessary

Solution

  1. Step 1: Understand lossless join vs dependency preservation

    Lossless join ensures no spurious tuples after decomposition, while dependency preservation ensures all FDs are enforceable without joins.
  2. Step 2: Analyze the trade-off

    Sometimes, to achieve lossless join, some dependencies may not be preserved in decomposed relations.
  3. Step 3: Scenario matching

    When avoiding data anomalies is critical, lossless join is prioritized even if some dependencies are lost.
  4. Final Answer:

    Option C -> Option C
  5. Quick Check:

    When all functional dependencies must be preserved at the cost of possible data loss during joins reverses the priorities, C and D are irrelevant to the scenario.
Hint: Lossless join prevents data loss; dependency preservation keeps all FDs enforceable.
Common Mistakes:
  • Confusing dependency preservation as more important than lossless join always
  • Assuming BCNF always guarantees dependency preservation
  • Believing relations without FDs need normalization
3. You need to perform a complex data validation and transformation that must be explicitly called by an application and return a single scalar value without side effects. Which database component is most appropriate?
easy
A. Function
B. Stored Procedure
C. Trigger
D. View

Solution

  1. Step 1: Identify the requirement for explicit invocation and return value

    Functions are designed to be called explicitly and return a value, often a scalar, without side effects.
  2. Step 2: Exclude triggers and stored procedures

    Triggers execute automatically on data changes and do not return values; stored procedures can have side effects and may not return a value directly.
  3. Step 3: Exclude views

    Views are virtual tables and do not perform procedural logic or return scalar values.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    Function fits the scenario of explicit call and scalar return without side effects.
Hint: Functions return values and have no side effects; procedures perform actions; triggers run automatically.
Common Mistakes:
  • Confusing stored procedures with functions regarding return values
  • Thinking triggers can be called explicitly
  • Assuming views can perform procedural logic
4. Which of the following statements about Armstrong's Axioms is INCORRECT?
medium
A. Reflexivity states that if Y is a subset of X, then X -> Y holds
B. Augmentation allows adding attributes to both sides of a dependency to infer new dependencies
C. Transitivity can be used to infer dependencies even if the intermediate attribute set is not minimal
D. Armstrong's Axioms alone guarantee minimal cover of functional dependencies

Solution

  1. Step 1: Review Armstrong's Axioms

    Reflexivity: If Y ⊆ X, then X -> Y.
    Augmentation: If X -> Y, then XZ -> YZ.
    Transitivity: If X -> Y and Y -> Z, then X -> Z.
  2. Step 2: Analyze each statement

    A is correct as per Reflexivity.
    B is correct; Augmentation adds attributes symmetrically.
    C is correct; Transitivity applies regardless of minimality.
    D is incorrect; Armstrong's Axioms infer dependencies but do not guarantee minimal cover without further reduction steps.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Minimal cover requires additional steps beyond Armstrong's Axioms.
Hint: Armstrong's Axioms infer dependencies but don't minimize them
Common Mistakes:
  • Assuming Armstrong's Axioms produce minimal covers automatically
  • Misunderstanding Augmentation as adding attributes only to one side
  • Believing Transitivity requires minimal intermediate sets
5. What is a key trade-off or limitation of MVCC in terms of storage and performance?
medium
A. MVCC requires maintaining multiple versions, increasing storage and cleanup complexity
B. MVCC guarantees serializable isolation without any anomalies
C. MVCC eliminates all locking overhead, resulting in zero contention
D. MVCC reduces write amplification by avoiding version creation

Solution

  1. Step 1: Understand MVCC version management

    MVCC stores multiple versions of rows to provide snapshot isolation, which increases storage usage and requires vacuuming or cleanup.
  2. Step 2: Analyze locking overhead

    MVCC reduces locking but does not eliminate all contention, especially for writes.
  3. Step 3: Analyze isolation guarantees

    MVCC typically provides snapshot isolation, which can allow anomalies; serializability requires additional mechanisms.
  4. Step 4: Analyze write amplification

    MVCC increases write amplification due to version creation, not reduces it.
  5. Final Answer:

    Option A -> Option A
  6. Quick Check:

    MVCC trades off storage and cleanup overhead for concurrency benefits [OK]
Hint: MVCC trades storage and cleanup overhead for non-blocking reads
Common Mistakes:
  • Assuming MVCC removes all contention
  • Confusing snapshot isolation with serializability
  • Believing MVCC reduces write amplification
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard