Skip to content

MySQL Tutorial Filtering Rows with WHERE

Common WHERE Mistakes

Lesson 97 of 308 2 min read By Deepak Paswan Updated Oct 5, 2026

Most “wrong results” in SQL come from a few repeated mistakes. Each one is shown below with a fix.

query.sql
SQL
CREATE TABLE staff (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(30) NOT NULL,
    department VARCHAR(20) NOT NULL,
    salary INT NOT NULL,
    manager_id INT NULL,
    left_on DATE NULL
);
INSERT INTO staff (name, department, salary, manager_id, left_on) VALUES
    ('Asha',   'engineering', 120000, NULL, NULL),
    ('Ben',    'engineering',  90000, 1,    NULL),
    ('Chitra', 'sales',        70000, 1,    '2026-03-31'),
    ('Dev',    'sales',        65000, 3,    NULL),
    ('Esha',   'support',      50000, 1,    NULL);

1. = NULL instead of IS NULL

query.sql
SQL
SELECT name FROM staff WHERE left_on = NULL;
SELECT name FROM staff WHERE left_on IS NULL ORDER BY id;
Output
OUTPUT
Empty set

+------+
| name |
+------+
| Asha |
| Ben  |
| Dev  |
| Esha |
+------+
4 rows in set

2. OR without brackets

query.sql
SQL
SELECT name, department, salary FROM staff WHERE department = 'engineering' OR department = 'sales' AND salary > 80000 ORDER BY id;
SELECT name, department, salary FROM staff WHERE (department = 'engineering' OR department = 'sales') AND salary > 80000 ORDER BY id;
Output
OUTPUT
+------+-------------+--------+
| name | department  | salary |
+------+-------------+--------+
| Asha | engineering | 120000 |
| Ben  | engineering |  90000 |
+------+-------------+--------+
2 rows in set

+------+-------------+--------+
| name | department  | salary |
+------+-------------+--------+
| Asha | engineering | 120000 |
| Ben  | engineering |  90000 |
+------+-------------+--------+
2 rows in set

Ben (engineering, 90000) and Asha qualify in both. In the first query, Chitra and Dev would be included only if they earned more than 80000, and AND was applied only to sales. Be explicit.

3. NOT IN with NULL in the list

query.sql
SQL
SELECT name FROM staff WHERE id NOT IN (SELECT manager_id FROM staff);
SELECT name FROM staff s WHERE NOT EXISTS (SELECT 1 FROM staff m WHERE m.manager_id = s.id) ORDER BY id;
Output
OUTPUT
Empty set

+------+
| name |
+------+
| Ben  |
| Dev  |
| Esha |
+------+
3 rows in set

The first query returns nothing because manager_id contains NULL. The second correctly lists the people who manage nobody.

4. A function on the column

query.sql
SQL
SELECT name FROM staff WHERE YEAR(left_on) = 2026;
SELECT name FROM staff WHERE left_on >= '2026-01-01' AND left_on < '2027-01-01';
Output
OUTPUT
+--------+
| name   |
+--------+
| Chitra |
+--------+
1 row in set

+--------+
| name   |
+--------+
| Chitra |
+--------+
1 row in set

Same answer, but the second form can use an index (Sargable Conditions).

5. Comparing a number with text

query.sql
SQL
SELECT name FROM staff WHERE department = 0 ORDER BY id;
Output
OUTPUT
+--------+
| name   |
+--------+
| Asha   |
| Ben    |
| Chitra |
| Dev    |
| Esha   |
+--------+
5 rows in set

Every row matches! department = 0 converts the text to a number ('engineering' becomes 0), and all of them equal zero. That is the type-conversion trap (Type Conversion). Compare text with text.

6. Searching with BETWEEN on DATETIME

Covered in BETWEEN. Use >= start AND < next_start.

7. LIKE with a leading wildcard on a big table

LIKE '%son' reads every row. Consider full-text search (Full-Text Search).

8. Forgetting WHERE on UPDATE or DELETE

query.sql
SQL
UPDATE staff SET salary = 0;
SELECT name, salary FROM staff ORDER BY id LIMIT 2;
Output
OUTPUT
+------+--------+
| name | salary |
+------+--------+
| Asha |      0 |
| Ben  |      0 |
+------+--------+
2 rows in set

Every salary is now 0. Without a WHERE, UPDATE and DELETE hit all rows. Protect yourself:

  • Run the matching SELECT first.
  • Run changes inside a transaction and check the row count before COMMIT (Transactions).
  • In the mysql client, start it with --safe-updates (also -U), which refuses UPDATE and DELETE that have no key in the WHERE or a LIMIT.
query.sql
SQL
ROLLBACK;

(The rollback does nothing here, because autocommit had already saved the statement. In a real job, you would have started a transaction first.)

9. WHERE vs HAVING

WHERE filters rows before grouping. HAVING filters groups after. A condition on an aggregate (COUNT(*) > 1) belongs in HAVING (HAVING).

10. Trusting case or space

WHERE name = 'asha' matches Asha by default. Trailing spaces are significant in the default collation. Values with hidden spaces from imports do not match: use TRIM() when cleaning data, not in every query (Importing CSV Safely).

A debugging method

  1. Remove all conditions. Is the data what you think?
  2. Add them back one at a time, checking the row count after each.
  3. Print the intermediate values: SELECT name, department, salary, department = 'sales' AS is_sales ....
  4. Read the query in execution order: FROM, WHERE, GROUP BY, HAVING, SELECT (Order of Evaluation).
lightbulb
A habit that saves jobs
Before a data change, write the SELECT with the same WHERE and read the rows. If they are right, change SELECT * to DELETE or UPDATE.