Database Design & Development · Implementation

DDD8 — SQL: aggregate functions, GROUP BY, computed values, aliases

📅 To be scheduled
~50–60 minutes · single period
💻 DataGrip · standard SQL
Learning intentions
Success criteria
Warm up — what do you already know?

Answer before the lesson begins — it's fine if you're unsure.

WU1
1. In DDD7, which SQL clause sorts the final results?
WU2
2. In DDD6's Example 3 query design, why was grouping needed?
WU3
3. Which wildcard symbol matches any number of characters, used with LIKE?

Key vocabulary

Aggregate function
A function that calculates a single summary value from a set of records: MIN, MAX, AVG, SUM, COUNT.
GROUP BY
The clause that splits records into categories so an aggregate function calculates separately per category.
Computed value
A new value produced by an aggregate function or an arithmetic expression, appearing as its own column in the results.
Alias
A readable, custom name given to a field or computed value using AS, so results are clearly labelled.
Two-query pattern
Solving a "compare against an aggregate" problem using two separate, named queries, since single-statement subqueries are out of scope.

Summarising data with SQL

The five aggregate functions

An aggregate function calculates a single summary value from a whole set of records: MIN (smallest), MAX (largest), AVG (average), SUM (total), and COUNT (number of records). These five are the only aggregate functions in the Higher specification. ROUND, covered later in this lesson, is a genuinely useful related function — but it isn't itself one of the five aggregate functions, and shouldn't be listed as a sixth if you're ever asked to name them.

Mixing aggregate and non-aggregate fields — the GROUP BY rule

A query cannot freely mix an aggregate field (like COUNT(classCode)) with a non-aggregate field (like firstName) in the same SELECT unless GROUP BY is used. Without grouping, SQL has no way to decide which single firstName value should sit alongside a count calculated across many rows — GROUP BY resolves this by collecting rows into one group per distinct value of the grouped field, so the aggregate calculates once per group instead of once overall.

Computed values

A computed value is any result produced by processing existing data, rather than a value stored directly — this includes aggregate functions, but also simple arithmetic on ordinary fields, like calculating a total cost from feePerSession * numberOfSessions. A computed value becomes its own column in the results table, exactly as if it had been stored, even though it wasn't.

Aliases: AS [Readable Name]

Without an alias, a computed value's column heading is the expression itself (e.g. AVG(feePerSession)), which is technically correct but not reader-friendly. AS [Readable Name] gives it a proper label — this project's house style always uses square brackets, e.g. AS [Average Fee], kept consistent across every query from here on.

Clause order: GROUP BY before ORDER BY

SQL's clause order is fixed: SELECT, FROM, WHERE, GROUP BY, then ORDER BY last. Reversing GROUP BY and ORDER BY is a genuine syntax error, not just a style issue — a useful memory aid is GO: Group by, then Order by, always in that order.

Comparing against an aggregate: the two-query pattern

A single SQL statement cannot nest one query's aggregate result directly inside another query's WHERE clause at Higher level — subqueries are explicitly outside the specification. Instead, "compare against an aggregate" problems (like "which class has the highest fee?") are solved as two separate, named queries: Query 1 calculates the single aggregate value; Query 2's WHERE clause then uses that value directly, once you know what it is.

Worked examples

Example 1 — A simple aggregate with alias and ROUND
1
Requirement: "What is the average fee per session across all classes, rounded to 2 decimal places?"
2
One overall value, no grouping needed: AVG(feePerSession), wrapped in ROUND(..., 2).
Give it a readable alias. See the statement below.
SELECT ROUND(AVG(feePerSession), 2) AS [Average Fee]
FROM Class;
Example 2 — DDD6's per-instructor count, in SQL
1
Non-aggregate fields (firstName, surname) mixed with an aggregate (COUNT(classCode)) — GROUP BY is required.
2
Join Instructor and Class first, then group by instructorRef — every instructor's classes are counted separately.
Sort by the alias, most classes first — GROUP BY comes before ORDER BY. See the statement below.
SELECT Instructor.firstName, Instructor.surname, COUNT(Class.classCode) AS [Number Of Classes]
FROM Instructor, Class
WHERE Instructor.instructorRef = Class.instructorRef
GROUP BY Instructor.instructorRef
ORDER BY [Number Of Classes] DESC;
Example 3 — The two-query pattern: which class has the highest fee?
1
Query 1 finds the single highest fee value across all classes.
2
Run Query 1, read off its result (say, £12.50).
Query 2 uses that value directly in its own WHERE clause to find the matching class(es).
-- Query 1
SELECT MAX(feePerSession) AS [Highest Fee]
FROM Class;

