Skip to main content

WHERE Clause

Learning Focus

Use this lesson to understand WHERE Clause with practical syntax and examples.

Concept Overview

Definition

The WHERE clause filters rows for SQL statements such as SELECT, UPDATE, and DELETE. It defines a condition that determines which rows are included in the operation.

In PostgreSQL, a WHERE condition is evaluated using three-valued logic: it can be TRUE, FALSE, or NULL (unknown). Only rows where the condition evaluates to TRUE are included.

Why is it important?

Without WHERE, you read or modify all rows in a table. That is a correctness risk (wrong results) and a safety risk (accidental bulk updates/deletes). A well-written WHERE clause gives you precision, prevents mistakes, and is often the difference between a fast query and a full table scan.

Where does it fit?

WHERE is used across SQL categories:

  • DQL: SELECT ... WHERE ... filters which rows you retrieve
  • DML: UPDATE ... WHERE ... and DELETE ... WHERE ... restrict which rows you change

It is also foundational for:

  • joins (filtering joined sets)
  • indexing (matching predicates to access paths)
  • aggregation pipelines (filtering input rows before grouping)

Syntax & Rules

Basic Syntax

SELECT column1, column2, ...
FROM table_name
WHERE condition;

WHERE in UPDATE / DELETE:

UPDATE table_name
SET column1 = value
WHERE condition;

DELETE FROM table_name
WHERE condition;

Available Operators and Patterns

CategoryOperator / PatternExample
Comparisons=, !=, <>, <, >, <=, >=salary >= 60000
LogicalAND, OR, NOT(a OR b) AND c
RangeBETWEEN ... AND ...price BETWEEN 10 AND 30
ListIN (...)status IN ('paid','pending')
NULLIS NULL, IS NOT NULLdeleted_at IS NULL
PatternLIKE, ILIKEemail ILIKE '%@company.com'
Set membershipEXISTS (...)EXISTS (SELECT 1 FROM ...)
NULL-safe compareIS DISTINCT FROMa IS DISTINCT FROM b

Key Rules and Considerations

  • Use single quotes for string literals: 'IT', '2026-03-05'.
  • Use double quotes only for identifiers (table/column names) when required; quoted identifiers become case-sensitive.
  • AND has higher precedence than OR. Use parentheses to make intent explicit.
  • NULL cannot be compared with = or !=. Use IS NULL / IS NOT NULL.
  • Filters should be index-friendly when performance matters; avoid wrapping filter columns in functions unless you have expression indexes.

Step-by-Step Examples

Example 1: Basic Filtering (Beginner)

Create data and filter by a simple equality predicate.

-- Sample data setup
CREATE TABLE employees (
employee_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
full_name text NOT NULL,
department text NOT NULL,
salary numeric(12,2) NOT NULL CHECK (salary >= 0),
hire_date date NOT NULL,
email text,
active boolean NOT NULL DEFAULT true
);

INSERT INTO employees (full_name, department, salary, hire_date, email, active) VALUES
('John Smith', 'IT', 65000.00, '2020-01-15', 'john.smith@company.com', true),
('Sarah Johnson', 'HR', 55000.00, '2019-03-22', 'sarah.johnson@company.com', true),
('Mike Davis', 'IT', 72000.00, '2021-06-10', 'mike.davis@company.com', true),
('Lisa Brown', 'Finance', 58000.00, '2020-09-05', 'lisa.brown@company.com', true),
('David Wilson', 'IT', 68000.00, '2022-02-18', NULL, true);

-- Query: employees in IT
SELECT employee_id, full_name, department, salary
FROM employees
WHERE department = 'IT'
ORDER BY employee_id;

Expected output:

 employee_id | full_name    | department | salary
-------------+--------------+------------+----------
1 | John Smith | IT | 65000.00
3 | Mike Davis | IT | 72000.00
5 | David Wilson | IT | 68000.00
(3 rows)

Explanation:

  • department = 'IT' keeps only rows where the department matches exactly.
  • The ORDER BY makes results stable for review.

Example 2: Numeric Comparisons and Multiple Conditions (Intermediate)

Filter by salary and status using AND.

-- Query: IT employees with salary > 60000
SELECT full_name, department, salary
FROM employees
WHERE department = 'IT'
AND salary > 60000
ORDER BY salary DESC;

Expected output:

 full_name    | department | salary
--------------+------------+----------
Mike Davis | IT | 72000.00
David Wilson | IT | 68000.00
John Smith | IT | 65000.00
(3 rows)

Explanation:

  • AND requires both conditions to be true.
  • This pattern is index-friendly when you have indexes on filtered columns.

Example 3: IN, BETWEEN, and Parentheses (Intermediate)

Use list and range filters together; demonstrate precedence.

