Why interviewers love this: IN looks simple, but it quietly tests filtering logic, readability, and the classic NULL trap.
Question: What does IN do in SQL?
Answer: IN checks whether a value matches any item in a list or in the result of a subquery. It is a compact way to write several equality checks at once, like col = 'A' OR col = 'B' OR col = 'C'. The main gotcha is that NOT IN behaves unexpectedly when NULL appears in the list or subquery result.
Interview-Ready Answer: I use IN when I want to filter rows by membership in a set, either a literal list or a subquery. It makes the query shorter and easier to read than many OR conditions, and the optimizer can often turn it into an efficient membership check or semi-join. The detail I always remember is that NOT IN plus NULL can eliminate every row, so when nulls are possible I consider NOT EXISTS instead.
IN really meansIN is a membership test: it asks, “Is this value one of these values?” For a single column, the database compares the left-hand value against each candidate and keeps the row if any candidate matches. In SQL, matching is based on three-valued logic, which means a comparison can be TRUE, FALSE, or UNKNOWN. That third state matters because WHERE only keeps rows where the result is TRUE.
IN list or the subquery and builds a membership test.NOT IN, the engine must also account for NULL, because a single null can turn the whole result into UNKNOWN.Use IN when the set of allowed values is small, clear, and stable: statuses, categories, country codes, or IDs from a subquery. It is especially nice when you would otherwise write several equality checks. If the list becomes very large, a table or join is usually cleaner and easier to maintain.
| Form | Best for | Main risk |
|---|---|---|
IN | Small membership lists | Long lists get messy |
OR | Only a few values | Verbose and easy to mistype |
EXISTS | Checking a related table | Less obvious to beginners |
JOIN | Need columns from both sides | Can duplicate rows |
There is no single fixed complexity for SQL because the optimizer matters. Conceptually, a naive membership check is O(n * k) for n rows and k list items, but real engines often do better by hashing the list or using an index. If the filtered column is indexed, the database may do multiple index lookups instead of scanning the whole table. Small lists of a few values are usually fine; hundreds or thousands of values can increase parse time, plan size, and network payload. Also, many databases have parameter limits, such as SQL Server’s 2100-parameter limit, so a giant IN list may need a temp table or staging table instead.
IN (NULL) does not match normal values, because value = NULL is UNKNOWN, not TRUE.NOT IN with any NULL in the right side can return no rows at all.IN with a subquery ignores duplicate values; duplicates do not make the answer more true.IN () is invalid syntax in most databases.Memory model: think of IN as a bouncer with a guest list. If your name is on the list, you get in; if the list contains a blank name (NULL), the bouncer gets confused, and NOT IN can shut the door for everyone.
Imagine a checkout service for an e-commerce app. The team keeps a list of allowed order statuses and uses WHERE status IN ('PAID', 'PACKING', 'SHIPPED') to drive the warehouse dashboard. That works well because the business rule is small, readable, and changes rarely.
Now the bug story: a nightly job used NOT IN to exclude blocked customers from a marketing email list. One bad data import inserted a NULL into the block table, and suddenly the query returned zero customers. The symptom was a silent outage: no email campaign, empty logs except for a normal-looking query, and a support ticket from marketing asking why the send count dropped to zero. The fix was to replace NOT IN with NOT EXISTS and add a data-quality check to prevent nulls in the block list.
-- Demonstration of IN, NOT IN, and the NULL edge case
DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS blocked_departments;
CREATE TABLE employees (
employee_id INTEGER PRIMARY KEY,
employee_name VARCHAR(50) NOT NULL,
department VARCHAR(50),
salary INTEGER NOT NULL
);
INSERT INTO employees (employee_id, employee_name, department, salary) VALUES
(1, 'Ava', 'Sales', 70000),
(2, 'Ben', 'Engineering', 95000),
(3, 'Cara', 'HR', 60000),
(4, 'Dan', 'Engineering', 105000),
(5, 'Eli', NULL, 50000);
-- 1) IN with a literal list: keep rows whose department is one of these values.
SELECT employee_name, department
FROM employees
WHERE department IN ('Sales', 'Engineering')
ORDER BY employee_id;
-- 2) IN with a subquery: keep rows whose department appears in a derived set.
SELECT employee_name, department
FROM employees
WHERE department IN (
SELECT department
FROM employees
WHERE salary >= 90000
)
ORDER BY employee_id;
-- 3) NOT IN with a literal list: this excludes the named departments.
-- Note: the row with NULL department does not pass the WHERE clause.
SELECT employee_name, department
FROM employees
WHERE department NOT IN ('HR', 'Sales')
ORDER BY employee_id;
-- 4) Edge case: NULL inside the right side of NOT IN.
-- This is the classic trap. Because the subquery returns NULL, the result
-- of the comparison becomes UNKNOWN for every non-matching row.
DROP TABLE IF EXISTS blocked_departments;
CREATE TABLE blocked_departments (
department VARCHAR(50)
);
INSERT INTO blocked_departments (department) VALUES
('HR'),
(NULL);
-- In many databases, this returns NO ROWS because NOT IN + NULL is poisonous.
SELECT employee_name, department
FROM employees
WHERE department NOT IN (
SELECT department
FROM blocked_departments
)
ORDER BY employee_id;
-- Safer alternative when NULLs may exist: NOT EXISTS.
-- This checks for an actual matching row instead of relying on set membership.
SELECT e.employee_name, e.department
FROM employees e
WHERE NOT EXISTS (
SELECT 1
FROM blocked_departments b
WHERE b.department = e.department
)
ORDER BY e.employee_id;Follow-up & Tricky Questions:
IN different from EXISTS? IN asks whether a value is in a set; EXISTS asks whether at least one related row exists. They often return the same result, but EXISTS is usually safer when nulls are involved.IN? Yes. Many engines rewrite IN (subquery) into a semi-join or a hashed membership test, which can be much faster than evaluating the subquery repeatedly.IN use indexes? It can. If the filtered column is indexed, the engine may probe the index for each candidate value instead of scanning the whole table.IN remove duplicates? It does not change the source table’s duplicates by itself; it only decides which rows pass the filter. If the left table has duplicate rows, both can still be returned.NOT IN with NULL return nothing? Because one comparison becomes UNKNOWN, and in SQL a row is kept only when the WHERE condition is TRUE. UNKNOWN is not true, so the row is filtered out.IN (NULL) ever true? No for normal scalar comparisons. A null comparison does not produce true; it produces unknown, so the row is not selected.IN list? Usually no. IN () is invalid syntax in most databases, so if your list may be empty you need a different pattern.Common Mistakes:
NOT IN with possible NULLs — correction: use NOT EXISTS or clean the nulls first.IN list — correction: if the list is large, load the values into a table and join.IN and JOIN are always interchangeable — correction: JOIN can duplicate rows and bring back extra columns, so it is not the same question.WHERE keeps only TRUE — correction: FALSE and UNKNOWN both get filtered out.Memory Hook: IN is a guest list. If your name is on the list, you enter; if the list has a blank name, NOT IN can lock the whole door.
Cheat Sheet:
IN = membership test.OR conditions.NOT IN + NULL is the classic trap.NOT EXISTS when nulls are possible.Practice Tasks:
OR, once with IN, and compare readability.NULL value and test how NOT IN and NOT EXISTS differ.