Level 14: Queries inside queries: subqueries
Updated on
Pro · level 14 of 20. Goal: Use the result of a query inside another one.
Topics: scalar subquery IN (SELECT …) NOT IN subquery in FROM
Lesson
Sometimes a question contains another one. “Which movies have a rating above the average?” first requires computing the average, then comparing each movie with it. A subquery answers the first question inside the second: it is a complete SELECT written between parentheses inside another query.
Why not write WHERE rating > AVG(rating)? Because WHERE works row by row, before any aggregate is calculated. The subquery (SELECT AVG(rating) FROM movies) is an independent query, calculated separately: its value (7.2) is then used like a plain number.
A scalar subquery returns a single value, one row and one column: you compare it with =, < or >. It really must return a single row: in standard SQL, several rows cause an error, and SQLite silently takes the first one.
A subquery that returns a column of several values is used with IN: WHERE author_id IN (SELECT id FROM authors WHERE country = 'Sweden'). NOT IN calls for caution: if the subquery returns a single NULL, NOT IN no longer returns any row (see level 12). Add WHERE … IS NOT NULL in the subquery, or use NOT EXISTS (level 15).
Finally, a subquery can replace a table in FROM: you then query its result like a temporary table, which must be given an alias. FROM (SELECT seller, SUM(amount) AS total FROM sales GROUP BY seller) AS t lets you, for example, compute the average of the totals per seller.
Reading tip: always start with the subquery. Run it on its own to see what it returns, then read the main query.
Syntax
SELECT …
FROM t
WHERE x > (SELECT AVG(x) FROM t) AND id IN (SELECT t_id FROM u);Worked example
SELECT title, rating
FROM movies
WHERE rating > (SELECT AVG(rating) FROM movies);The subquery computes the average rating (7.2); the main query keeps the movies above it.
| id | title | genre | year | duration | rating | director_id |
|---|---|---|---|---|---|---|
| 1 | Night Train | Thriller | 2015 | 118 | 7.8 | 1 |
| 2 | Blue Harbor | Drama | 2018 | 102 | 7.1 | 2 |
| 3 | Paper Moon City | Comedy | 2012 | 95 | 6.4 | 5 |
| 4 | Silent Peak | Drama | 2020 | 131 | 8.2 | 3 |
| 5 | Last Signal | Sci-Fi | 2019 | 142 | 7.5 | 1 |
| 6 | Summer Keys | Comedy | 2016 | 88 | 5.9 | 4 |
| 7 | Iron Garden | Sci-Fi | 2021 | 125 | 8 | 3 |
| 8 | Dust and Gold | Western | 2014 | 110 | 6.8 | 2 |
| 9 | The Quiet Hour | Drama | 2022 | 97 | 7.4 | 5 |
| 10 | Deep Current | Thriller | 2017 | 105 | 6.9 | NULL |
Example result
| title | rating |
|---|---|
| Night Train | 7.8 |
| Silent Peak | 8.2 |
| Last Signal | 7.5 |
| Iron Garden | 8 |
| The Quiet Hour | 7.4 |
Key points
- The subquery goes between parentheses.
- Scalar: one value, compared with = < >. Column: used with IN.
- In FROM, a subquery behaves like a temporary table.
Common pitfalls
- WHERE salary > AVG(salary) is not allowed: you need a subquery.
- NOT IN with a subquery that contains a NULL returns nothing.
Going further
Standard SQL also offers x > ALL (subquery) (“greater than all the values”) and x > ANY (subquery) (“greater than at least one”). SQLite does not know them: you write x > (SELECT MAX(…) …) and x > (SELECT MIN(…) …). One detail differs: on an empty subquery, > ALL is true, whereas the comparison with the maximum gives NULL.
IN or EXISTS? To keep the rows that have a match, both give the same result. For the opposite, prefer NOT EXISTS to NOT IN as soon as the column can contain NULL (exercise 14.4).
The level’s 5 exercises
- Show the name and salary of the employees who earn more than the average salary.
- Show the title of the books written by Swedish or German authors.
- Show the id, type and price of the most expensive room.
- Show the name of the directors who have directed no movie. Careful: one movie of the table has no known director.
- What is the average of the sales totals per seller? (First compute each seller's total, then the average of those totals.)
In SpeedQL, every query is checked straight away: its result is compared with the expected one, then the query is run again on a hidden control database. Each exercise has written hints, to show only if you get stuck.
See also: the cheat sheet card · the SQL exercises with solutions on this topic
Do the level 14 exercises
Free, no sign-up: the lesson and the 5 exercises open right in your browser.
← Previous level: Combining results: UNION, INTERSECT, EXCEPT · Next level: EXISTS and correlated subqueries →