Skip to content

MySQL Tutorial SQL Practice and Interview Preparation

Practice 4: Find the Bug in the Query

Lesson 304 of 308 4 min read By Deepak Paswan Updated Oct 5, 2026

Most SQL bugs are not syntax errors. The query runs and returns wrong results, and nobody notices. This lesson shows ten classic bugs, with the real output of the wrong query, the explanation, and the fix. Try to spot the problem before you read the explanation.

query.sql
SQL
CREATE TABLE people (id INT PRIMARY KEY, name VARCHAR(20) NOT NULL, phone VARCHAR(15) NULL, city VARCHAR(20) NULL, age INT);
INSERT INTO people VALUES (1, 'Maya', '9000000001', 'Pune', 31), (2, 'Ben', NULL, 'Delhi', 28), (3, 'Isha', '9000000003', NULL, 31), (4, 'Kabir', NULL, NULL, NULL), (5, 'Zoya', '9000000005', 'Pune', 25);

CREATE TABLE orders (id INT PRIMARY KEY, person_id INT NOT NULL, total DECIMAL(8,2) NOT NULL, placed_on DATETIME NOT NULL);
INSERT INTO orders VALUES (1, 1, 100.00, '2026-03-30 10:00:00'), (2, 1, 50.00, '2026-03-31 18:30:00'), (3, 2, 75.00, '2026-03-15 09:00:00'), (4, 5, 20.00, '2026-04-01 08:00:00');
Output
OUTPUT
Query OK, 0 rows affected

Query OK, 5 rows affected
Records: 5  Duplicates: 0  Warnings: 0

Query OK, 0 rows affected

Query OK, 4 rows affected
Records: 4  Duplicates: 0  Warnings: 0

Bug 1: comparing with NULL

“List the people with no phone.”

query.sql
SQL
SELECT name FROM people WHERE phone = NULL;
Output
OUTPUT
Empty set

Nobody is returned, although Ben and Kabir have no phone. = NULL is never true, because a comparison with an unknown value is unknown. Use IS NULL (NULL):

query.sql
SQL
SELECT name FROM people WHERE phone IS NULL ORDER BY name;
Output
OUTPUT
+-------+
| name  |
+-------+
| Ben   |
| Kabir |
+-------+
2 rows in set

Bug 2: COUNT(*) is not COUNT(column)

“How many people have a city?”

query.sql
SQL
SELECT COUNT(*) AS wrong_count, COUNT(city) AS people_with_city FROM people;
Output
OUTPUT
+-------------+------------------+
| wrong_count | people_with_city |
+-------------+------------------+
|           5 |                3 |
+-------------+------------------+
1 row in set

COUNT(*) counts rows (5). COUNT(city) counts only rows where city is not NULL (3). Pick the one that answers the question (Aggregates).

Bug 3: NOT IN and NULL

“Which people have never referred anyone?” A referrals table stores who referred whom. Some referrals have no known referrer (NULL):

query.sql
SQL
CREATE TABLE referrals (id INT PRIMARY KEY, referrer_id INT NULL, referred_name VARCHAR(20) NOT NULL);
INSERT INTO referrals VALUES (1, 1, 'Zoya'), (2, NULL, 'Walk-in customer');

SELECT name FROM people WHERE id NOT IN (SELECT referrer_id FROM referrals);
Output
OUTPUT
Empty set

The result is empty, but people 2 to 5 have never referred anyone. The subquery returns 1, NULL, and id NOT IN (1, NULL) is never true: “not equal to NULL” is unknown. NOT IN with a subquery that can return NULL returns nothing. Use NOT EXISTS, or remove the NULLs (Subqueries):

query.sql
SQL
SELECT p.name FROM people p WHERE NOT EXISTS (SELECT 1 FROM referrals r WHERE r.referrer_id = p.id) ORDER BY p.name;
Output
OUTPUT
+-------+
| name  |
+-------+
| Ben   |
| Isha  |
| Kabir |
| Zoya  |
+-------+
4 rows in set