-- Query: employees in IT or Finance with salary between 60000 and 70000
SELECT full_name, department, salary
FROM employees
WHERE (department IN ('IT', 'Finance'))
AND salary BETWEEN 60000 AND 70000
ORDER BY department, salary;

Expected output:

 full_name    | department | salary
--------------+------------+----------
Lisa Brown | Finance | 58000.00
John Smith | IT | 65000.00
David Wilson | IT | 68000.00
(3 rows)

Notes:

  • BETWEEN 60000 AND 70000 is inclusive on both ends.
  • Parentheses make the IN group explicit.

Example 4: Pattern Matching, Case Sensitivity, and NULLs (Advanced)

PostgreSQL supports ILIKE for case-insensitive pattern matching.

-- Query: employees with company email domain (case-insensitive)
SELECT full_name, email
FROM employees
WHERE email ILIKE '%@company.com'
ORDER BY full_name;

Expected output:

 full_name     | email
---------------+------------------------------
John Smith | john.smith@company.com
Lisa Brown | lisa.brown@company.com
Mike Davis | mike.davis@company.com
Sarah Johnson | sarah.johnson@company.com
(4 rows)

Now find employees with missing email values:

SELECT full_name
FROM employees
WHERE email IS NULL
ORDER BY full_name;

Expected output:

 full_name
-------------
David Wilson
(1 row)

Explanation:

  • ILIKE is PostgreSQL-specific and avoids manual LOWER() wrapping.
  • IS NULL is required for NULL checks.

Example 5: Production-Ready Filtering (Advanced)

Demonstrate safe time-window filtering and index-friendly predicates.

