Skip to content

MySQL - MINUS Operator

In standard SQL, set operators like UNION, INTERSECT, and MINUS (or EXCEPT in some dialects like PostgreSQL and SQL Server) are used to combine the results of two SELECT statements into a single result set. The MINUS operator is used to return all unique rows from the first query that are not present in the second query.

Important: MySQL does not support the MINUS or EXCEPT operator. However, you can achieve the exact same result using a LEFT JOIN.

Imagine you have two lists (results of two queries). Query1 MINUS Query2 would give you only the items that appear in the first list but not in the second.

Query1 Results Query2 Results
+-----------------------+ +-----------------------+
| Rows | | Rows |
| A, B, C | | B, D |
+-----------------------+ +-----------------------+
Query1 MINUS Query2 => A, C

For the MINUS operation to work, the SELECT statements must have the same number of columns with compatible data types.

The MySQL Solution: LEFT JOIN with a NULL Check

Section titled “The MySQL Solution: LEFT JOIN with a NULL Check”

The standard and most efficient way to emulate the MINUS operator in MySQL is by using a LEFT JOIN combined with a WHERE clause that checks for NULL values. This pattern effectively filters for rows in the left table that have no match in the right table.

SELECT
table1.column_to_return
FROM
table1
LEFT JOIN
table2 ON table1.matching_column = table2.matching_column
WHERE
table2.matching_column IS NULL;

Let’s use the classic customers and orders tables. We want to find all customers who have never placed an order. This is a perfect use case for a MINUS operation.

-- Setup Tables
CREATE TABLE customers (id INT PRIMARY KEY, name VARCHAR(100));
CREATE TABLE orders (order_id INT, customer_id INT, amount DECIMAL(10,2));
INSERT INTO customers VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie'), (4, 'David');
INSERT INTO orders VALUES (101, 1, 50.0), (102, 3, 120.0);

Conceptually, we want to do: (SELECT id FROM customers) MINUS (SELECT customer_id FROM orders).

Here’s how we implement it in MySQL:

SELECT
c.id,
c.name
FROM
customers AS c
LEFT JOIN
orders AS o ON c.id = o.customer_id
WHERE
o.customer_id IS NULL;
  1. The LEFT JOIN starts with all rows from the customers table.
  2. It then tries to find a matching customer_id in the orders table.
  3. If a match is found (for Alice and Charlie), the columns from orders are filled with data.
  4. If no match is found (for Bob and David), the columns from orders are filled with NULL.
  5. The WHERE o.customer_id IS NULL clause then filters the result set, keeping only those rows where no match was found.
idname
2Bob
4David

Alternative Methods (and Why LEFT JOIN is Often Better)

Section titled “Alternative Methods (and Why LEFT JOIN is Often Better)”

While LEFT JOIN is the canonical solution, you might also see other patterns like NOT IN or NOT EXISTS.

SELECT id, name
FROM customers
WHERE id NOT IN (SELECT DISTINCT customer_id FROM orders WHERE customer_id IS NOT NULL);

Caveat: NOT IN can behave unexpectedly if the subquery returns any NULL values. It also can have poor performance on large datasets compared to a JOIN.

SELECT id, name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);

Note: NOT EXISTS is often as performant as LEFT JOIN, and some developers find it more readable for this specific task. Both are generally considered superior to NOT IN.

Since the solution is a standard SELECT query, implementing it in an application is straightforward. The following Node.js example demonstrates how to find employees in one department who are not in another.

const mysql = require('mysql2/promise');
// Imagine we have two tables: 'engineering_staff' and 'on_call_roster'
// We want to find engineers who are NOT on the on-call roster.
async function findEngineersNotOnCall() {
let connection;
try {
connection = await mysql.createConnection({
host: 'localhost', user: 'root', password: 'your_password', database: 'your_database'
});
const sql = `
SELECT e.employee_id, e.name
FROM engineering_staff AS e
LEFT JOIN on_call_roster AS o ON e.employee_id = o.employee_id
WHERE o.employee_id IS NULL;
`;
const [engineers] = await connection.execute(sql);
console.log('Engineers not currently on the on-call roster:');
if (engineers.length > 0) {
engineers.forEach(eng => {
console.log(` - ID: ${eng.employee_id}, Name: ${eng.name}`);
});
} else {
console.log('All engineers are on the roster.');
}
} catch (error) {
console.error('Database query failed:', error);
} finally {
if (connection) await connection.end();
}
}
// Assume tables are pre-populated.
findEngineersNotOnCall();