Bug 4: an aggregate in WHERE

“People with more than one order.”

query.sql
SQL
SELECT person_id FROM orders WHERE COUNT(*) > 1 GROUP BY person_id;
Output
OUTPUT
ERROR 1111 (HY000): Invalid use of group function

WHERE runs before the grouping, so it cannot use COUNT(*). Conditions on aggregates go in HAVING:

query.sql
SQL
SELECT person_id, COUNT(*) AS orders FROM orders GROUP BY person_id HAVING COUNT(*) > 1;
Output
OUTPUT
+-----------+--------+
| person_id | orders |
+-----------+--------+
|         1 |      2 |
+-----------+--------+
1 row in set

Bug 5: a column that is not grouped

“Show the oldest person’s name per city.”

query.sql
SQL
SELECT city, name, MAX(age) FROM people GROUP BY city;
Output
OUTPUT
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'learn.people.name' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

MySQL 8 runs with ONLY_FULL_GROUP_BY: every selected column must be grouped or aggregated. name is neither, and which name would belong to the maximum age is undefined. (Old MySQL versions silently returned an arbitrary name.) The right answer needs a different approach, such as a window function (Top N per Group):

query.sql
SQL
SELECT city, name, age FROM (SELECT city, name, age, ROW_NUMBER() OVER (PARTITION BY city ORDER BY age DESC, name) AS r FROM people WHERE city IS NOT NULL) x WHERE r = 1 ORDER BY city;
Output
OUTPUT
+-------+------+------+
| city  | name | age  |
+-------+------+------+
| Delhi | Ben  |   28 |
| Pune  | Maya |   31 |
+-------+------+------+
2 rows in set

Bug 6: a WHERE that turns a LEFT JOIN into an inner join

“All people, and the number of their orders above 60.”

query.sql
SQL
SELECT p.name, COUNT(o.id) AS big_orders FROM people p LEFT JOIN orders o ON o.person_id = p.id WHERE o.total > 60 GROUP BY p.id, p.name ORDER BY p.name;
Output
OUTPUT
+------+------------+
| name | big_orders |
+------+------------+
| Ben  |          1 |
| Maya |          1 |
+------+------------+
2 rows in set

Isha, Kabir and Zoya are missing. For them, o.total is NULL (no match), and NULL > 60 is not true, so WHERE removes the rows that the LEFT JOIN had kept. Move the condition into ON:

query.sql
SQL
SELECT p.name, COUNT(o.id) AS big_orders FROM people p LEFT JOIN orders o ON o.person_id = p.id AND o.total > 60 GROUP BY p.id, p.name ORDER BY p.name;
Output
OUTPUT
+-------+------------+
| name  | big_orders |
+-------+------------+
| Ben   |          1 |
| Isha  |          0 |
| Kabir |          0 |
| Maya  |          1 |
| Zoya  |          0 |
+-------+------------+
5 rows in set

Bug 7: joins that multiply rows

“Total of each person’s orders, with their phone and a label.” Joining a second one-to-many table repeats the rows:

query.sql
SQL
CREATE TABLE tags (person_id INT NOT NULL, tag VARCHAR(10) NOT NULL);
INSERT INTO tags VALUES (1, 'vip'), (1, 'beta'), (1, 'early');

SELECT p.name, SUM(o.total) AS wrong_total
FROM people p JOIN orders o ON o.person_id = p.id JOIN tags t ON t.person_id = p.id
GROUP BY p.id, p.name;
Output
OUTPUT
+------+-------------+
| name | wrong_total |
+------+-------------+
| Maya |      450.00 |
+------+-------------+
1 row in set

Maya’s orders total 150, but the result says 450: each of her 2 orders appears once per tag (3 tags), tripling the sum. Aggregate each side before joining, or use a subquery:

