IN, BETWEEN, and LIKE
Three operators that replace long chains of OR with something a human can read.
IN — one of a list
SQLSELECT first_name, dept_id FROM employees WHERE dept_id IN (1, 2) ORDER BY dept_id, first_name;
Identical to dept_id = 1 OR dept_id = 2, and it stays readable at ten values
where the OR chain does not. It works on text too:
SQLSELECT order_id, status, amount FROM orders WHERE status IN ('pending', 'cancelled') ORDER BY order_id;
The NOT IN trap
NOT IN with a NULL anywhere in the list returns nothing at all:
SQLSELECT COUNT(*) AS surprising_zero FROM employees WHERE dept_id NOT IN (1, 2, NULL);
Zero rows. x NOT IN (1, 2, NULL) expands to
x <> 1 AND x <> 2 AND x <> NULL, and that last comparison is never true, so
the whole AND chain can never be true. This bites hardest when the list comes
from a subquery that happens to contain a NULL — the query returns nothing, no
error is raised, and the report simply shows a blank page. NOT EXISTS is the
safe form, and gets its own lesson later.
BETWEEN — an inclusive range
SQLSELECT first_name, hired_on FROM employees WHERE hired_on BETWEEN '2020-01-01' AND '2021-12-31' ORDER BY hired_on;
BETWEEN a AND b means >= a AND <= b. Both ends are included — the most
common misreading, and the reason off-by-one bugs cluster here.
Two rules worth internalising:
- The low value goes first.
BETWEEN 100 AND 1matches nothing, silently. - Do not use
BETWEENon timestamps.BETWEEN '2024-03-01' AND '2024-03-31'misses everything that happened during the last day, because a timestamp on the 31st at 09:00 is greater than the 31st at midnight. Use a half-open range instead —>= '2024-03-01' AND < '2024-04-01'— which is correct regardless of month length, leap years, or how many decimal places the timestamps carry.
Half-open on our date-only column, for comparison:
SQLSELECT order_id, placed_at, amount FROM orders WHERE placed_at >= '2024-03-01' AND placed_at < '2024-04-01' ORDER BY placed_at;
LIKE — pattern matching on text
Two wildcards:
| Pattern | Matches |
|---|---|
% | any run of characters, including none |
_ | exactly one character |
SQLSELECT first_name, email FROM employees WHERE email LIKE 'a%' ORDER BY email;
More patterns:
SQLSELECT name, category FROM products WHERE name LIKE '%a%' ORDER BY name;
'a%'— starts with a'%plan'— ends with plan'%wid%'— contains wid'_at'— three characters ending in "at"
Case sensitivity, again
LIKE follows the same rules as =: case-sensitive on PostgreSQL, Oracle and
SQLite (though SQLite's LIKE is case-insensitive for ASCII by default — a
genuine inconsistency with its own =), usually insensitive on MySQL and SQL
Server. PostgreSQL offers ILIKE for an explicitly case-insensitive match; the
portable version is LOWER(col) LIKE LOWER('pattern').
Matching a literal %
To search for a percent sign, escape it: LIKE '100!%' ESCAPE '!'. Any
character can be the escape character; \ is conventional but must still be
declared with ESCAPE.
The performance note
LIKE 'abc%' can use an index — the database knows where to start looking.
LIKE '%abc' and LIKE '%abc%' cannot: with an unknown prefix, every row must
be examined. On a large table that is the difference between a millisecond and a
minute, and it is why search boxes that do leading-wildcard matching are the
first thing to fall over under load. Full-text search exists for this.
Common mistakes
NOT INover a list containing NULL — returns nothing, silently.- Assuming
BETWEENis exclusive — it includes both ends. BETWEENon timestamps — loses the final day.LIKE '%x%'on a large table — no index can help.LIKEwith no wildcard —LIKE 'Ada'is just= 'Ada', written slower.
Interview question
Why can
NOT IN (SELECT …)return zero rows when you expect many?
If the subquery yields even one NULL, every comparison in the expanded AND
chain becomes unknown and no row qualifies. Use NOT EXISTS, or filter the
NULLs out of the subquery.
Check yourself
- Rewrite
dept_id = 1 OR dept_id = 3 OR dept_id = 5withIN. - Write a March 2024 filter that is correct even if
placed_atgains a time. - Which of
LIKE 'ab%',LIKE '%ab',LIKE '%ab%'can use an index?