-- Orders table for realistic filtering
CREATE TABLE orders (
order_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL,
status text NOT NULL CHECK (status IN ('pending', 'paid', 'cancelled')),
amount numeric(12,2) NOT NULL CHECK (amount >= 0),
created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO orders (customer_id, status, amount, created_at) VALUES
(42, 'paid', 120.00, '2026-02-01 01:00:00+00'),
(42, 'paid', 90.00, '2026-02-15 12:00:00+00'),
(42, 'pending', 15.00, '2026-02-28 23:59:00+00'),
(42, 'paid', 199.99, '2026-03-01 00:00:00+00'),
(99, 'paid', 49.00, '2026-02-10 10:30:00+00');

-- Index designed for common filters
CREATE INDEX idx_orders_customer_created
ON orders (customer_id, created_at DESC);

-- Query: orders for customer 42 in February 2026 (half-open interval)
SELECT order_id, status, amount, created_at
FROM orders
WHERE customer_id = 42
AND created_at >= '2026-02-01'
AND created_at < '2026-03-01'
ORDER BY created_at;

Expected output:

 order_id | status  | amount | created_at
----------+---------+--------+---------------------------
1 | paid | 120.00 | 2026-02-01 01:00:00+00
2 | paid | 90.00 | 2026-02-15 12:00:00+00
3 | pending | 15.00 | 2026-02-28 23:59:00+00
(3 rows)

Explanation:

  • created_at >= start AND created_at < end avoids window overlaps.
  • Composite index matches the filter + order pattern.

Practical Use Cases

1. Customer Segmentation (Marketing)

Business problem: Target customers in a city and age range.

SELECT customer_id, full_name
FROM customers
WHERE city = 'Chicago'
AND age BETWEEN 25 AND 35;

2. Inventory Management (Operations)

Business problem: Identify low-stock items.

SELECT sku, product_name, quantity
FROM inventory
WHERE quantity < 10
ORDER BY quantity ASC;

3. Payroll Forecasting (Finance)

Business problem: Find employees in Engineering with high salaries.

SELECT employee_id, full_name, salary
FROM employees
WHERE department = 'Engineering'
AND salary > 80000;

4. Order Fulfillment Backlog (Support)

Business problem: Pull pending orders for a queue.

SELECT order_id, customer_id, created_at
FROM orders
WHERE status = 'pending'
ORDER BY created_at ASC
LIMIT 100;

5. Compliance Review (Risk)

Business problem: Find large transactions that are not approved.

SELECT transaction_id, amount, approved_at
FROM transactions
WHERE amount > 10000
AND approved_at IS NULL;

Common Mistakes & Troubleshooting

Mistake 1: Using = NULL

Wrong SQL:

SELECT *
FROM employees
WHERE email = NULL;

Bad outcome: returns zero rows (because email = NULL is never TRUE).

Fix:

SELECT *
FROM employees
WHERE email IS NULL;

Mistake 2: Incorrect string quoting

Wrong SQL:

SELECT *
FROM employees
WHERE department = "IT";

Error message: ERROR: column "IT" does not exist

Fix:

SELECT *
FROM employees
WHERE department = 'IT';

Mistake 3: AND/OR precedence confusion

Wrong SQL:

SELECT full_name, department, salary
FROM employees
WHERE department = 'IT'
OR department = 'HR'
AND salary > 60000;

Bad outcome: the query is interpreted as IT OR (HR AND salary > 60000).

Fix:

SELECT full_name, department, salary
FROM employees
WHERE (department = 'IT' OR department = 'HR')
AND salary > 60000;

Mistake 4: Killing index usage with functions

Wrong SQL:

SELECT *
FROM users
WHERE lower(email) = lower('dev@example.com');

Bad outcome: often forces a sequential scan if there is no expression index.

Fix (add expression index):

CREATE INDEX idx_users_email_lower ON users (lower(email));

Fix (use citext, if appropriate):

CREATE EXTENSION IF NOT EXISTS citext;
-- store email as citext to get case-insensitive comparisons

Debugging Tips

  1. Start by validating the WHERE clause with SELECT ... LIMIT 10.
  2. Inspect table structure and column names with \d table_name in psql.
  3. Add parentheses to remove ambiguity when mixing AND and OR.
  4. If performance is slow, run EXPLAIN (ANALYZE, BUFFERS) to confirm index usage.

Best Practices

✅ Always test a WHERE clause with SELECT before using it in UPDATE or DELETE.
✅ Always use parentheses when mixing AND and OR.
✅ Always use IS NULL / IS NOT NULL for NULL checks.
✅ Always filter timestamps with half-open ranges (>= start AND < end) to avoid overlap.
✅ Always index columns used in frequent filters and sorts.
❌ Avoid using DISTINCT as a band-aid for incorrect joins.
❌ Avoid leading-wildcard LIKE patterns on large tables without the right indexes.
❌ Avoid applying functions to filter columns unless you have expression indexes.

Hands-On Practice

Exercise 1 (Easy)

Task: Select all employees hired after 2020-01-01.

-- Write your query here

Solution:

SELECT employee_id, full_name, hire_date
FROM employees
WHERE hire_date > '2020-01-01'
ORDER BY hire_date;

Exercise 2 (Medium)

Task: Find employees whose name starts with S and are active.

-- Write your query here

Solution:

SELECT employee_id, full_name
FROM employees
WHERE full_name ILIKE 's%'
AND active = true
ORDER BY full_name;

Exercise 3 (Advanced)

Task: Select IT employees with salary between 60000 and 80000 hired in 2020 or later, and with company email domain.

-- Write your query here

Solution:

SELECT full_name, department, salary, hire_date, email
FROM employees
WHERE department = 'IT'
AND salary BETWEEN 60000 AND 80000
AND hire_date >= '2020-01-01'
AND email ILIKE '%@company.com';

Connection to Other Concepts

Related lessonWhy it matters
SELECT StatementWHERE filters which rows a SELECT returns
SELECT DISTINCTDISTINCT often pairs with WHERE to de-duplicate a filtered set
ORDER BYSorting filtered results requires explicit ordering
Filtering and ConditionsDeeper coverage of operators, patterns, and NULL-safe logic
Index StrategyIndexes determine whether WHERE predicates can be fast
Performance OptimizationEXPLAIN shows whether WHERE uses indexes or scans

Visual Learning Diagram

graph TD
A[SELECT Statement] --> B[WHERE Clause]
B --> C[Simple comparisons]
B --> D[AND / OR logic]
B --> E[IN / BETWEEN]
B --> F[LIKE / ILIKE]
B --> G[NULL handling]
B --> H[Index strategy]
H --> I[EXPLAIN and tuning]
B --> J[UPDATE / DELETE safety]

classDef allNodes fill:#3e3e3e,stroke:#ffffff,stroke-width:2px,color:#f5f5f5
classDef highlight fill:#3e3e3e,stroke:#ffffff,stroke-width:4px,color:#f5f5f5

class A,C,D,E,F,G,H,I,J allNodes
class B highlight

Common Pitfalls

PitfallConsequencePrevention
Omitting WHERE in UPDATE/DELETEAccidental bulk data changesAlways preview with SELECT first and use transactions
Mixing AND/OR without parenthesesWrong rows matchedUse parentheses to make intent explicit
Comparing NULL with =Conditions never matchUse IS NULL / IS NOT NULL
Leading-wildcard pattern (%term%) on large tablesSlow scansUse trigrams/full-text where appropriate
Function-wrapped filters (lower(col) = ...) without indexIndex not usedUse expression index or citext

Quick Reference

SELECT * FROM t WHERE col = 1;
SELECT * FROM t WHERE col IN (1,2,3);
SELECT * FROM t WHERE col BETWEEN 10 AND 20;
SELECT * FROM t WHERE col IS NULL;
SELECT * FROM t WHERE name ILIKE 'a%';

What's Next