query.sql
SQL
SELECT p.name, (SELECT SUM(o.total) FROM orders o WHERE o.person_id = p.id) AS total, (SELECT GROUP_CONCAT(t.tag ORDER BY t.tag) FROM tags t WHERE t.person_id = p.id) AS tags
FROM people p WHERE p.id = 1;
Output
OUTPUT
+------+--------+----------------+
| name | total  | tags           |
+------+--------+----------------+
| Maya | 150.00 | beta,early,vip |
+------+--------+----------------+
1 row in set

Bug 8: numbers stored as text

query.sql
SQL
CREATE TABLE versions (v VARCHAR(10));
INSERT INTO versions VALUES ('9'), ('10'), ('100'), ('2');
SELECT v FROM versions ORDER BY v;
Output
OUTPUT
+------+
| v    |
+------+
| 10   |
| 100  |
| 2    |
| 9    |
+------+
4 rows in set

As text, '10' sorts before '2' (character by character). If the values are numbers, store them as numbers. If you cannot change the column, convert in the query:

query.sql
SQL
SELECT v FROM versions ORDER BY CAST(v AS UNSIGNED);
Output
OUTPUT
+------+
| v    |
+------+
| 2    |
| 9    |
| 10   |
| 100  |
+------+
4 rows in set

Bug 9: BETWEEN and a DATETIME

“Orders placed in March.”

query.sql
SQL
SELECT id, placed_on FROM orders WHERE placed_on BETWEEN '2026-03-01' AND '2026-03-31' ORDER BY id;
Output
OUTPUT
+----+---------------------+
| id | placed_on           |
+----+---------------------+
|  1 | 2026-03-30 10:00:00 |
|  3 | 2026-03-15 09:00:00 |
+----+---------------------+
2 rows in set

Order 2 (31 March at 18:30) is missing. '2026-03-31' means midnight at the start of that day, so everything later on the 31st is outside the range. Use a half-open range:

query.sql
SQL
SELECT id, placed_on FROM orders WHERE placed_on >= '2026-03-01' AND placed_on < '2026-04-01' ORDER BY id;
Output
OUTPUT
+----+---------------------+
| id | placed_on           |
+----+---------------------+
|  1 | 2026-03-30 10:00:00 |
|  2 | 2026-03-31 18:30:00 |
|  3 | 2026-03-15 09:00:00 |
+----+---------------------+
3 rows in set

Bug 10: case-insensitive comparisons

query.sql
SQL
CREATE TABLE logins (username VARCHAR(20) NOT NULL, UNIQUE KEY uq_username (username));
INSERT INTO logins VALUES ('Maya');
INSERT INTO logins VALUES ('maya');
Output
OUTPUT
ERROR 1062 (23000): Duplicate entry 'maya' for key 'logins.uq_username'

The default collation utf8mb4_0900_ai_ci is case-insensitive and accent-insensitive, so 'Maya' and 'maya' are the same value, and the unique index refuses the second. That is often wanted for usernames and emails. If you need a case-sensitive comparison for one query, say so (Character Sets and Collations):

query.sql
SQL
SELECT 'Maya' = 'maya' AS ci_equal, 'Maya' COLLATE utf8mb4_0900_as_cs = 'maya' AS cs_equal;
Output
OUTPUT
+----------+----------+
| ci_equal | cs_equal |
+----------+----------+
|        1 |        0 |
+----------+----------+
1 row in set

A debugging routine

  1. Read the error message to the end. It names the line and the problem.
  2. Run the pieces: the subquery alone, the join without the WHERE, the GROUP BY without the HAVING.
  3. Check the row count after every join. If it grew unexpectedly, a join multiplies rows.
  4. Look for NULL: in comparisons, NOT IN, joins and aggregates.
  5. Use EXPLAIN when the result is right but slow (EXPLAIN).
  6. Keep a tiny test table with the awkward values (NULLs, ties, empty groups, boundary dates), and run every important query on it.
lightbulb
Test the edges
Put a NULL, a duplicate, a tie, a row at midnight and a customer with no orders into your test data. Most wrong queries fail on exactly those rows.