Filtering & Conditions
Filtering is how you tell the database "I only want rows that meet these criteria". The WHERE clause supports a rich set of operators for every type of comparison. This chapter covers them all, explains common pitfalls, and shows how to combine filters intelligently.
We will continue using the same employees table:
| emp_id | name | dept | salary | hire_date | manager_id | bonus |
|---|---|---|---|---|---|---|
| 1 | Alice | HR | 60000 | 2020-01-15 | 5 | 3000 |
| 2 | Bob | IT | 85000 | 2019-03-10 | 3 | NULL |
| 3 | Carol | IT | 92000 | 2018-07-22 | NULL | 5000 |
| 4 | David | HR | 55000 | 2021-09-01 | 1 | NULL |
| 5 | Eve | Finance | 78000 | 2020-11-30 | NULL | 4000 |
| 6 | Frank | IT | 95000 | 2017-05-14 | 3 | 6000 |
AND, OR, NOT โ Combining Conditions
Real filters usually combine multiple conditions. SQL provides three logical operators for this.
AND โ both conditions must be true
-- Only IT employees earning more than 85000
SELECT name, dept, salary
FROM employees
WHERE dept = 'IT'
AND salary > 85000;
Result:
| name | dept | salary |
|---|---|---|
| Carol | IT | 92000 |
| Frank | IT | 95000 |
OR โ at least one condition must be true
-- HR or Finance employees (either department)
SELECT name, dept
FROM employees
WHERE dept = 'HR'
OR dept = 'Finance';
Result:
| name | dept |
|---|---|
| Alice | HR |
| David | HR |
| Eve | Finance |
NOT โ reverses a condition
-- Everyone except IT employees
SELECT name, dept
FROM employees
WHERE NOT dept = 'IT';
-- This is equivalent to:
WHERE dept <> 'IT'
Operator Precedence โ The AND/OR Trap
This is one of the most common SQL bugs. AND has higher precedence than OR, just like multiplication has higher precedence than addition in maths. This means AND conditions are evaluated first, then OR conditions.
-- DANGEROUS: what does this actually filter?
SELECT name, dept, salary
FROM employees
WHERE dept = 'HR'
OR dept = 'Finance'
AND salary > 70000;
Because AND binds tighter, this is evaluated as:
dept = 'HR' OR (dept = 'Finance' AND salary > 70000)
Which means: all HR employees PLUS Finance employees earning above 70k.
That is probably not what was intended. The fix is always use parentheses when mixing AND and OR:
-- CORRECT: parentheses make the intent crystal clear
SELECT name, dept, salary
FROM employees
WHERE (dept = 'HR' OR dept = 'Finance')
AND salary > 70000;
Now it means: (HR or Finance) AND salary above 70000 โ two departments, filtered by salary.
BETWEEN โ Range Testing
BETWEEN checks whether a value falls within a range. It is inclusive โ meaning both the lower and upper bounds are included.
-- Salaries from 70000 to 90000 (includes 70000 and 90000 exactly)
SELECT name, salary
FROM employees
WHERE salary BETWEEN 70000 AND 90000;
Result:
| name | salary |
|---|---|
| Bob | 85000 |
| Eve | 78000 |
BETWEEN 70000 AND 90000 is exactly equivalent to salary >= 70000 AND salary <= 90000.
BETWEEN on Dates
-- Employees hired during 2019 or 2020
SELECT name, hire_date
FROM employees
WHERE hire_date BETWEEN '2019-01-01' AND '2020-12-31';
Result:
| name | hire_date |
|---|---|
| Bob | 2019-03-10 |
| Alice | 2020-01-15 |
| Eve | 2020-11-30 |
'2020-12-31' and timestamps include time, records at 2020-12-31 14:30:00 will be excluded because that is greater than midnight. Use '2020-12-31 23:59:59' or < '2021-01-01' to safely include the full last day.
BETWEEN on Strings
BETWEEN also works on text, comparing alphabetically:
-- Names between 'A' and 'D' alphabetically
SELECT name FROM employees
WHERE name BETWEEN 'A' AND 'D';
-- Returns: Alice, Bob, Carol, David
NOT BETWEEN
-- Salaries outside the 60000-90000 range
SELECT name, salary
FROM employees
WHERE salary NOT BETWEEN 60000 AND 90000;
Result: Frank (95000) and Carol (92000) โ above the range. David (55000) โ below the range.
IN โ Matching a List of Values
IN is a cleaner way to write multiple OR conditions. It checks whether a value matches any value in a list.
-- HR or Finance employees
SELECT name, dept
FROM employees
WHERE dept IN ('HR', 'Finance');
-- This is equivalent to:
WHERE dept = 'HR' OR dept = 'Finance'
IN is much more readable when the list is long:
-- Instead of: emp_id=1 OR emp_id=3 OR emp_id=5 OR emp_id=6
SELECT name
FROM employees
WHERE emp_id IN (1, 3, 5, 6);
NOT IN โ Excluding a List
-- Everyone except IT employees
SELECT name, dept
FROM employees
WHERE dept NOT IN ('IT');
-- More practically, exclude multiple values
SELECT name
FROM employees
WHERE emp_id NOT IN (2, 4);
IN with a Subquery
IN can check against the results of another query (called a subquery):
-- Employees in departments where the average salary exceeds 70000
SELECT name, dept
FROM employees
WHERE dept IN (
SELECT dept
FROM employees
GROUP BY dept
HAVING AVG(salary) > 70000
);
If the list in
NOT IN contains even one NULL value, the entire expression returns UNKNOWN and no rows are returned at all. This is one of the most dangerous SQL bugs.This happens because SQL cannot say "the value is definitely not in this list" when the list contains unknown (NULL) values โ any unknown item could potentially match.
-- Safe: the list contains no NULLs
SELECT name FROM employees WHERE dept NOT IN ('IT', 'HR');
-- Returns: Eve (Finance) โ works correctly
-- DANGEROUS: if any row in the subquery has a NULL bonus...
SELECT name FROM employees
WHERE bonus NOT IN (
SELECT bonus FROM employees -- this subquery may contain NULLs!
);
-- Returns: ZERO rows, even if many employees clearly have non-matching bonuses
-- SAFE fix: filter NULLs from the subquery
SELECT name FROM employees
WHERE bonus NOT IN (
SELECT bonus FROM employees WHERE bonus IS NOT NULL
);
-- BETTER fix: use NOT EXISTS instead
SELECT name FROM employees e1
WHERE NOT EXISTS (
SELECT 1 FROM employees e2
WHERE e2.bonus = e1.bonus
AND e2.emp_id <> e1.emp_id
);
LIKE โ Pattern Matching
LIKE lets you search for rows where a text column matches a pattern. It uses two wildcard characters:
%โ matches any sequence of zero or more characters_โ matches exactly one character
-- Names starting with 'A' (% after = anything after the A)
SELECT name FROM employees WHERE name LIKE 'A%';
-- Result: Alice
-- Names ending with 'e' (% before = anything before the e)
SELECT name FROM employees WHERE name LIKE '%e';
-- Result: Alice, Eve
-- Names containing 'ar' anywhere
SELECT name FROM employees WHERE name LIKE '%ar%';
-- Result: Carol
-- Names that are exactly 3 characters long
SELECT name FROM employees WHERE name LIKE '___';
-- Result: Bob, Eve (each _ is one character)
-- Second character is 'o'
SELECT name FROM employees WHERE name LIKE '_o%';
-- Result: Bob
Wildcard Pattern Reference
| Pattern | Meaning | Example matches |
|---|---|---|
'A%' |
Starts with A | Alice, Andrew, AB |
'%e' |
Ends with e | Alice, Eve, Joe |
'%ar%' |
Contains "ar" anywhere | Carol, Gary, Margaret |
'B__' |
B followed by exactly 2 characters | Bob, Ben |
'_o%' |
Second character is "o" | Bob, Joe, Tony |
'%2024%' |
Contains "2024" anywhere | Report_2024_final |
NOT LIKE
-- Names that do NOT start with A or B
SELECT name FROM employees WHERE name NOT LIKE 'A%' AND name NOT LIKE 'B%';
LIKE with ESCAPE โ Searching for Literal % or _
What if you need to search for an actual percent sign or underscore in the data (not as a wildcard)?
-- Find products where the name literally contains a % sign
-- Define \ as the escape character
SELECT name FROM products
WHERE name LIKE '%50\%%' ESCAPE '\';
-- Matches: "50% OFF", "Get 50% discount"
-- Find codes containing a literal underscore
SELECT code FROM products
WHERE code LIKE 'A\_%' ESCAPE '\';
-- Matches: "A_123", "A_XYZ"
LIKE '%value' cannot use a standard B-tree index, forcing the database to read every row in the table (a "full table scan"). On large tables this is very slow. If you need to search for values anywhere in a string frequently, consider a full-text search index instead.
IS NULL / IS NOT NULL โ Handling Missing Values
NULL in SQL means "unknown" or "missing". It is not zero, it is not an empty string, it is the absence of any value.
The critical rule: you cannot use = or <> to compare NULL.
Any comparison with NULL using = returns UNKNOWN (not TRUE or FALSE), and rows returning UNKNOWN are excluded just as if they returned FALSE.
-- WRONG โ this always returns no rows
SELECT name FROM employees WHERE bonus = NULL; -- never works!
SELECT name FROM employees WHERE bonus != NULL; -- never works!
-- CORRECT โ use IS NULL / IS NOT NULL
SELECT name FROM employees WHERE bonus IS NULL;
SELECT name FROM employees WHERE bonus IS NOT NULL;
-- Find employees with no bonus (bonus column is NULL)
SELECT name, dept
FROM employees
WHERE bonus IS NULL;
Result:
| name | dept |
|---|---|
| Bob | IT |
| David | HR |
-- Find employees who DO have a bonus
SELECT name, bonus
FROM employees
WHERE bonus IS NOT NULL
ORDER BY bonus DESC;
Result:
| name | bonus |
|---|---|
| Frank | 6000 |
| Carol | 5000 |
| Eve | 4000 |
| Alice | 3000 |
Why NULL Behaves This Way
Think of it this way: if someone asks "Is the number in this box equal to 5?" and the box is empty (NULL), you cannot say "yes" or "no" โ you simply do not know. SQL reflects this reality by returning UNKNOWN, not TRUE or FALSE.
-- SQL three-valued logic demonstration:
NULL = NULL -- UNKNOWN (not TRUE!)
NULL = 5 -- UNKNOWN
NULL IS NULL -- TRUE
5 IS NOT NULL -- TRUE
WHERE col = NULL looks reasonable but always returns zero rows. Always use IS NULL or IS NOT NULL.
COALESCE โ First Non-NULL Value
COALESCE takes a list of values and returns the first one that is not NULL. It is the standard way to replace NULL with a default value.
-- Replace NULL bonus with 0
SELECT name,
COALESCE(bonus, 0) AS bonus_display,
salary + COALESCE(bonus, 0) AS total_comp
FROM employees;
Result:
| name | bonus_display | total_comp |
|---|---|---|
| Alice | 3000 | 63000 |
| Bob | 0 | 85000 |
| Carol | 5000 | 97000 |
| David | 0 | 55000 |
| Eve | 4000 | 82000 |
| Frank | 6000 | 101000 |
-- COALESCE with multiple fallback values
-- Use department_email if available, else personal_email, else 'no email'
SELECT name,
COALESCE(department_email, personal_email, 'no email on file') AS contact_email
FROM employees;
COALESCE evaluates its arguments left to right and returns the first non-NULL value found. If all arguments are NULL, it returns NULL.
NVL โ Oracle's COALESCE Alternative
Oracle has a function called NVL which replaces NULL with a specified value. It only takes two arguments (unlike COALESCE which can take many).
-- Oracle: replace NULL with 0
SELECT name, NVL(bonus, 0) AS bonus
FROM employees;
-- NVL2: different values depending on whether the first arg is NULL
-- NVL2(expr, value_if_not_null, value_if_null)
SELECT name,
NVL2(bonus, 'Has bonus', 'No bonus') AS bonus_status
FROM employees;
COALESCE for new code โ it is the ANSI standard and works in all databases. Use NVL only when working with legacy Oracle code that already uses it, or when you need the two-arg version for compatibility with older Oracle versions.
NULLIF โ Return NULL When Two Values Are Equal
NULLIF(a, b) returns NULL if a equals b, otherwise returns a. It is the inverse of COALESCE in a sense.
Primary use case: preventing division by zero.
-- Without NULLIF: division by zero error if count = 0
SELECT total_sales / total_count AS avg_sale FROM report;
-- With NULLIF: returns NULL instead of error when count = 0
SELECT total_sales / NULLIF(total_count, 0) AS avg_sale FROM report;
-- Another use: treat a specific sentinel value as NULL
-- If dept is 'Unknown', return NULL instead
SELECT name,
NULLIF(dept, 'Unknown') AS dept_cleaned
FROM employees;
CASE in WHERE Clause
While CASE is typically used in SELECT, it can also appear in WHERE and ORDER BY:
-- Conditional filter using CASE
SELECT name, dept, salary
FROM employees
WHERE CASE
WHEN dept = 'IT' THEN salary > 80000
WHEN dept = 'Finance' THEN salary > 70000
ELSE salary > 55000
END;
This applies a different salary threshold depending on the department โ IT employees must earn more than 80000, Finance more than 70000, all others more than 55000.
Combining Multiple Filter Types โ Real-World Examples
Real queries combine many of these operators together:
-- Example 1: Employees hired in 2020, in IT or Finance, with a bonus
SELECT name, dept, salary, hire_date, bonus
FROM employees
WHERE hire_date BETWEEN '2020-01-01' AND '2020-12-31'
AND dept IN ('IT', 'Finance')
AND bonus IS NOT NULL
ORDER BY salary DESC;
-- Example 2: Not in HR, salary not between 55000 and 65000, name contains 'a'
SELECT name, dept, salary
FROM employees
WHERE dept NOT IN ('HR')
AND salary NOT BETWEEN 55000 AND 65000
AND LOWER(name) LIKE '%a%'
ORDER BY name;
-- Example 3: High earners with no bonus (potential compensation review)
SELECT name,
dept,
salary,
COALESCE(bonus, 0) AS bonus,
'Review needed' AS action
FROM employees
WHERE salary > 75000
AND bonus IS NULL;
Performance Tips โ Sargable vs Non-Sargable Predicates
A sargable predicate (Search ARGument ABLE) is one that the database can use an index to accelerate. A non-sargable predicate forces a full table scan.
Non-sargable (slow on large tables) โ avoid:
-- Function on the column prevents index use
WHERE UPPER(name) = 'ALICE'
WHERE YEAR(hire_date) = 2020
WHERE salary + 1000 > 80000
WHERE SUBSTR(dept, 1, 2) = 'IT'
-- Leading wildcard prevents index use
WHERE name LIKE '%lice'
Sargable (index-friendly) โ prefer:
-- No function on the indexed column
WHERE name = 'Alice' -- exact match
WHERE name = 'ALICE' OR name = 'alice' -- or use case-insensitive collation
WHERE hire_date >= '2020-01-01' -- range on date
AND hire_date < '2021-01-01'
WHERE salary > 79000 -- comparison without arithmetic on col
WHERE name LIKE 'Ali%' -- trailing wildcard is sargable
Quick Reference โ All Filter Operators
| Operator | Purpose | Example |
|---|---|---|
= |
Equals | dept = 'IT' |
<> / != |
Not equals | dept <> 'HR' |
>, <, >=, <= |
Comparisons | salary >= 80000 |
AND |
Both conditions true | a AND b |
OR |
Either condition true | a OR b |
NOT |
Negation | NOT dept = 'IT' |
BETWEEN x AND y |
Inclusive range | salary BETWEEN 50000 AND 90000 |
IN (list) |
Matches any in list | dept IN ('IT', 'HR') |
NOT IN (list) |
Matches none in list | dept NOT IN ('IT') |
LIKE 'pattern' |
Text pattern match | name LIKE 'A%' |
NOT LIKE 'pattern' |
Does not match pattern | name NOT LIKE 'B%' |
IS NULL |
Value is missing | bonus IS NULL |
IS NOT NULL |
Value is present | bonus IS NOT NULL |
COALESCE(a,b,c) |
First non-NULL value | COALESCE(bonus, 0) |
NULLIF(a,b) |
NULL if a=b, else a | NULLIF(count, 0) |
Common Errors
| Error | Cause | Fix |
|---|---|---|
| ORA-01722 | Invalid number โ non-numeric string compared to a numeric column | Quote string literals; avoid implicit type conversion in WHERE |
| ORA-01830 | Date format picture ends before converting entire input string | Use TO_DATE('2024-01-15','YYYY-MM-DD') with a matching format mask |
| ORA-00907 | Missing right parenthesis โ unbalanced parentheses in complex WHERE clause | Count opening vs closing parentheses; use indentation to spot the mismatch |
| ORA-00936 | Missing expression โ trailing AND/OR with nothing after it | Check for incomplete boolean expressions: WHERE dept = 'IT' AND |
| ORA-00932 | Inconsistent datatypes โ comparing a DATE column to a plain string | Wrap with TO_DATE() or use a date literal: DATE '2024-01-15' |
| ORA-01843 | Not a valid month โ month string does not match the session NLS date format | Always use TO_DATE with explicit format rather than relying on NLS settings |
Interview Corner
column != NULL (or column <> NULL) never return any rows in Oracle?โถ Show answer
In SQL, NULL represents an unknown value. Any comparison of the form column = NULL, column != NULL, or column > NULL evaluates to UNKNOWN (the third truth value in SQL's three-valued logic), not TRUE or FALSE. Because WHERE only passes rows where the condition is TRUE, no rows are returned.
The correct operators are IS NULL and IS NOT NULL:
-- Wrong โ returns 0 rows even if commission_pct has NULLs
SELECT * FROM employees WHERE commission_pct != NULL;
-- Correct
SELECT * FROM employees WHERE commission_pct IS NOT NULL;
LIKE 'A%' and LIKE '%A%' from an index-usage perspective?โถ Show answer
LIKE 'A%' has a known prefix โ Oracle can use a B-tree index range scan to jump directly to entries starting with 'A' and stop at 'B'. This is efficient even on large tables.
LIKE '%A%' has a leading wildcard โ Oracle cannot use a standard B-tree index because it does not know where in the index to start. It falls back to a full table scan (or a full index scan if the index is smaller).
For production full-text search patterns, use Oracle Text (CONTAINS()) or a function-based index with UPPER() for case-insensitive prefix searches.
Related Topics
- Basic SQL โ SELECT fundamentals, column aliases, ORDER BY
- Aggregate Functions โ GROUP BY and HAVING for filtered aggregations
- Subqueries โ subqueries in WHERE for dynamic filtering
- Regular Expressions โ REGEXP_LIKE as a powerful alternative to LIKE