Lecture 16: Aggregating and Joins
September 1, 2026
๐ So far we have looked at:
SELECT queriesMAX, MIN, COUNT)๐ Today we will look more closely at:
COUNT, SUM, AVERAGE, etc.We will also look at Joins, specifically:
For this lecture you will need the usual database connection. Make sure you have it set up now:
Create an appropriate database object:
๐ As usual, if you do not have an appropriate file path pointing to the .sqlite file, R will populate an empty database object. If you wish to avoid working with file paths, place the .sqlite file in your current working directory.
All of our queries in previous lectures had something in common:
SELECT [attributes] FROM [table] WHERE [logicals]
A simple query retrieves information from a single table.
A complex query searches for information across multiple tables.
To retrieve information across multiple tables, we will need to use joins.
SUMAggregation functions collapse many rows into a single summary value. We have previously worked with COUNT, MIN and MAX.
SUM returns the total of a numeric column.AVGWe often want to find the average of a numeric column.
AVG returns the mean of a numeric column.What if we wanted to find the average track length for each album with AlbumId between 150 and 155?
AlbumId avg_minutes
1 1 4.000692
2 2 5.709367
3 3 4.767156
4 4 5.110956
5 5 4.901899
Directly using logicals we run into problems:
We can combine GROUP BY with WHERE clauses. However if we try just using WHERE we run into problems:
HAVINGWe must use HAVING to declare clauses on aggregates:
Suppose we wish to select AlbumId, alongside a column detailing the number of tracks on each album with AlbumId between 150 and 155.
WHERE and AggregationIt is important to understand the structure of SQL queries with aggregation.
WHERE is used when creating clauses applied on all data.WHERE cannot be combined with GROUP BY.GROUP BY combines all data corresponding to an appropriate clause.HAVING is used with queries on aggregate clauses.๐ All of these tools can be combined in the same query.
WHERE and AggregationSuppose we are interested in returning the AlbumId, and the Total_Price of all albums with MediaTypeId of 3 and AlbumId between 220 and 250:
AlbumId Total_Price
1 226 1.99
2 227 37.81
3 228 45.77
4 229 51.74
5 230 49.75
6 231 47.76
7 249 11.94
8 250 43.78
As soon as we add aggregation, we need to group the data appropriately. We can then use HAVING to filter on the grouped data.
Recall our simple queries followed the structure:
SELECT [attribute] FROM [table] WHERE [clause]
05:00
Try to construct the query returning the AlbumId, GenreId, and length of each album in minutes as Album_Length (use scaled Milliseconds to compute, and use aliasing) of all albums with ID between 100 and 200 from the relation Track.
GenreId of 3. AlbumId GenreId AlbumLength
1 151 3 78
2 153 3 76
3 155 3 75
4 162 3 73
5 174 3 72
6 149 3 70
7 141 3 68
Reading the query clause by clause:
WHERE GenreId = 3 filters to the right genre before grouping.GROUP BY AlbumId collapses tracks so SUM gives one length per album.HAVING AlbumId BETWEEN 100 AND 200 filters on the grouped result.ORDER BY AlbumLength DESC then LIMIT 7 return the longest albums first.Track).I will prove it to you:
Query tracks:
TrackId Name AlbumId
1 2375 By The Way 194
2 2376 Universally Speaking 194
3 2377 This Is The Place 194
4 2378 Dosed 194
5 2379 Don't Forget Me 194
6 2380 The Zephyr Song 194
7 2381 Can't Stop 194
8 2382 I Could Die For You 194
9 2383 Midnight 194
10 2384 Throw Away Your Television 194
Query albums:
๐ญ There has to be a better way!!

Track and Album info together, we follow the arrow on AlbumId.Recall the structure of relational databases from lectures 13-15:
AlbumId is the primary key in Album.AlbumId is the foreign key in Track corresponding to AlbumId in Album.JOIN on the keys! TrackId Name AlbumId Title
1 2375 By The Way 194 By The Way
2 2376 Universally Speaking 194 By The Way
3 2377 This Is The Place 194 By The Way
Inner join structure is as follows:
[Table 1] INNER JOIN [Table 2] ON [key1] = [key2]
Track.AlbumId and Album.AlbumId.Table.Attribute syntax is used when identically named attributes exist in multiple tables.05:00
Write a single query that does the following:
GenreAvg.๐ Hint 1: You will need to join the Track and Genre tables. Hint 2: You will need to group by GenreId.
JOIN can directly replace INNER JOIN.๐ Make sure you understand how aliases were used here โ this format will appear throughout assignments.
We can also join tables without specifying the key to join on.
The Cartesian product of two tables returns every combination of records in both tables.
Title ArtistId ArtistId Name
1 For Those About To Rock We Salute You 1 1 AC/DC
2 For Those About To Rock We Salute You 1 2 Accept
3 For Those About To Rock We Salute You 1 3 Aerosmith
CROSS JOIN.We can effectively construct an inner join using the Cartesian product:
WHERE clauses to select appropriate attributes.An implicit join constructs an inner join from the Cartesian product by adding a WHERE clause on the keys.
๐ Do not use this technique! Specify your joins explicitly.

Note that foreign key names often match primary key names:
AlbumId is foreign key in Track, primary key in Album.GenreId is foreign key in Track, primary key in Genre.ArtistId is foreign key in Album, primary key in Artist.When foreign and primary key names align, NATURAL JOIN can be used.
What is happening here?
Name appears in both Track and MediaType.Name corresponds to a different thing in each table.๐ If you use natural joins, you need to be extremely careful โ use explicit joins!
ArtistId and AlbumId values.Suppose we add a track to the database. Letโs add โCardiganโ:
We want to join this song to its album. Using an inner join gets us nowhere:
A left outer join solves the problem:
TrackId Name AlbumId Title
1 9995 Cardigan 999 <NA>
Track), and matches rows from the right table (Album) where possible.

๐ค This lecture covered aggregation and joins, including:
GROUP BY commandWHERE versus HAVING๐คฉ Next class we will look at: