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

Working with text and numbers

Functions that shape each row as it comes back: upper, lower and length, joining text with the double pipe, and rounding numbers that came out of arithmetic.

Part 9 of 12

Intermediate15 min readBack to Courses

Functions work row by row

A function in the SELECT list runs once per row and its result takes the column's place: SELECT upper(title) FROM books; returns every title in capitals without changing what is stored. lower, length, trim and substr work the same way.

The result of a function has no name worth keeping, so this is where aliases earn their place: SELECT upper(title) AS heading FROM books;. Everything else about the query — filtering, sorting, grouping — is unchanged.

Joining text

Two bars side by side — || — join text into one value: SELECT author || ' wrote ' || title AS line FROM books;. Mixed types are converted on the way, so a year joins into the sentence as text without ceremony.

Functions and joins compose: upper(substr(title, 1, 3)) takes the first three characters and capitalises them. Reading such a line from the inside out is the same skill as reading a nested function call in any language.

Numbers, and the sharp edge in division

ROUND(value, 2) rounds to two decimals, and ABS removes the sign. Both are most useful around a calculation: ROUND(year / 10.0, 1) gives a decade with a fraction where a plain integer division would have thrown the remainder away.

That is the sharp edge worth knowing about: in SQLite, dividing two integers keeps an integer — 7 / 2 is 3, not 3.5. Write one side as a decimal (7 / 2.0) when the fraction is what you are after.

Text functions, one row at a time

length(text) counts characters, not bytes, so an accented letter is one. substr(text, start, length) counts from 1, and a negative start counts back from the end: substr('Dune', 2, 2) is 'un', and substr('Dune', -2) is 'ne'.

replace, trim, ltrim, rtrim and instr are the rest of the everyday set. instr(haystack, needle) returns the 1-based position or 0 when it is absent, which makes it the tool for splitting a value at a delimiter.

These functions run on the way out. WHERE upper(author) = 'DUNE' compares the shaped value and leaves the stored one alone, which is also why a function in a condition can stop an index from being used: the database has to compute it for every row first.

Numbers have storage classes

SQLite stores a number as an integer or a real, and typeof() tells you which. An integer stays exact; a real is a floating-point value, so 0.1 + 0.2 is not exactly 0.3. ROUND hides the difference in the display without changing the underlying value.

Integer division is the sharpest edge: 7 / 2 is 3, because both sides are integers. 7 / 2.0, CAST(7 AS REAL) / 2 and 7 * 1.0 / 2 all produce 3.5; the first is the shortest. CAST(value AS INTEGER) and CAST(value AS REAL) make the type explicit, which is easier to trust than relying on SQLite's conversion rules.

ROUND(value, digits) returns a real, not a formatted string. Formatting — two decimal places, a currency symbol — belongs to the program that displays the answer, not to the query that computes it.

Numbers that are not numbers

SQLite will store the text '12' in an INTEGER column and convert it on the way in, but a value that is not a number at all — 'twelve' — stays text, and arithmetic on it treats it as zero. typeof() is the honest check, and CAST is the explicit conversion. A stricter database would have refused the insert; here it succeeds, and the mistake surfaces later as a total that is quietly too small.

The fix is not to cast everything in sight. Cast where a value enters the table, so the column holds one kind of thing, and let the query trust it afterwards. A cast inside a comparison is usually a sign that the data already drifted.

Shaping the answer as it comes back

SELECT upper(title) AS heading, length(title) AS letters
FROM books
ORDER BY letters DESC;

SELECT author || ' — ' || title AS line
FROM books;

SELECT title, ROUND(year / 10.0, 0) AS decade
FROM books;

Three mistakes with text and numbers: joining with || when one side may be NULL, which makes the whole line NULL, so wrap the nullable side in COALESCE; dividing two integers and losing the fraction; and comparing a number stored as text with a number, where SQLite's conversion rules can surprise you.

Try these next

  • Build a surname, initial label from a full name with instr and substr.
  • Compute a percentage with ROUND(part * 100.0 / total, 1) and check the type with typeof.
  • Replace every double space in a title with one, then trim the result.

Capitals, lengths and decades

Show each title in capitals with the number of letters in it, and a second query with the decade each book was published in.

Live Preview

A price list, shaped for reading

Run it, then add a `saving` column that shows how much the discount took off, rounded to two decimals.

Live Preview

Text sorts as text, which is why dates are usually stored as YYYY-MM-DD: in that shape, sorting alphabetically is the same as sorting by time, and no date arithmetic is needed to compare two of them.

Check what you learned

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

Part 1 of 5

What does SELECT upper(title) FROM books; change?