00:00

MySQL EXISTS

Last updated:

EXISTS tests whether a sub-query returns at least one row. It answers yes/no questions about related data: which courses have students?, which students have no enrolments? Its opposite, NOT EXISTS, is the safest way to find rows that have no match.

1. Syntax

syntax.sql
SELECT ... FROM outer_table o
WHERE [NOT] EXISTS (SELECT 1 FROM inner_table i WHERE i.key = o.key);
  • The sub-query is usually correlated: it refers to the outer row (o.key).
  • Only whether a row exists matters, so SELECT 1, SELECT * or SELECT NULL all behave the same.
  • EXISTS is never NULL: it is TRUE or FALSE.
EXISTS checks each outer row for at least one matchFor each course, MySQL runs the inner query looking for an enrolment with that course id. Java and SQL have matches so EXISTS is true; Rust has none so it is false. With NOT EXISTS, only Rust is returned.courses (outer query)course_idtitle1Java2SQL3Rustenrolments (inner query)studentcourse_idIra1Joel2Kush1Java: match → TRUESQL: match → TRUERust: no row → FALSEEXISTS stops at the first match; it never needs the inner rows' values
Figure 1: EXISTS returns TRUE as soon as the sub-query finds one row for the current outer row.

2. Rows That Have a Match

query.sql
SELECT c.course_id, c.title
FROM courses c
WHERE EXISTS (SELECT 1 FROM enrolments e WHERE e.course_id = c.course_id);

Result: 3 rows

course_idtitle
1Java
2SQL
4COBOL

COBOL is included because Ira enrolled in it once. Add conditions to the sub-query to be more specific, for example only 2026 enrolments:

query.sql
SELECT c.title
FROM courses c
WHERE EXISTS (SELECT 1 FROM enrolments e
              WHERE e.course_id = c.course_id AND e.enrolled_on >= '2026-01-01');

Result: 2 rows

title
Java
SQL

3. Rows Without a Match: NOT EXISTS

query.sql
SELECT s.name
FROM students s
WHERE NOT EXISTS (SELECT 1 FROM enrolments e WHERE e.student_id = s.student_id);

Result: 1 row

name
Lina
query.sql
SELECT c.title
FROM courses c
WHERE c.active = TRUE
  AND NOT EXISTS (SELECT 1 FROM enrolments e WHERE e.course_id = c.course_id);

Result: 1 row

title
Rust

4. EXISTS Never Duplicates Rows

Kush has two enrolments. A JOIN returns him twice; EXISTS returns each student once:

query.sql
SELECT s.name FROM students s JOIN enrolments e ON e.student_id = s.student_id WHERE s.city = 'Pune';

JOIN: duplicates

name
Ira
Ira
Kush
Kush
query.sql
SELECT s.name FROM students s
WHERE s.city = 'Pune'
  AND EXISTS (SELECT 1 FROM enrolments e WHERE e.student_id = s.student_id);

EXISTS: one row each

name
Ira
Kush

5. NOT EXISTS vs NOT IN with NULLs

Suppose a leads table records where prospective students came from, and one walk-in lead has no student ID yet (NULL). Which students are not leads?

not-in-null.sql
CREATE TABLE leads (student_id INT NULL, source VARCHAR(10));
INSERT INTO leads VALUES (10, 'ads'), (NULL, 'walk-in');

SELECT name FROM students WHERE student_id NOT IN (SELECT student_id FROM leads);

NOT IN: nothing, because of the NULL

name
No rows
query.sql
SELECT name FROM students s
WHERE NOT EXISTS (SELECT 1 FROM leads l WHERE l.student_id = s.student_id);

NOT EXISTS: correct answer

name
Joel
Kush
Lina
Make NOT EXISTS your default for "rows without a match". It is NULL-safe and MySQL runs it as an efficient anti-join.

6. EXISTS vs IN vs JOIN

QuestionGood choiceWhy
Does a related row exist?EXISTS / INSemi-join: each outer row returned once
Rows with no related rowNOT EXISTS or LEFT JOIN … IS NULLNULL-safe anti-join
Need columns from the other tableJOINEXISTS can't return inner columns
Match against a short fixed listIN (…)Simplest to read

The LEFT JOIN form of the anti-join, for comparison:

query.sql
SELECT s.name FROM students s
LEFT JOIN enrolments e ON e.student_id = s.student_id
WHERE e.student_id IS NULL;

Result: 1 row

name
Lina

7. EXISTS in UPDATE and DELETE

update-exists.sql
UPDATE courses c
SET active = FALSE
WHERE NOT EXISTS (SELECT 1 FROM enrolments e
                  WHERE e.course_id = c.course_id AND e.enrolled_on >= '2026-01-01');
SELECT title, active FROM courses;

Result: 4 rows

titleactive
Java1
SQL1
Rust0
COBOL0
delete-exists.sql
DELETE FROM students
WHERE NOT EXISTS (SELECT 1 FROM enrolments e WHERE e.student_id = students.student_id);
SELECT name FROM students;

Result: 3 rows

name
Ira
Joel
Kush

8. Performance Tips

  • Index the column the sub-query filters on (here enrolments.course_id and student_id) so each check is a quick index lookup.
  • MySQL 8 converts most EXISTS and IN sub-queries into semi-joins and NOT EXISTS into anti-joins; check with EXPLAIN.
  • Put selective conditions inside the sub-query, so it finds or rules out a match quickly.

9. Practice Exercises

Exercise 1: Students who scored above 90 in any course.
solution.sql
SELECT name FROM students s
WHERE EXISTS (SELECT 1 FROM enrolments e WHERE e.student_id = s.student_id AND e.score > 90);

Result: 1 row

name
Kush
Exercise 2: Courses where no enrolment has a NULL score.
solution.sql
SELECT title FROM courses c
WHERE NOT EXISTS (SELECT 1 FROM enrolments e WHERE e.course_id = c.course_id AND e.score IS NULL);

Result: 3 rows

title
Java
SQL
Rust

10. Summary

  • EXISTS (sub-query) is TRUE if the sub-query returns any row; it is never NULL.
  • Correlated sub-queries refer to the outer row; the select list inside doesn't matter.
  • NOT EXISTS finds rows without a match and, unlike NOT IN, is safe with NULLs.
  • EXISTS returns each outer row once; use JOIN when you need the other table's columns.
  • Index the columns used inside the sub-query.