07 - Advanced SQL: Subqueries, Set Operations & Views
2062 words
10 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# 07 - Advanced SQL: Subqueries, Set Operations & Views ## 🎯 Learning Objectives After reading this topic, you will be able to: - Write nested subqueries using IN, ANY, ALL, EXISTS, UNIQUE - Use set operations UNION, INTERSECT, and EXCEPT (with/without ALL) - Create and use views (virtual tables) - Write WITH (CTE)...

07 - Advanced SQL: Subqueries, Set Operations & Views
🎯 Learning Objectives
After reading this topic, you will be able to:
- Write nested subqueries using IN, ANY, ALL, EXISTS, UNIQUE
- Use set operations UNION, INTERSECT, and EXCEPT (with/without ALL)
- Create and use views (virtual tables)
- Write WITH (CTE) queries for temporary view definitions
📋 Prerequisites
- 06 - SQL Aggregation — GROUP BY, HAVING, aggregate functions
- 05 - SQL Queries — Basic SELECT-FROM-WHERE
📖 Core Content
7.1 Intuition: Queries Within Queries
Sometimes you need the result of one query to answer another:
- "Find instructors whose salary is greater than the average" — you need the average first
- "Find departments that have at least one instructor earning > 90000"
- "Find courses that every student has taken" Subqueries (inner queries nested inside outer queries) solve these problems elegantly.
Why This Matters: Subqueries let you express complex conditions that would be impossible with a single flat SELECT. They're essential for real-world analytics.
7.2 Subqueries with IN
The
IN operator checks if a value is in a set returned by a subquery:sql-- Find instructors who teach in departments with budget > 100000 SELECT name FROM instructor WHERE dept_name IN ( SELECT dept_name FROM department WHERE budget > 100000 );
Execution: The inner query runs first, returning departments with budget > 100000. The outer query then finds instructors in those departments.
NOT IN
sql-- Find instructors NOT in departments with budget > 100000 SELECT name FROM instructor WHERE dept_name NOT IN ( SELECT dept_name FROM department WHERE budget > 100000 );
Warning: If the subquery result contains NULL,NOT INreturns no rows! (BecauseX NOT IN (NULL, 'CS', 'Math')evaluates to UNKNOWN — NULL comparison always yields UNKNOWN.)
7.3 Subqueries with ANY and ALL
ANY (some)
F <comp> ANY r is TRUE if the comparison holds for at least one tuple in r.sql-- Find instructors whose salary is greater than SOME instructor in Biology SELECT name FROM instructor WHERE salary > SOME ( SELECT salary FROM instructor WHERE dept_name = 'Biology' );
Key equivalences:
= SOME≡IN<> SOME≠NOT IN(important distinction!)
ALL
F <comp> ALL r is TRUE if the comparison holds for every tuple in r.sql-- Find the instructor(s) with the highest salary SELECT name FROM instructor WHERE salary >= ALL ( SELECT salary FROM instructor );
Key equivalences:
<> ALL≡NOT IN= ALL≠IN(unless the subquery has only one distinct value)
7.4 Subqueries with EXISTS
EXISTS r returns TRUE if r is non-empty.sql-- Find departments that have at least one instructor SELECT dept_name FROM department D WHERE EXISTS ( SELECT * FROM instructor I WHERE I.dept_name = D.dept_name );
Correlated subquery: The inner query references the outer query's table (D.dept_name). For each department, the inner query checks if there's at least one matching instructor.
NOT EXISTS
sql-- Find departments with NO instructors SELECT dept_name FROM department D WHERE NOT EXISTS ( SELECT * FROM instructor I WHERE I.dept_name = D.dept_name );
EXISTS for Relational Division
"Find students who have taken ALL courses offered by the CS department":
sqlSELECT S.ID, S.name FROM student S WHERE NOT EXISTS ( -- Courses in CS that this student has NOT taken (SELECT course_id FROM course WHERE dept_name = 'Comp. Sci.') EXCEPT (SELECT course_id FROM takes WHERE takes.ID = S.ID) );
This is the SQL version of relational division (÷) — one of the most powerful query patterns.
7.5 Subqueries with UNIQUE
UNIQUE r returns TRUE if r has no duplicate tuples:sql-- Find courses that have only one section (no duplicates in offering) SELECT course_id FROM course C WHERE UNIQUE ( SELECT course_id FROM section S WHERE S.course_id = C.course_id );
7.6 Set Operations: UNION, INTERSECT, EXCEPT
Set operations combine results from two queries. Both queries must have the same number of columns with compatible data types.
(Diagram)
UNION
Rows that appear in either query result:
sql-- Courses taught in Fall 2017 or Spring 2018 (SELECT course_id FROM teaches WHERE semester = 'Fall' AND year = 2017) UNION (SELECT course_id FROM teaches WHERE semester = 'Spring' AND year = 2018);
INTERSECT
Rows that appear in both query results:
sql-- Courses taught in both Fall 2017 AND Spring 2018 (SELECT course_id FROM teaches WHERE semester = 'Fall' AND year = 2017) INTERSECT (SELECT course_id FROM teaches WHERE semester = 'Spring' AND year = 2018);
EXCEPT (also called MINUS)
Rows that appear in the first query but NOT the second:
sql-- Courses taught in Fall 2017 but NOT in Spring 2018 (SELECT course_id FROM teaches WHERE semester = 'Fall' AND year = 2017) EXCEPT (SELECT course_id FROM teaches WHERE semester = 'Spring' AND year = 2018);
ALL Variants
By default, set operations remove duplicates. To preserve duplicates:
| Operation | Duplicate Count |
|---|---|
UNION ALL | m + n duplicates |
INTERSECT ALL | min(m, n) duplicates |
EXCEPT ALL | max(0, m - n) duplicates |
Where m = count in first result, n = count in second result.
7.7 WITH Clause (CTE — Common Table Expression)
The
WITH clause defines a temporary view that exists only for the duration of the query:sql-- Find all departments with above-average budget WITH dept_avg AS ( SELECT AVG(budget) AS avg_budget FROM department ) SELECT dept_name, budget FROM department, dept_avg WHERE department.budget > dept_avg.avg_budget;
Multiple CTEs
sql-- Find the department with the maximum total salary WITH dept_total(dept_name, total_salary) AS ( SELECT dept_name, SUM(salary) FROM instructor GROUP BY dept_name ), max_total(max_salary) AS ( SELECT MAX(total_salary) FROM dept_total ) SELECT dept_name, total_salary FROM dept_total, max_total WHERE dept_total.total_salary = max_total.max_salary;
7.8 Views
A view is a virtual table — it doesn't store data, just a query definition.
sqlCREATE VIEW faculty AS SELECT ID, name, dept_name FROM instructor;
Now you can query the view like a regular table:
sqlSELECT * FROM faculty WHERE dept_name = 'Comp. Sci.';
Why views matter:
| Purpose | Example |
|---|---|
| Security | Hide salary column from certain users |
| Simplicity | Provide a simplified interface to complex queries |
| Consistency | Changes to underlying tables automatically reflected |
Materialized Views
A materialized view stores the result physically:
sqlCREATE MATERIALIZED VIEW dept_salary_stats AS SELECT dept_name, COUNT(*), AVG(salary) FROM instructor GROUP BY dept_name;
- Pros: Fast access (no recomputation needed)
- Cons: Must be refreshed when underlying data changes
7.9 Worked Examples
Example 1: Find instructors with salary > average (correlated)
sqlSELECT name, salary FROM instructor I WHERE salary > ( SELECT AVG(salary) FROM instructor WHERE dept_name = I.dept_name -- correlated: same department );
This finds instructors earning above the average for their OWN department.
Example 2: Using WITH for readability
Without WITH:
sqlSELECT dept_name FROM instructor WHERE salary > (SELECT AVG(salary) FROM instructor) ORDER BY dept_name;
With WITH:
sqlWITH overall_avg AS ( SELECT AVG(salary) AS avg_sal FROM instructor ) SELECT dept_name FROM instructor, overall_avg WHERE salary > overall_avg.avg_sal ORDER BY dept_name;
📐 Key Formulas / Concepts
| Operator | Meaning | Key Equivalence |
|---|---|---|
IN | Value in set | = SOME |
NOT IN | Value not in set | <> ALL |
ANY/SOME | Comparison with any | > ANY = greater than smallest |
ALL | Comparison with all | > ALL = greater than largest |
EXISTS | Set is non-empty | EXISTS r ≡ r ≠ ∅ |
UNIQUE | No duplicates | UNIQUE r ≡ no dups in r |
⚠️ Common Pitfalls
Pitfall 1: NOT IN with NULL
The Mistake:
WHERE dept_name NOT IN (SELECT dept_name FROM department WHERE budget < 50000) — if any department has NULL budget, this returns no rows!
Why: X NOT IN (NULL, 'CS', 'Math') is equivalent to X <> NULL AND X <> 'CS' AND X <> 'Math'. X <> NULL is UNKNOWN, so the whole AND becomes UNKNOWN or FALSE.
Fix: Use NOT EXISTS instead, or filter NULLs explicitly.Pitfall 2: Confusing = ANY with = ALL
The Mistake: Using
= ALL when you mean = ANY.
Why: = ANY means "equal to at least one" (like IN). = ALL means "equal to every one" — which is almost never what you want unless all values are identical.
Fix: Think: ANY = "at least one", ALL = "for every one".Pitfall 3: Forgetting UNION removes duplicates
The Mistake: Using UNION when you need ALL results including duplicates.
Why: UNION adds the cost of duplicate elimination. If you know there are no duplicates, or if duplicates are acceptable, use UNION ALL (which is faster).
Fix: Use
UNION ALL when duplicates are fine and performance matters.📝 Practice Questions
Q1. Write a query using EXISTS to find departments that have at least one instructor.
AnswersqlSELECT dept_name FROM department D WHERE EXISTS ( SELECT 1 FROM instructor I WHERE I.dept_name = D.dept_name );SELECT 1is an optimization — we don't care about the actual data, just existence.
Q2. Find all courses taught in Fall 2017 but not in Spring 2018, using EXCEPT.
Answersql(SELECT course_id FROM teaches WHERE semester = 'Fall' AND year = 2017) EXCEPT (SELECT course_id FROM teaches WHERE semester = 'Spring' AND year = 2018);
Q3. What is the difference between UNION and UNION ALL?
Answer
- UNION: Removes duplicate tuples from the result (slower, but cleaner)
- UNION ALL: Preserves all duplicates (faster)
If a tuple appears m times in the first query and n times in the second:
- UNION: appears once
- UNION ALL: appears m + n times
Q4. Why does NOT IN fail with NULL values? How do you fix it?
AnswerX NOT IN (NULL, 'A', 'B')is equivalent toX <> NULL AND X <> 'A' AND X <> 'B'. SinceX <> NULLis always UNKNOWN (regardless of X), the entire expression becomes UNKNOWN, and the WHERE clause treats it as FALSE.Fix: UseNOT EXISTS:sqlSELECT name FROM instructor I WHERE NOT EXISTS ( SELECT 1 FROM department D WHERE D.dept_name = I.dept_name AND D.budget < 50000 );
Q5. Write a query using WITH to find departments with above-average number of instructors.
AnswersqlWITH dept_count AS ( SELECT dept_name, COUNT(*) AS num_instructors FROM instructor GROUP BY dept_name ), avg_count AS ( SELECT AVG(num_instructors) AS avg_instructors FROM dept_count ) SELECT dept_name, num_instructors FROM dept_count, avg_count WHERE dept_count.num_instructors > avg_count.avg_instructors;
Q6. What is a view in SQL? When would you use one?
AnswerA view is a virtual table defined by a query. It stores the query definition, not the data.Use when:
- Security: Hide sensitive columns (e.g., salary) from certain users
- Simplicity: Provide a simplified interface to complex joins
- Consistency: Ensure everyone uses the same query logic
Q7. Write a query to find students who have taken ALL courses offered by the CS department.
AnswersqlSELECT S.ID, S.name FROM student S WHERE NOT EXISTS ( (SELECT course_id FROM course WHERE dept_name = 'Comp. Sci.') EXCEPT (SELECT T.course_id FROM takes T WHERE T.ID = S.ID) );This uses double negation: find students for whom there does NOT exist a CS course they have NOT taken.
Q8. What is the execution order of a query with subqueries?
AnswerFor uncorrelated subqueries: The inner query runs first, produces a result, and the outer query uses that result.For correlated subqueries: For each tuple in the outer query, the inner query is re-evaluated using the current outer tuple's values. This is like a nested loop.Correlated subqueries can be slower because they run the inner query many times.
🔗 Cross-References
- Next Topic: 08 - SQL Joins
- Previous Topic: 06 - SQL Aggregation
- Related: BSMS2001 (BDM) — Complex business queries
- Textbook: Silberschatz, Korth, Sudarshan — Chapter 3 (SQL), Chapter 4 (Intermediate SQL) Join Discord Previous06 - SQL AggregationNext08 - SQL Joins