-- Query 2 (using Query 1's result, e.g. 12.50)
SELECT className, feePerSession
FROM Class
WHERE feePerSession = 12.50;
Now you try

Write a SQL statement to find the total number of bookings ever made, with the alias [Total Bookings].

SELECT COUNT(bookingRef) AS [Total Bookings]
FROM Booking;
⚠️ Common mistakes — examiner feedback
📝 Exam tip

When a question describes a "compare against an aggregate" problem, write it explicitly as two labelled queries (Query 1, Query 2) rather than attempting a single nested statement — this is SQA's own examinable method, and a single-statement subquery answer may not gain full marks even if it technically works in some SQL products.

Task Set B — Core questions
Work through all questions. Written and code answers use self-assessment — compare with the model answer.
B1
Name the five aggregate functions (comma-separated).
B2
Which clause must be used whenever a query mixes an aggregate field with a non-aggregate field?
Write a SQL statement to find the total combined fee-per-session value across all classes, aliased as [Total Fees].
SELECT SUM(feePerSession) AS [Total Fees]
FROM Class;
Write a SQL statement to find the earliest (minimum) dateJoined value in Member, aliased as [Earliest Join Date].
SELECT MIN(dateJoined) AS [Earliest Join Date]
FROM Member;
B5
Read and explain this SQL statement: SELECT Class.className, COUNT(Booking.bookingRef) AS [Number Of Bookings] FROM Class, Booking WHERE Class.classCode = Booking.classCode GROUP BY Class.classCode;
Model answer
Write the two-query pattern to find which member has made the most bookings: Query 1 finds the highest booking count per member, and Query 2 finds which member(s) match it. Assume Query 1 returns 5.
-- Query 1: find the highest per-member booking count
SELECT MAX(bookingCount) AS [Most Bookings]
FROM (
  SELECT COUNT(bookingRef) AS bookingCount
  FROM Booking
  GROUP BY memberID
);
-- (or, per the two-query pattern taught this lesson, run the
-- GROUP BY query first, then read off the highest count manually: 5)

-- Query 2: find which member(s) made exactly that many bookings
SELECT Member.firstName, Member.surname, COUNT(Booking.bookingRef) AS [Number Of Bookings]
FROM Member, Booking
WHERE Member.memberID = Booking.memberID
GROUP BY Member.memberID
HAVING COUNT(Booking.bookingRef) = 5;
B7
Why is a single nested subquery not the expected method for "compare against an aggregate" problems at Higher?
B8
What does the "O" stand for in the GO mnemonic for clause order?
B9
Explain what a computed value is, and give one example that is an aggregate function and one example that is not.
Model answer
Task Set C — Extension · Beyond the specification
Optional challenge questions. Not required for your exam.
C1
Explain why ROUND is genuinely useful alongside aggregate functions, but is still correctly excluded from the list of five aggregate functions.
One possible answer
C2
In Example 2, the query grouped by Instructor.instructorRef rather than Instructor.surname. Would grouping by surname instead have worked correctly? Explain the risk.
One possible answer
C3
What is a genuine practical limitation of the two-query pattern, compared to a real single-statement subquery (even though subqueries aren't examinable at Higher)?
One possible answer

📁 File this in OneNote under:
Higher Computing Science → Database Design & Development → DDD8

📌 Teacher notes — Shift+T to hide

Suggested timing: 8 min warm up · 15 min notes (aggregates, GROUP BY, aliases, GO mnemonic) · 15 min worked examples, ideally live in DataGrip · 5 min now you try · 20 min Task Set B · 5–10 min Task Set C / review, if time allows.

Key misconception: pupils very commonly try to write ORDER BY before GROUP BY, or forget GROUP BY entirely when mixing aggregate and non-aggregate fields. The GO mnemonic (from Chris Meechan's materials) is worth repeating verbally every time a GROUP BY query is demonstrated.

Spec-accuracy note: do not let ROUND slip into an auto-checked "name the aggregate functions" answer — B1's expected answer is exactly MIN, MAX, AVG, SUM, COUNT, per the confirmed spec reading in ddd/DDD.md §1 and §9 item 3.

B6's model answer note: the "Query 1" shown for B6 demonstrates what a single nested subquery would look like, purely so pupils can see why it's excluded — the taught, examinable method is to run the inner GROUP BY query first, read off the highest count by eye, then hand-type that value into Query 2's HAVING clause, exactly as modelled in Example 3.

SQA command words covered: "describe", "exemplify", "use" (write working aggregate SQL), and "read and explain code" (B5).