SQLMentor // learn sql

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.

โš 
The rule: whenever you mix AND and OR in the same WHERE clause, always wrap the OR group in parentheses. Even if you think the precedence works in your favour โ€” add the parentheses anyway to make the code unambiguous to anyone reading it.

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
โš 
Be careful with TIMESTAMP columns and BETWEEN. If your upper bound is '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
);
โš 
The NOT IN + NULL Trap โ€” Critical to Understand

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"
โš 
A leading wildcard pattern like 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
โš 
A very common beginner mistake: 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;
โ„น
Use 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
โœ“
The rule of thumb: if you wrap a column in a function call in the WHERE clause, the database usually cannot use a regular index on that column. Move the function to the other side of the comparison, or store pre-computed values in a separate column with its own index.

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

IQ ยท Filtering
Why does 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;
IQ ยท Filtering
What is the difference between 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