Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

alias
A short second name given to a table or column in a query, so the rest of the query can refer to it briefly.
Cartesian product
Every row of one table paired with every row of another, which happens when a join has no matching rule.
column
One named field in a table, with the same kind of value in every row, such as a title or a year.
constraint
A rule a table enforces on its data, such as requiring a value or making one column unique.
distinct
A word in a query that removes repeated rows from the result, so each one appears once.
foreign key
A column that refers to the primary key of another table, so each row can be tied to one row there.
index
A stored lookup that helps the database find rows quickly without reading the whole table.
inner join
The default way of combining two tables: only pairs of rows present on both sides appear in the result.
join
A query operation that combines rows from two tables by matching values they share, usually a shared identifier.
junction table
A bridge between two tables that lets a row on either side pair with many on the other; each of its rows holds one pair.
left join
A combination that keeps every row of the first table named, filling in nulls where the second has no match.
many-to-many
A relationship where one row on each side can pair with many rows on the other, usually held in a junction table.
null
A marker for a value that is missing or unknown, which is not the same as zero or an empty string.
outer join
A join that keeps rows from one or both tables even when no row on the other side matches.
primary key
The column or columns that together identify each row uniquely, and which can never be missing.
query
A request written in SQL that asks the database for an answer, usually a table of rows.
row
One record in a table, with one value for each column that table defines.
schema
The stored plan of a database: its tables, the columns in each, and the rules those columns must follow.
table
A named set of rows that all share the same columns, such as one holding every book in a library.

SQL from the ground up

Counting and grouping

Turning many rows into a few numbers: COUNT, SUM, AVG, MIN and MAX, the GROUP BY that gives each group its own answer, and HAVING for filtering the groups.

Part 5 of 12

Beginner16 min readBack to Courses

One number from many rows

An aggregate function folds a column of values into a single answer. Counting with the wildcard counts the rows — every row, NULLs included — while COUNT(series) counts only the rows where series is set. SUM, AVG, MIN and MAX do what their names say.

On their own they answer one question about the whole table: how many books there are, the oldest year, the average year. The result is a single row with a single column, and naming that column with AS makes the answer readable.

GROUP BY folds rows into buckets

GROUP BY author sorts the rows into one bucket per author and then applies the aggregates to each bucket separately — one result row per author, with that author's count next to the name. That is the whole idea: one row in, one row out, per group.

Every column in the SELECT must either be one of the grouped columns or wrapped in an aggregate. The query below asks for the author and a count — a name and a number per bucket — and nothing that would need to be picked arbitrarily from inside the bucket.

WHERE before, HAVING after

WHERE filters rows before they are grouped; HAVING filters the groups after the counting. "Books published after 1960, grouped by author" uses WHERE year > 1960; "only authors with more than one such book" adds a HAVING clause that tests the count as a number.

Using WHERE when you mean HAVING is one of the classic errors, and the database will tell you about it — an aggregate cannot be tested before it has been computed.

What the aggregates see

COUNT(*) counts rows. COUNT(column) counts the rows where that column is not NULL, and COUNT(DISTINCT column) counts the distinct values that are there. The difference is not cosmetic: five books with two missing series have five rows, five titles, and three recorded series.

SUM, AVG, MIN and MAX also skip NULLs. AVG(column) divides by the number of non-NULL values, not by the number of rows, so a column with missing values reports the average of the values it has. SUM over no rows is NULL, while COUNT over no rows is 0 — one means "nothing to add", the other "none found".

An aggregate without GROUP BY always answers with exactly one row, even when the table is empty. That row is the answer to "how many", not a list of the rows it counted.

The rule behind GROUP BY

Once rows are folded into a group, a bare column has no single value to show. Standard SQL therefore requires every selected column to be grouped or aggregated. SQLite is looser: it lets you select any column and quietly picks a value from one row of the group — usually the last one it read, which is not a promise.

That looseness is a portability trap. A query that works in SQLite can fail in PostgreSQL with "column must appear in the GROUP BY clause", and the value it picked in SQLite was arbitrary anyway. Write the grouped column or an aggregate, and the query means the same thing everywhere.

GROUP BY can name more than one column, and the groups are the combinations. GROUP BY author, year gives one row per author per year, which is a different report from one row per author. NULLs form a group of their own.

It does not promise an order, either. Groups come back in whatever order the database produced them, which is often the order it first met each one — not alphabetical, and not the order the query listed them in. ORDER BY is the only clause that decides the order of the answer.

A count per group, filtered

SELECT author, COUNT(*) AS books
FROM books
WHERE year > 1950
GROUP BY author
HAVING COUNT(*) > 1
ORDER BY books DESC, author;

Three ways a count goes wrong: testing COUNT(*) in WHERE, where the count does not exist yet (that is HAVING); counting a nullable column and expecting the row count; and filtering rows in WHERE when you meant to filter groups — WHERE year > 1960 removes old books before they are counted, which changes the groups themselves.

Try these next

  • Count the distinct authors in the table and compare the answer with COUNT(author).
  • Group by author and year, then keep only the pairs with more than one book.
  • Add HAVING SUM(year) > 4000 and explain which groups survive and why.

How many each author wrote

Show every author beside the number of books they wrote, most prolific first.

Live Preview

The busiest authors, with a HAVING

Run both queries. Then change the first `HAVING` to `HAVING COUNT(*) > 1 AND MIN(year) > 1966` and explain which author disappears.

Live Preview

Aggregates skip NULLs, which is usually what you want — the average year of a column with a few missing years is the average of the years that are there. The wildcard count is the exception: it counts rows, not values.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is the difference between counting every row and counting one column?