Congrats, we’ve reached the point where things get really interesting! Today we’ll see how to mix different types of subqueries to tackle complex problems. EXISTS, IN, HAVING—that’s the trio that’ll make you feel like a database wizard. We’ll pull data from one table, filter it using another, group it, and then filter those groups. And as a bonus, we’ll check out some tricks to make your queries more efficient.
Let’s start by setting up a general problem that we’ll solve step by step during this lecture.
Problem Statement
Let’s say we have a university database with three tables:
Table students
| id | name | group_id |
|---|---|---|
| 1 | Otto | 101 |
| 2 | Maria | 101 |
| 3 | Alex | 102 |
| 4 | Anna | 103 |
Table courses
| id | name |
|---|---|
| 1 | Mathematics |
| 2 | Programming |
| 3 | Philosophy |
Table enrollments
| student_id | course_id | grade |
|---|---|---|
| 1 | 1 | 90 |
| 1 | 2 | NULL |
| 2 | 1 | 85 |
| 3 | 3 | 70 |
We need to select all students who:
- Are enrolled in at least one course
EXISTS. - Don’t have a grade for at least one of their enrolled courses
IN. - Belong to groups where the average grade is above 80
HAVING.
Solution Using EXISTS and IN
Step 1: Check enrolled students (EXISTS). Let’s start with the simplest condition. We need to figure out which students are enrolled in at least one course. For that, we can use EXISTS.
SELECT name
FROM students s
WHERE EXISTS (
SELECT 1
FROM enrollments e
WHERE e.student_id = s.id
);
- The outer query selects names from the
studentstable. - In the subquery, we check if there are any records in the
enrollmentstable that match the student from the outer query (WHERE e.student_id = s.id). SELECT 1is used to show that we only care about the existence of records, not their content.
Result:
| name |
|---|
| Otto |
| Maria |
| Alex |
Now we know who’s enrolled in courses. But we want more. We want to filter them by missing grades.
Step 2: Check for missing grades (IN + NULL). Now let’s add a filter: we only want students who are missing a grade for at least one course. Here, IN and knowing how to work with NULL will help.
SELECT name
FROM students s
WHERE id IN (
SELECT e.student_id
FROM enrollments e
WHERE e.grade IS NULL
);
- The outer query selects student names.
- The subquery builds a list of
student_idfrom theenrollmentstable wheregrade IS NULL.
Result:
| name |
|---|
| Otto |
So, Otto is the only student with a course that’s missing a grade. How dramatic! But we’re not done yet: we need to only consider groups where the average grade is above 80.
Solution Using HAVING
Step 3: Grouping and filtering with HAVING.
Now it’s time to put it all together. We need to:
- Calculate the average grade for each group.
- Filter groups where the average grade is above 80.
- Show students from those groups, considering the previous conditions.
SELECT name
FROM students s
WHERE s.group_id IN (
SELECT group_id
FROM students
JOIN enrollments ON students.id = enrollments.student_id
WHERE grade IS NOT NULL
GROUP BY group_id
HAVING AVG(grade) > 80
)
AND id IN (
SELECT e.student_id
FROM enrollments e
WHERE e.grade IS NULL
);
- The outer query selects names of students who meet all the conditions.
- The first subquery in
WHEREreturns a list ofgroup_idfor groups with an average grade above 80.- We join
studentswithenrollmentsto get the grades. - We filter only records where
grade IS NOT NULL. - We group the data by
group_id. - We use
HAVINGto filter the groups.
- We join
- The second subquery in
WHEREchecks that the student has at least one course with a missing grade. - Both parts of the condition are combined with
AND.
Result:
| name |
|---|
| Otto |
So, we found out that Otto is not only the only student with a missing grade, but also belongs to a group that’s killing it.
Comparing Approaches: EXISTS vs IN
EXISTS works best when you just want to quickly check if records exist. It’s efficient because it stops searching as soon as it finds the first record. That’s especially important for big tables.
At the same time, IN is useful when you care about the actual data. For example, if you’re outputting a list of IDs for further filtering. But keep in mind, IN can get slow if your subquery returns a ton of values.
When to use HAVING
For aggregated data, where you need to filter based on results, HAVING is your best friend. But if you can move the condition to WHERE (like filtering by a column), that’ll make your query simpler and faster.
Full Example
To lock it in, let’s break down another example: select groups where at least one student has a grade below 75, but at the same time isn’t enrolled in the “Philosophy” course.
Just to remind you, here are our tables:
Table students
| id | name | group_id |
|---|---|---|
| 1 | Otto | 101 |
| 2 | Maria | 101 |
| 3 | Alex | 102 |
| 4 | Anna | 103 |
Table courses
| id | name |
|---|---|
| 1 | Mathematics |
| 2 | Programming |
| 3 | Philosophy |
Table enrollments
| student_id | course_id | grade |
|---|---|---|
| 1 | 1 | 90 |
| 1 | 2 | NULL |
| 2 | 1 | 85 |
| 2 | 2 | 70 |
| 3 | 3 | 70 |
SELECT DISTINCT group_id
FROM students s
WHERE group_id IN (
SELECT s.group_id
FROM students s
JOIN enrollments e ON s.id = e.student_id
WHERE e.grade < 75
)
AND group_id NOT IN (
SELECT s.group_id -- nested query level 1
FROM students s
JOIN enrollments e ON s.id = e.student_id
WHERE e.course_id = (
SELECT id FROM courses WHERE name = 'Philosophy' -- nested query level 2 :P
)
);
- The first subquery selects groups where there’s at least one student with a grade below 75.
- The second subquery excludes groups linked to the “Philosophy” course.
- We combine the conditions using
INandNOT INto get the final result.
Result:
| group_id |
|---|
| 101 |
How Useful Is This?
In real life, these approaches save the day when you have to analyze complex data relationships. For example:
- In analytics, to highlight “special” client groups (VIP, problematic, etc.).
- In building recommendation systems, where you filter users by a bunch of criteria.
- In interviews, when you’re asked to optimize a gnarly SQL query.
Practice! That’s your path to mastery.
GO TO FULL VERSION