Queries inside queries

Remember that the result of a query is itself a little table? That means you can put a query inside another one. The inner one is called a subquery, and it lives in brackets.

Say you want every song longer than average. You know how to get the average:

SELECT AVG(seconds) FROM songs;

You can't write WHERE seconds > AVG(seconds), because aggregates aren't allowed in WHERE (it runs row by row, remember). But you can drop the whole query in:

SELECT title, seconds
FROM songs
WHERE seconds > (SELECT AVG(seconds) FROM songs);

The database runs the inner query first, gets one number, then uses it in the outer query as if you had typed it in. The best part: if you add more songs, the average updates by itself. Hard-coding 212.3 would go stale.

A subquery that returns a list pairs nicely with IN:

SELECT title FROM songs
WHERE artist_id IN (SELECT id FROM artists WHERE country = 'Japan');

The inner query makes a list of ids; the outer one keeps songs whose artist_id is in it. Flip it to NOT IN to find the opposite. One warning there: if the list contains even a single NULL, NOT IN matches nothing at all, the same NULL weirdness as before. Filtering NULLs out inside the subquery fixes it.

Many subqueries can also be written as joins, and neither is "the right one". Pick the version you can read out loud and understand. Subqueries are often easiest when the question is in two steps: first work out this, then use it.

Tip: write and run the inner query on its own first. If that part is right, the outer part is usually easy.

Tables in this lesson
artists 6 rows
idnamecountry
1Lofi OwlJapan
2GravelUK
3Kai NeonUSA
4Pixel ChoirJapan
5Velvet StaticIreland
6The ToastersUK
songs 10 rows
idtitleartist_idsecondsplays
1Homework Can Wait115212000
2Thunder Lunchbox2254870
3Love You Like WiFi32319100
4The Final Boss41763300
5Monday Again21892100
6Rainy Bus Window1245640
7Glow Up316615000
8The Long Goodbye5302450
9Skate Park Sunset52101800
10Lovesick Robot61985400
Show the SQL that built them
CREATE TABLE artists (id INTEGER PRIMARY KEY, name TEXT, country TEXT);
INSERT INTO artists VALUES
 (1, 'Lofi Owl', 'Japan'),
 (2, 'Gravel', 'UK'),
 (3, 'Kai Neon', 'USA'),
 (4, 'Pixel Choir', 'Japan'),
 (5, 'Velvet Static', 'Ireland'),
 (6, 'The Toasters', 'UK');
CREATE TABLE songs (id INTEGER PRIMARY KEY, title TEXT, artist_id INTEGER, seconds INTEGER, plays INTEGER);
INSERT INTO songs VALUES
 (1, 'Homework Can Wait', 1, 152, 12000),
 (2, 'Thunder Lunchbox', 2, 254, 870),
 (3, 'Love You Like WiFi', 3, 231, 9100),
 (4, 'The Final Boss', 4, 176, 3300),
 (5, 'Monday Again', 2, 189, 2100),
 (6, 'Rainy Bus Window', 1, 245, 640),
 (7, 'Glow Up', 3, 166, 15000),
 (8, 'The Long Goodbye', 5, 302, 450),
 (9, 'Skate Park Sunset', 5, 210, 1800),
 (10, 'Lovesick Robot', 6, 198, 5400);

Try it yourself

Edit it. Break it. Run it again.
-- Songs longer than the average song
SELECT title, seconds
FROM songs
WHERE seconds > (SELECT AVG(seconds) FROM songs);
Ctrl/Cmd + Enter runs. Esc, then Tab, leaves the editor.

Your turn

Type it yourself. That is the whole trick.

1Crowd favourites

Return the title and plays of every song with more plays than the average number of plays. Work the average out with a subquery, don't type the number in.

SELECT title, plays
FROM songs
WHERE plays > 1000;
Ctrl/Cmd + Enter runs. Esc, then Tab, leaves the editor.

2UK sounds

Return the title of every song by an artist from the UK, using a subquery with IN.

SELECT title
FROM songs
WHERE artist_id IN ();
Ctrl/Cmd + Enter runs. Esc, then Tab, leaves the editor.

Code editor. Press Control or Command plus Enter to run the code. Tab indents; to move focus out of the editor, press Escape and then Tab.