Patterns, ranges and missing values
LIKE for a pattern rather than a word, IN and BETWEEN for the two shapes a condition often takes, and the right way to ask about a value that is not there.
LIKE matches a shape
= wants the whole value. LIKE wants a pattern: % stands for any run of characters, _ for exactly one. title LIKE 'The%' finds every title that starts with those three letters, and LIKE '%Darkness%' finds the word anywhere in the title.
Unlike =, LIKE compares letters case-insensitively for plain ASCII, so a search typed in lower case still finds a capitalised title. The two wildcards are the entire vocabulary, and they are enough for most searching.
IN and BETWEEN
A list of alternatives reads badly with OR: author = 'A' OR author = 'B' OR author = 'C'. IN says it once — WHERE author IN ('A', 'B', 'C') — and any value in the list passes.
BETWEEN describes a range and is inclusive at both ends: WHERE year BETWEEN 1960 AND 1980 keeps 1960 and 1980 themselves, and everything between. It works on dates too, as long as they are stored in a sortable text form like ISO dates.
The value that is not there
A column with no value stores NULL, and NULL is not a value — it is the absence of one. That is why WHERE series = NULL never matches anything: the comparison is unknown, not true. The question is asked with IS NULL and IS NOT NULL.
This is the one place SQL is strict about itself. Every other comparison either matches or it does not; a comparison with NULL is a third answer, and the row is kept only when the answer is a definite yes.
How a pattern is matched
LIKE compares the whole value with the pattern, not a part of it. The pattern is built from literal characters, % for any run of characters including none, and _ for exactly one character. 'Dune Messiah' matches 'Dune%', but '%Dune' would ask for a value that ends with those four letters.
The comparison is case-insensitive for ASCII letters, which is convenient for search and surprising when case matters. SQLite's GLOB is the case-sensitive cousin, and LIKE ... ESCAPE lets a backslash make a literal % or _ instead of a wildcard.
A pattern that begins with % cannot use an index, because the database cannot know where the match starts. A prefix pattern can, which is why LIKE 'Dune%' is cheaper than LIKE '%Dune%' on a large table.
Missing values spread through a condition
NOT IN looks like the safe way to say "none of these", but it has a trap. If the list contains NULL, every comparison against it is unknown, and the test can never be true — the query returns no rows, not the rows you excluded. The positive form is subtler: year IN (1965, NULL) can still match 1965, because one true comparison is enough.
NOT EXISTS does not have that problem, because it asks whether a row exists rather than comparing values. When a subquery might produce NULL, that is the form to reach for.
The same three-valued logic runs through AND and OR: true or unknown is true, false and unknown is false, and everything else stays unknown. A WHERE keeps only the rows whose answer is a definite yes.
Ranges of text, not just numbers
BETWEEN is not only for numbers. On text it means alphabetical betweenness, which is useful for codes and names and dangerous for dates stored in the wrong shape: BETWEEN '01/01/2020' AND '31/12/2020' compares the first characters, so it will happily include a date from 2021. ISO dates avoid the trap because their text order is their time order.
The other edge is the end of a range that holds times. BETWEEN '2020-01-01' AND '2020-12-31' leaves out anything recorded later on the 31st, because that timestamp is greater than the bare date. The safe shape is >= '2020-01-01' AND < '2021-01-01', which includes the whole final day without naming its last second.
A pattern, a list, a range, an absence
SELECT title FROM books WHERE title LIKE 'The%';
SELECT title FROM books WHERE author IN ('Ursula K. Le Guin', 'Octavia E. Butler');
SELECT title, year FROM books WHERE year BETWEEN 1960 AND 1980;
SELECT title FROM books WHERE series IS NULL;Three mistakes this chapter invites: writing LIKE 'Dune' with no wildcard, which is just a case-insensitive equals; putting a NULL in a NOT IN list, which makes the whole condition return nothing; and comparing dates stored as DD/MM/YYYY text, where alphabetical order is not chronological order.
Try these next
- Find every title whose second letter is
u, using_for the first character. - Search for a title that contains a literal percent sign, using
LIKE ... ESCAPE. - Write the same exclusion with
NOT INand withNOT EXISTS, then add a NULL to the list and compare.
Search and absence
Write two queries: one for every book whose title starts with the word The, and one for every book with no series set.
Codes, one character at a time
Run the three shelf queries. Then change the first pattern to `QA-1%` and explain why a second row appears.
Put the wildcards where the uncertainty is. A leading % cannot use an index, so a search that starts with a known prefix — LIKE 'Dune%' — is both more precise and faster on a large table.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does LIKE 'Dark%' match?
