acuamitca.com logo ACUAMITCA
~/blog/article

Understanding Semi Joins and Anti Joins in SQL

SQL 8 mins read August 07, 2026
 

INNER JOIN is often used to find matching rows, but it can create duplicate results if the matching table has multiple records. Semi-joins and anti-joins are conceptual patterns for existence checks, often implemented with EXISTS or NOT EXISTS.

⚡ Key Insight: Semi-joins find rows that have a match. Anti-joins find rows that don't have a match. Both are essential for existence-based queries.


The Problem: INNER JOIN Duplication

You need a list of customers who have placed at least one order over $100. A simple INNER JOIN would return each qualifying order, duplicating customer data.

-- ❌ Duplicates customer data
SELECT c.customer_id, c.customer_name, o.order_id, o.amount
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.amount > 100;

If a customer has 5 orders over $100, they appear 5 times. For a customer list, this is incorrect.


Solution 1: Semi-Join with EXISTS

A semi-join returns each customer only once, regardless of how many qualifying orders they have. It "semi-joins" the customer table with the orders table.

-- ✅ Clean approach with EXISTS
SELECT c.customer_id, c.customer_name
FROM customers c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.customer_id
      AND o.amount > 100
);

💡 Pro Tip: EXISTS often outperforms IN on large datasets because it can stop after the first match, while IN often builds a full result set.


Solution 2: Anti-Join with NOT EXISTS

An anti-join finds customers who have no orders over $100. It's the complement of the semi-join.

-- ✅ Clean approach with NOT EXISTS
SELECT c.customer_id, c.customer_name
FROM customers c
WHERE NOT EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.customer_id
      AND o.amount > 100
);

Alternative: Using IN and NOT IN

The IN operator can also implement semi-joins and anti-joins. However, EXISTS is often more performant and handles NULL values more safely.

-- Using IN (semi-join)
SELECT customer_id, customer_name
FROM customers
WHERE customer_id IN (
    SELECT customer_id
    FROM orders
    WHERE amount > 100
);

-- Using NOT IN (anti-join) - BE CAREFUL WITH NULLS!
SELECT customer_id, customer_name
FROM customers
WHERE customer_id NOT IN (
    SELECT customer_id
    FROM orders
    WHERE amount > 100
);

⚠️ Warning: NOT IN with a subquery that can return NULL values will return no results. NOT EXISTS is the safer choice.


EXISTS vs. IN: Performance Comparison

Scenario EXISTS IN
Large outer table, small inner table Good Better
Large inner table, small outer table Better Good
NULL handling Safe Risky (for NOT IN)
Early termination Yes Sometimes

Real-World Scenarios

1. Finding Inactive Users

SELECT u.id, u.username, u.email
FROM users u
WHERE NOT EXISTS (
    SELECT 1
    FROM logins l
    WHERE l.user_id = u.id
      AND l.login_date > DATE_SUB(NOW(), INTERVAL 30 DAY)
);

2. Finding Products Never Ordered

SELECT p.product_id, p.product_name
FROM products p
WHERE NOT EXISTS (
    SELECT 1
    FROM order_items oi
    WHERE oi.product_id = p.product_id
);

3. High-Value Customers with Multiple Orders

SELECT c.customer_id, c.customer_name
FROM customers c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.customer_id
      AND o.amount > 1000
    HAVING COUNT(*) > 3
);

Best Practices

✅ Use EXISTS for Large Datasets

EXISTS can stop after finding the first match, making it efficient for large inner tables.

✅ Use IN for Small Lists

IN with a small, known list (not a subquery) is very fast.

✅ Avoid NOT IN with NULLs

Use NOT EXISTS instead of NOT IN when subqueries may return NULL values.

✅ Test Performance

Always test both approaches with your actual data volumes to see which performs better.


Conclusion

📌 Key Takeaway: Semi-joins with EXISTS and anti-joins with NOT EXISTS are essential tools for existence-based queries. They avoid duplication, handle NULLs safely, and often outperform IN and NOT IN on large datasets.

🔍 Further Reading: Explore EXISTS with correlated subqueries, performance tuning, and advanced anti-join patterns for complex business rules.