03 · Filtering with WHERE¶
🎥 Video walkthrough¶
WHERE narrows a result set down to the rows that match a condition. It's
evaluated per row, before any grouping or sorting happens.
We'll reuse the books table from Module 2:
CREATE TABLE books (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL,
price REAL,
published_year INTEGER,
genre TEXT
);
INSERT INTO books (title, author, price, published_year, genre) VALUES
('Dune', 'Frank Herbert', 9.99, 1965, 'Sci-Fi'),
('Foundation', 'Isaac Asimov', 8.50, 1951, 'Sci-Fi'),
('Neuromancer', 'William Gibson', 7.25, 1984, 'Cyberpunk'),
('The Left Hand of Darkness', 'Ursula K. Le Guin', 8.99, 1969, 'Sci-Fi'),
('Snow Crash', 'Neal Stephenson', 10.50, 1992, 'Cyberpunk'),
('The Hobbit', 'J.R.R. Tolkien', 6.99, 1937, 'Fantasy');
Comparison operators¶
SELECT title, price FROM books WHERE price > 8.50;
SELECT title, price FROM books WHERE price >= 8.50;
SELECT title FROM books WHERE published_year = 1965;
SELECT title FROM books WHERE published_year != 1965; -- or <> 1965
| Operator | Meaning |
|---|---|
= |
equal to |
!= / <> |
not equal to |
>, < |
greater / less than |
>=, <= |
greater-or-equal / less-or-equal |
Combining conditions — AND, OR, NOT¶
-- AND: both conditions must be true
SELECT title FROM books
WHERE genre = 'Sci-Fi' AND price < 9.00;
-- OR: at least one condition must be true
SELECT title FROM books
WHERE genre = 'Fantasy' OR genre = 'Cyberpunk';
-- NOT: negates a condition
SELECT title FROM books
WHERE NOT genre = 'Sci-Fi';
AND binds tighter than OR, exactly like * binds tighter than + in
arithmetic — use parentheses whenever you mix them, since relying on
precedence rules is a common source of bugs:
-- Ambiguous-looking without parens; means: (genre = 'Sci-Fi') AND (price < 8 OR price > 10)
SELECT title FROM books
WHERE genre = 'Sci-Fi' AND (price < 8 OR price > 10);
-- vs a totally different meaning if you drop the parens:
SELECT title FROM books
WHERE genre = 'Sci-Fi' AND price < 8 OR price > 10;
BETWEEN — inclusive range¶
Equivalent to published_year >= 1960 AND published_year <= 1990 — both
endpoints are included.
IN — matching a set of values¶
SELECT title, genre FROM books
WHERE genre IN ('Fantasy', 'Cyberpunk');
-- equivalent to, but shorter than:
SELECT title, genre FROM books
WHERE genre = 'Fantasy' OR genre = 'Cyberpunk';
NOT IN negates it:
LIKE — pattern matching¶
LIKE matches text against a pattern using two wildcards:
%matches any sequence of characters (including zero characters)_matches exactly one character
SELECT title FROM books WHERE title LIKE 'The%'; -- starts with "The"
SELECT title FROM books WHERE title LIKE '%Crash'; -- ends with "Crash"
SELECT title FROM books WHERE title LIKE '%Dune%'; -- contains "Dune"
SELECT title FROM books WHERE author LIKE '_ea%'; -- 2nd char is 'e', 3rd is 'a'
LIKE is case-insensitive for ASCII in SQLite by default, but this varies —
Postgres's LIKE is case-sensitive (use ILIKE there for case-insensitive
matching). Always test the behavior of the database you're actually using.
IS NULL / IS NOT NULL¶
You cannot test for NULL with = NULL — it silently matches nothing,
because NULL represents "unknown," and nothing is known to equal an unknown
value. This is explored fully in Module 7.
Combining everything¶
SELECT title, author, price, published_year
FROM books
WHERE genre = 'Sci-Fi'
AND published_year BETWEEN 1950 AND 1970
AND price < 10.00;
title author price published_year
----------- --------------- ----- --------------
Dune Frank Herbert 9.99 1965
Foundation Isaac Asimov 8.5 1951
How It Actually Works¶
A WHERE clause without a supporting index forces SQLite into a full table
scan: the query planner walks every leaf page of the table's B-tree in
physical order, decodes each row's record, evaluates the predicate against
it, and discards rows that don't match. You can watch this happen with
EXPLAIN QUERY PLAN SELECT ... WHERE ... — a plan of SCAN t means every row
is visited; SEARCH t USING INDEX ... means the planner found a B-tree it
could seek into directly. AND-connected conditions are evaluated
short-circuit, left to right as compiled, so cheap, selective conditions
should come first if you're hand-tuning a hot query. OR is more expensive:
unless every branch is separately indexed (letting SQLite use an OR-optimization
that unions multiple index searches), an OR clause typically forces a full
scan because no single B-tree ordering can satisfy both branches at once.
LIKE '%foo%' (leading wildcard) can never use a plain index seek either,
because a B-tree is sorted by prefix — only LIKE 'foo%' (anchored prefix)
lets the planner binary-search into the tree.
🔀 See this in another language¶
Exercise¶
Using the books table above, write queries to find:
- All books priced under $8.00.
- All books that are either
'Sci-Fi'or'Fantasy', usingIN. - All books with a title containing the word
"the"(any case), usingLIKE. - All books published between 1960 and 2000 that cost more than $7.
- All books whose author's name starts with a letter in
('F', 'N')— hint: combineLIKEpatterns withOR, or usesubstr().