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

Labels and missing values

CASE chooses a value per row — the if/else of SQL — and COALESCE gives a missing value a stand-in, which is how a report stays readable without a single blank cell.

Part 11 of 12

Advanced15 min readBack to Courses

CASE chooses per row

CASE WHEN year < 1970 THEN 'classic' ELSE 'modern' END is an expression that runs once per row and produces the value of the branch it lands in. It can stand in the SELECT list, in an ORDER BY, and inside an aggregate.

The branches are read from the top, and the first true condition wins — order matters, exactly as it does with a chain of ifs. Without an ELSE, an unmatched row gets NULL, which is occasionally what you mean and more often an invitation to a second thought.

NULL spreads

Any arithmetic or comparison with NULL produces NULL — the unknown flows through the calculation. That is why a missing year turns a whole expression into nothing, and why a report tends to show blanks where the data is incomplete.

COALESCE(year, 0) returns the first value that is not NULL: the year when it is there, the stand-in otherwise. IFNULL(year, 0) is the same idea with two arguments. COALESCE accepts a whole list, so it can be a chain of fallbacks.

Where this pays off

A label built with CASE is how a report says what a number means; a stand-in from COALESCE is how the table stays printable. Together they are the difference between a dump of rows and something a person can read.

In a real database, a column that is frequently NULL is sometimes a hint that the data belongs in its own table — the schema conversation this course leaves you ready to have.

A label is text, and text sorts as text

A CASE label is text, so ORDER BY era sorts the words: classic, modern, recent. That happens to match the chronology here and would not if the branches were named old, middle and new. When the order matters, sort by the column the label was derived from, or give each branch a rank and sort on that.

A label is for reading; the rank is for ordering. Both can sit in the same SELECT, one shown and one not, and the query stays honest about which of the two it is sorting by.

CASE is an expression

CASE produces a value, so it can stand anywhere a value can: in the SELECT list, in WHERE, in ORDER BY, and inside an aggregate such as SUM(CASE WHEN ... THEN 1 ELSE 0 END). That last shape is how you count a subset without filtering the rows away.

There are two forms. The searched form writes each condition in full and is the one to reach for. The simple form compares one expression against a list — CASE status WHEN 'a' THEN ... WHEN 'b' THEN ... END — and is shorter when every branch tests the same value.

The branches should agree on a type. Mixing a number and a word is allowed in SQLite, which will store either in the same column, but the result is hard to sort and awkward to sum. A label is text in every branch.

NULL has a type of its own

NULL is not equal to anything, including itself. Two rows with a NULL in the same column are not duplicates for UNIQUE, and GROUP BY puts all of them into one group because it treats them as the same unknown.

COALESCE walks its arguments and returns the first non-NULL. IFNULL(a, b) is the two-argument shorthand, and NULLIF(a, b) goes the other way: it returns NULL when the two are equal, which is how a sentinel such as an empty string becomes a real missing value.

The functions differ in what they count. COUNT(column) skips NULLs; COUNT(COALESCE(column, 0)) counts every row, because the value handed to COUNT is never NULL. That is a feature when you mean rows and a bug when you meant values.

A label, and a year with a stand-in

SELECT title,
       CASE
         WHEN year < 1970 THEN 'classic'
         WHEN year < 1990 THEN 'modern'
         ELSE 'recent'
       END AS era,
       COALESCE(year, 0) AS year_shown
FROM books
ORDER BY year_shown;

Three CASE and NULL mistakes: leaving out ELSE, so unmatched rows become NULL; ordering branches from broad to narrow, so a later, more specific condition never gets a chance; and wrapping a column in COALESCE before COUNT, which quietly turns a count of values into a count of rows.

Try these next

  • Count the books per era with SUM(CASE WHEN ... THEN 1 ELSE 0 END) and no GROUP BY.
  • Use NULLIF to turn an empty series name into NULL, then count the missing ones.
  • Sort a report with NULL labels last by adding NULLS LAST or a computed flag.

Label the shelf

Give each book an era label — before 1970, before 1990, and everything after — and show its year or the word unknown when the year is missing.

Live Preview

A price sheet with a readable label

Run it, then add a `pay` label that says "full price" when the discount is NULL and "reduced" otherwise, and a `saving` column with the money taken off.

Live Preview

Give every CASE an ELSE while you are exploring. An unmatched row quietly becomes NULL, and a NULL label at the top of a sorted report is easy to mistake for a bug in the sort.

Check what you learned

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

Part 1 of 5

Two WHEN conditions in a CASE are both true. Which value comes out?