SELECT, WHERE and NULL: Filtering and Query Order
Codevity Admin · 10/25/2026
Step 3 of 25 · SQL Interview Preparation for Beginners · Stage 2: Foundations
Codevity Admin · 10/25/2026
Before you can write good queries, you need to know the order in which SQL evaluates them and how NULL changes the rules. These two ideas explain most "why does my query return the wrong rows?" questions.
You write SELECT first, but the database processes the clauses in a different order.
| Step | Clause | What happens |
|---|---|---|
| 1 | FROM and JOIN | Pick the tables and combine them |
| 2 | WHERE | Filter individual rows |
| 3 | GROUP BY | Form groups |
| 4 | HAVING | Filter groups |
| 5 | SELECT | Compute the output columns and aliases |
| 6 | DISTINCT | Remove duplicate output rows |
| 7 | ORDER BY | Sort the result |
| 8 | TOP or OFFSET ... FETCH | Limit the number of rows |
This is the logical order. The optimizer may execute the query differently, but the result must be the same as if it followed these steps. Three consequences interviewers love to ask about:
SELECT can be used in ORDER BY (step 7) but not in WHERE (step 2).COUNT(*) in WHERE, because grouping has not happened yet. Use HAVING.WHERE either. Wrap it in a CTE or subquery.SELECT name, salary
FROM employees
WHERE department_id IN (1, 2)
AND salary BETWEEN 55000 AND 80000
AND name LIKE '%a';IN (1, 2) is shorthand for department_id = 1 OR department_id = 2.BETWEEN a AND b is inclusive at both ends.LIKE: % matches any number of characters, _ matches exactly one. SQL Server also supports character classes such as [A-C]%.AND binds tighter than OR. This surprises beginners:-- Means: (department 1 AND salary above 70000) OR department 2
WHERE department_id = 1 AND salary > 70000 OR department_id = 2
-- Usually what you meant
WHERE department_id IN (1, 2) AND salary > 70000Use parentheses whenever you mix the two.
NULL is not zero and not an empty string. It means the value is missing or unknown, and SQL uses three-valued logic: a condition can be TRUE, FALSE or UNKNOWN. WHERE keeps only rows where the condition is TRUE.
| Expression | Result |
|---|---|
NULL = NULL | UNKNOWN |
NULL <> 5 | UNKNOWN |
5 > NULL | UNKNOWN |
| TRUE | |
| FALSE | |
| TRUE | |
| UNKNOWN |
So this query returns nothing, even though Farid has no salary:
A subtler trap: also drops rows where is , because is . To keep them, write .
The same logic makes dangerous when the list contains a . See subqueries, EXISTS and CTEs.
| Function | What it does | Notes |
|---|---|---|
| Returns if is | T-SQL only. Two arguments. The result has the data type of the first argument | |
| Returns the first non- argument | ANSI standard. Any number of arguments. The result type follows data type precedence | |
| Returns if , otherwise | Useful to avoid dividing by zero: |
The "first argument's type" rule can silently truncate data:
replaces a with a value. does the opposite: it turns a value into .
A table is an unordered set of rows. Without , the order of the result is not guaranteed, even if it looks stable today. Always sort when the order matters, and add a tie-breaker so that equal values come out in a predictable order:
Where do s go? SQL Server treats as the lowest value, so it sorts first in ascending order. PostgreSQL treats it as the highest, so it sorts last. Check your database, or handle it explicitly.
keeps the first rows (add to also keep rows tied with the last one). is the ANSI-standard way to page. In SQL Server it requires . MySQL and PostgreSQL also offer .
If contains a time, this query misses most of the last day:
The reliable pattern is a half-open range: start on or after the first day, and strictly before the day after the last one. See filter by date range. For columns in SQL Server, write date literals as , which is read the same way under every language setting.
and without a clause change every row. Run the matching first, do the change inside a transaction, and check the row count. See safe update with a transaction.
NULL IS NULLUNKNOWN AND FALSEUNKNOWN OR TRUENOT UNKNOWNWHERE salary <> 60000salaryNULLNULL <> 60000UNKNOWNWHERE salary <> 60000 OR salary IS NULLNOT INNULLCOUNT(*)COUNT(column)NULLSUMAVGMINMAXNULLAVGNULLNULLNULL5 + NULLNULL'a' + NULLNULLCONCAT('a', NULL)NULLISNULL(a, b)baNULLCOALESCE(a, b, c, ...)NULLNULLIF(a, b)NULLa = batotal / NULLIF(count, 0)ISNULLNULLNULLIFNULLORDER BYNULLNULLTOP (n)nWITH TIESOFFSET ... FETCHORDER BYLIMIT n OFFSET morder_dateDATETIME'YYYYMMDD'UPDATEDELETEWHERESELECTWHERE salary <> 60000WHERE name LIKE '%a' AND department_id = 1 OR department_id = 2OFFSET ... FETCHSELECT * FROM employees WHERE salary = NULL; -- wrong: always UNKNOWN
SELECT * FROM employees WHERE salary IS NULL; -- correctDECLARE @s VARCHAR(3) = NULL;
SELECT ISNULL(@s, 'abcdef'); -- 'abc' (cut to VARCHAR(3))
SELECT COALESCE(@s, 'abcdef'); -- 'abcdef'SELECT name, salary
FROM employees
ORDER BY salary DESC, id;-- SQL Server: the three highest salaries
SELECT TOP (3) name, salary
FROM employees
ORDER BY salary DESC;
-- SQL Server 2012+ and PostgreSQL: page 2, three rows per page
SELECT name, salary
FROM employees
ORDER BY salary DESC, id
OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY;WHERE order_date BETWEEN '20261001' AND '20261031' -- misses 31 Oct after 00:00:00