08 — IN

One line: IN checks whether a value matches any item in a list — a clean shorthand for a pile of ORs.


The idea in plain English

When you want "is this value one of these several options?", writing it with OR gets repetitive fast:

WHERE name = 'Zach' OR name = 'David' OR name = 'Claire'

IN says the same thing, readably:

WHERE name IN ('Zach', 'David', 'Claire')

It's literally a chain of ORs under the hood — match any value in the list and the row is returned. NOT IN flips it: return rows that match none of them.


Syntax

-- Strings: quoted, comma-separated, inside parentheses
SELECT * FROM customer
WHERE first_name IN ('John', 'Jake', 'Julie');

-- Numbers: NO quotes (match the column's data type)
SELECT * FROM payment
WHERE amount IN (0.99, 1.98, 1.99);

-- Exclude a set
SELECT * FROM payment
WHERE amount NOT IN (0.99, 1.98, 1.99);

Two rules to remember: parentheses around the list, commas between items, and the list values must match the column's type — quote strings, leave numbers bare.

-- Count matches vs non-matches
SELECT COUNT(*) FROM payment WHERE amount IN (0.99, 1.98, 1.99);      --> 3301
SELECT COUNT(*) FROM payment WHERE amount NOT IN (0.99, 1.98, 1.99);  --> 11295

Handy habit: SELECT DISTINCT amount FROM payment ORDER BY amount; first, to see the values before you build your IN list.


The NOT IN + NULL trap ⚠️ (career-saver)

This is one of the most infamous bugs in SQL. If the list contains a NULL, NOT IN returns no rows at all — even rows that obviously shouldn't be excluded.

-- Looks fine. Returns ZERO rows. 😱
SELECT * FROM film
WHERE rating NOT IN ('R', 'PG-13', NULL);

Why: for a 'G' film, SQL evaluates 'G' NOT IN ('R','PG-13',NULL) as NOT ('G'='R' OR 'G'='PG-13' OR 'G'=NULL) → NOT (false OR false OR **unknown**) → NOT (unknown) → unknown. And WHERE only keeps rows that are strictly true, so nothing survives.

  • IN with a NULL in the list is mostly harmless (that entry just never matches).
  • NOT IN with a NULL in the list is poison — it silently wipes your result set.

This matters most with subqueries (a later lesson): WHERE id NOT IN (SELECT col FROM t) breaks the instant col contains a single NULL. The safe alternatives are NOT EXISTS or adding WHERE col IS NOT NULL to the subquery. File this away now — it's a bug people spend hours on.


Production notes

  • IN beats stacked ORs for readability — and it's the fix for the "each side of OR needs a full condition" mistake from the WHERE lesson. rating IN ('G','PG','PG-13') vs rating = 'G' OR rating = 'PG' OR rating = 'PG-13'.
  • IN with a subquery is where it gets powerful (coming up): WHERE customer_id IN (SELECT customer_id FROM payment WHERE amount > 10). That's how you filter one table by results from another.
  • Order and duplicates in the list don't matter. IN (1, 2, 2) behaves like IN (1, 2).
  • Very long IN lists get slow. A hardcoded list of hundreds/thousands of values is a smell — usually better as a join against a temp table or a subquery.
  • Type mismatches can error or silently coerce. Putting '0.99' (quoted) in a numeric IN list may work via implicit casting on some engines but is fragile — always match the column's real type.

Challenges

All use the DVD Rental database.

Challenge 1 — Warm-up

Count how many films are rated either G or PG. (Table: film, column rating.)

Solution
SELECT COUNT(*) FROM film
WHERE rating IN ('G', 'PG');

Why: "One of these values" → IN with a quoted list. Cleaner than rating = 'G' OR rating = 'PG', and it scales if you add more ratings later.


Challenge 2 — Exclude a set (NOT IN)

Return the title and rating of every film that is not rated G or PG (i.e. the more mature titles).

Solution
SELECT title, rating
FROM   film
WHERE  rating NOT IN ('G', 'PG');

Why: NOT IN returns everything whose rating is neither G nor PG — so PG-13, R, NC-17 come through. (Safe here because the list has no NULLs — see Challenge 3 for when it isn't.)


Challenge 3 — Spot the bug 🔎 (the NULL trap)

A teammate wants all films that are not rated R or PG-13. They defensively toss a NULL into the list "just in case," and now the query returns nothing:

SELECT * FROM film
WHERE rating NOT IN ('R', 'PG-13', NULL);

Explain why, and give the correct query.

Solution

Bug: The NULL in a NOT IN list poisons the whole condition. For any film, the comparison includes rating = NULL, which is unknown, and NOT (… OR unknown) collapses to unknown. WHERE keeps only true rows, so every row is discarded — even G, PG, NC-17 films that clearly qualify.

Fix — just don't put NULL in the list:

SELECT * FROM film
WHERE rating NOT IN ('R', 'PG-13');

Why it matters: NOT IN + any NULL in the list = empty result, silently. Never include NULL in a NOT IN, and when the list comes from a subquery, guard it (WHERE col IS NOT NULL) or use NOT EXISTS instead. This is the single most notorious IN footgun.


30-second recap

ThingTakeaway
IN (a, b, c)Value matches any listed item (chained OR)
NOT IN (…)Value matches none
SyntaxParentheses + commas; match column's type
Strings vs numbersQuote strings, leave numbers bare
NOT IN + NULLReturns zero rows — never do it
SubqueriesIN (SELECT …) is the real power (later)
vs ORIN is the readable replacement