SQL AND, OR, and NOT
The AND, OR, and NOT operators are the building blocks of every non-trivial SQL WHERE clause. They let you combine two or more conditions, require any one of several conditions to be true, or reverse a condition entirely. Almost every real-world query — “active customers in the UK”, “orders placed this year but not refunded” — depends on chaining these logical operators correctly. Getting their precedence and behavior with NULL wrong is one of the most common sources of silently incorrect query results, so this lesson covers not just the syntax but exactly how the database engine evaluates these expressions.
Overview: How AND, OR, and NOT Work
SQL’s WHERE clause filters rows by evaluating a boolean expression once per row. AND, OR, and NOT are logical operators that combine simpler comparisons (like salary > 90000 or country = 'UK') into a single boolean expression:
- AND — the combined expression is true only if every condition joined by
ANDis true. If any one is false, the row is excluded. - OR — the combined expression is true if at least one condition joined by
ORis true. Only if every condition is false is the row excluded. - NOT — a unary operator that reverses the truth value of the condition that follows it:
NOT TRUEbecomesFALSE, andNOT FALSEbecomesTRUE.
Internally, the query engine evaluates the WHERE expression row by row (logically, right after the row has been assembled from FROM/JOIN), reducing it down to TRUE, FALSE, or — critically — NULL (unknown). Only rows where the final result is TRUE are kept; both FALSE and NULL rows are dropped. That third possible outcome, NULL, is what causes most of the surprises with NOT, and we’ll dig into it below.
Syntax
SELECT column1, column2, ...
FROM table_name
WHERE condition1 AND condition2;
SELECT column1, column2, ...
FROM table_name
WHERE condition1 OR condition2;
SELECT column1, column2, ...
FROM table_name
WHERE NOT condition;
| Operator | Meaning | Row kept when… |
|---|---|---|
AND |
Logical conjunction | ALL joined conditions are true |
OR |
Logical disjunction | AT LEAST ONE joined condition is true |
NOT |
Logical negation | The following condition evaluates to false |
You can chain any number of conditions with AND and OR, and you can freely mix them — but as soon as you mix AND and OR in the same clause, parentheses become essential (see Under the Hood below).
Examples
1. Using AND
Sample employees table: 6 rows across Engineering, Sales, and Marketing.
SELECT name, department, salary
FROM employees
WHERE department = 'Engineering' AND salary > 90000;
Result:
| name | department | salary |
|---|---|---|
| Alice | Engineering | 95000 |
| Eve | Engineering | 102000 |
Both conditions must hold for a row to appear. Carol is also in Engineering but earns 88000, which fails the salary > 90000 test, so she is excluded even though her department matches.
2. Using OR
SELECT name, department
FROM employees
WHERE department = 'Sales' OR department = 'Marketing';
Result:
| name | department |
|---|---|
| Bob | Sales |
| Dave | Marketing |
| Frank | Sales |
Only one of the two conditions needs to be true per row, so anyone in either department is returned.
3. Using NOT
SELECT name, department, salary
FROM employees
WHERE NOT (salary > 80000);
Result:
| name | department | salary |
|---|---|---|
| Bob | Sales | 62000 |
| Dave | Marketing | 54000 |
| Frank | Sales | 71000 |
NOT (salary > 80000) keeps every row where the inner condition is false — here, everyone earning 80000 or less. This is logically equivalent to salary <= 80000, and in practice <= or <> is often clearer than wrapping a comparison in NOT, but NOT becomes essential once conditions get more complex (e.g. negating an entire multi-part expression, or negating IN, LIKE, or EXISTS).
4. Combining AND and OR with parentheses
SELECT name, department, salary, hire_date
FROM employees
WHERE department = 'Engineering'
AND (salary > 90000 OR hire_date < '2020-01-01');
Result:
| name | department | salary | hire_date |
|---|---|---|---|
| Alice | Engineering | 95000 | 2021-03-15 |
| Eve | Engineering | 102000 | 2018-05-30 |
This finds Engineering staff who are either high earners or long-tenured (hired before 2020). Carol is Engineering but neither earns over 90000 nor was hired before 2020, so she’s filtered out. The parentheses around the OR are what make this “Engineering AND (high salary OR long tenure)” instead of something else entirely — see the precedence trap in Common Mistakes.
How It Works Step by Step (Query Processing Order)
Logically, a query executes in this order, and AND/OR/NOT only come into play in the WHERE (and HAVING) step:
- FROM — identify the source table(s) and perform any joins, producing a working set of rows.
- WHERE — evaluate the boolean expression (built from
AND,OR,NOT, and comparisons) against each row; keep only rows that evaluate toTRUE. - GROUP BY — bucket the remaining rows, if grouping is used.
- HAVING — filter groups, which can also use
AND/OR/NOTagainst aggregated values. - SELECT — compute the output columns for the surviving rows.
- ORDER BY / LIMIT — sort and cap the final result.
Within a single WHERE expression, the query planner (or, for a simple table scan, the row-by-row evaluator) works inside-out: it first resolves the innermost comparisons (salary > 90000, hire_date < '2020-01-01') to TRUE/FALSE/NULL, then applies NOT to any negated sub-expression, then combines with AND, and finally with OR — unless parentheses override that order. If an indexed column is involved, the optimizer may use the index to satisfy an AND-ed equality condition first (narrowing the row set cheaply) before evaluating the remaining, more expensive conditions, but the logical result is identical regardless of which physical order the engine chooses.
Under the Hood: Three-Valued Logic and NULL
SQL doesn’t use ordinary two-valued (true/false) logic — it uses three-valued logic, because any comparison involving NULL produces NULL (unknown), not TRUE or FALSE. A row is only returned when the final expression evaluates to TRUE; both FALSE and NULL are treated as “don’t return this row.”
| A | B | A AND B | A OR B | NOT A |
|---|---|---|---|---|
| TRUE | TRUE | TRUE | TRUE | FALSE |
| TRUE | FALSE | FALSE | TRUE | FALSE |
| TRUE | NULL | NULL | TRUE | FALSE |
| FALSE | FALSE | FALSE | FALSE | TRUE |
| FALSE | NULL | FALSE | NULL | TRUE |
| NULL | NULL | NULL | NULL | NULL |
Notice the asymmetry: FALSE AND NULL is FALSE (one false condition is enough to kill an AND), but TRUE OR NULL is TRUE (one true condition is enough to satisfy an OR). This matters most when NOT meets a NULL: NOT NULL is NULL, not TRUE. That single fact is responsible for the classic NOT IN pitfall covered next.
On operator precedence: when NOT, AND, and OR appear together without parentheses, SQL evaluates NOT first, then AND, then OR — the same left-to-right, tightest-binds-first rule as most programming languages. This means a OR b AND c is parsed as a OR (b AND c), not (a OR b) AND c. Relying on this default order is a frequent source of bugs (see below) — always use explicit parentheses when mixing AND and OR so the query reads unambiguously and does what you intend.
Common Mistakes
Mistake 1: Forgetting to repeat the column name with OR
-- Wrong: intends "Engineering or Sales"
SELECT name, department
FROM employees
WHERE department = 'Engineering' OR 'Sales';
This runs without error, which makes it dangerous. 'Sales' on its own isn’t a comparison — it’s just a string literal, and in a boolean context a non-numeric string is treated as falsy (0). So the clause silently collapses to just department = 'Engineering', and every Sales row is missing from the result with no warning. Always write out the full comparison on both sides of OR:
-- Correct
SELECT name, department
FROM employees
WHERE department = 'Engineering' OR department = 'Sales';
Mistake 2: Mixing AND and OR without parentheses
-- Wrong: intends "(Sales or Marketing) employees earning over 65000"
SELECT name, department, salary
FROM employees
WHERE department = 'Sales' OR department = 'Marketing' AND salary > 65000;
Because AND binds tighter than OR, this is actually evaluated as department = 'Sales' OR (department = 'Marketing' AND salary > 65000). Every Sales row is returned regardless of salary — including Bob, who earns only 62000 and should have been filtered out. Fix it by grouping the department check explicitly:
-- Correct
SELECT name, department, salary
FROM employees
WHERE (department = 'Sales' OR department = 'Marketing') AND salary > 65000;
Mistake 3: NOT IN with a NULL in the list
-- Wrong: returns zero rows, even though Dave (Marketing) looks like it should match
SELECT name, department
FROM employees
WHERE department NOT IN (SELECT department FROM managed_departments);
If the subquery’s result set contains even one NULL (for example, a manager row with no assigned department), NOT IN silently returns an empty result set for every single row of the outer query — not an error, just nothing. This happens because x NOT IN (a, b, NULL) expands to NOT (x = a OR x = b OR x = NULL), and x = NULL always evaluates to NULL, which poisons the whole expression to NULL for every row that doesn’t already match a or b. Always exclude NULLs from the subquery, or use NOT EXISTS instead, which doesn’t have this trap:
-- Correct
SELECT name, department
FROM employees
WHERE department NOT IN (
SELECT department FROM managed_departments WHERE department IS NOT NULL
);
Best Practices
- Use parentheses any time
ANDandORappear in the sameWHEREclause, even when you’re confident about precedence — it removes all ambiguity for the next reader (including future you). - Prefer
<=,<>, or!=over wrapping a simple comparison inNOT (...)when a direct operator exists; reserveNOTfor negating compound expressions,IN,LIKE,EXISTS, orBETWEEN. - Never use
NOT INagainst a subquery or column that might containNULL— filter theNULLs out first or switch toNOT EXISTS. - Repeat the full comparison on each side of
OR(col = 'x' OR col = 'y') rather than assuming a bare value will be understood as a comparison. - For long lists of
OR-ed equality checks on the same column, useIN (...)instead — it’s shorter, clearer, and the optimizer handles it just as well. - When a query returns unexpectedly few or zero rows, suspect
NULLhandling inAND/OR/NOTlogic before assuming the data itself is missing.
Practice Exercises
- Exercise 1: Using the
employeestable from this lesson, write a query that returns employees who are in Engineering and earn more than 85000. - Exercise 2: Write a query that returns employees who are in Sales or were hired before 2020-01-01, making sure your parentheses (if any) reflect exactly what you intend.
- Exercise 3: Write a query using
NOTthat returns every employee who is not in Marketing. Then rewrite it using<>instead ofNOTand confirm both return the same rows.
Summary
ANDrequires every joined condition to be true;ORrequires at least one to be true;NOTreverses a condition’s truth value.- Without parentheses,
NOTis evaluated first, thenAND, thenOR— mixingANDandORwithout parentheses is a common source of subtly wrong results. - SQL uses three-valued logic: any expression touching
NULLcan evaluate toNULL(unknown), andNULLrows are excluded fromWHEREresults just likeFALSErows. NOT INagainst a list containingNULLsilently returns zero rows — preferNOT EXISTSor filter outNULLs explicitly.- Always write full comparisons on both sides of
OR, and use parentheses generously to make intent unambiguous.
