Filter Rows with WHERE
Learn to filter rows using WHERE conditions in PostgreSQL. This lesson covers the core concept, practical steps, and common pitfalls to help you query data accurately.
Focus: filter rows using where conditions
Ever run a query and get back 10,000 rows when you only needed 12? That's the pain of unfiltered data — slow reads, noisy results, and debugging sessions that spiral. In this lesson, you'll learn to filter rows using WHERE conditions in PostgreSQL, turning blunt table scans into surgical queries. By the end, you'll write precise conditions using comparison operators, logical keywords, and pattern matching — and know what to do when things go sideways.
The problem this lesson solves
Databases are built to hold a lot of data, but returning everything is rarely what you need. Without filtering, every SELECT pulls the entire table, which wastes bandwidth, slows down your application, and makes it impossible to spot the record you actually care about. Worse, unfiltered queries can mask logical bugs (like duplicates or unexpected NULLs), and in high-traffic systems they degrade performance for everyone.
Consider a simple task: find all users who signed up last month. Without WHERE, you'd scan the whole users table and check dates in application code — brittle, slow, and plain wrong when hours cut off days. The WHERE clause solves this at the database level: it evaluates a condition for every row and returns only those that satisfy it. That's filtering rows using WHERE conditions.
Core concept / mental model
Think of a PostgreSQL table as a warehouse of boxes, each row a box with labeled fields. The WHERE clause is your filter sheet: you specify rules, and the database walks each box, compares its labels to your rules, and keeps only the boxes that match. Rows that don't meet the condition are skipped — they never make it into your result set.
Key terms
- WHERE clause: A keyword in a SQL statement that defines the filtering condition.
- Predicate: The conditional expression inside WHERE that evaluates to TRUE, FALSE, or NULL (we'll handle NULL later).
- Boolean logic: Combining predicates with AND, OR, NOT to express complex rules.
- Comparison operators:
=,<>,<,>,<=,>=for numeric and string comparisons. - Pattern matching: Using
LIKEandILIKEwith wildcards to match partial strings.
Pro tip: A WHERE clause filters rows, not groups. If you're tempted to filter after grouping, you'll need HAVING — but that's a later lesson. For now, remember: WHERE goes right after FROM, before GROUP BY or ORDER BY.
How it works step by step
When PostgreSQL executes a query with a WHERE clause, it follows a logical order (which you can observe with EXPLAIN, but that's a future lesson):
- Read the table: The database scans the table (or uses an index, if available) to access rows.
- Evaluate the predicate: For each row, it computes the WHERE expression. If the result is TRUE, the row is kept; if FALSE or NULL, it's discarded.
- Return matching rows: Only rows satisfying the condition are included in the result set.
Where can you use WHERE?
- SELECT: Filter rows from a table (the most common case).
- UPDATE: Specify which rows to change.
- DELETE: Specify which rows to remove.
In all three, the syntax is the same: ... WHERE condition.
Order of clauses matters
In a SELECT statement, the logical order is: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. The WHERE clause filters before grouping, so you can safely filter on raw columns even when you're also aggregating.
Hands-on walkthrough
Let's start with a sample table. Be sure to run these examples in your PostgreSQL environment (e.g., psql or pgAdmin).
Setup: create the products table
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT,
price NUMERIC(10,2),
in_stock BOOLEAN DEFAULT TRUE
);
INSERT INTO products (name, category, price, in_stock) VALUES
('Widget', 'Gadgets', 19.99, TRUE),
('Gadget', 'Gadgets', 29.99, FALSE),
('Super Widget', 'Gadgets', 49.99, TRUE),
('Gizmo', 'Tools', 9.99, TRUE),
('Hammer', 'Tools', 15.50, TRUE),
('Screwdriver', 'Tools', 7.25, FALSE);
Example 1: Filter with a basic comparison
SELECT name, price FROM products WHERE price < 20.00;
Expected output:
name | price
------------+-------
Widget | 19.99
Gizmo | 9.99
Hammer | 15.50
Screwdriver| 7.25
(4 rows)
Example 2: Combine multiple conditions with AND and OR
SELECT name, category, price
FROM products
WHERE category = 'Gadgets' AND price > 20.00;
Expected output:
name | category | price
------------+----------+-------
Gadget | Gadgets | 29.99
Super Widget| Gadgets | 49.99
(2 rows)
Now try OR — get products that are either in the Tools category OR in stock:
SELECT name, category, in_stock
FROM products
WHERE category = 'Tools' OR in_stock = TRUE;
Expected output: All rows except 'Gadget' (because it's not in Tools and not in stock).
Example 3: Pattern matching with LIKE
SELECT name FROM products WHERE name LIKE 'Widget%';
Expected output:
name
---------
Widget
Super Widget
(2 rows)
% matches zero or more characters. _ matches exactly one character.
Example 4: Filter with dates (common in real apps)
Create a simple orders table and filter by date range:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
product_id INT REFERENCES products(id),
qty INT,
order_date DATE
);
INSERT INTO orders (product_id, qty, order_date) VALUES
(1, 2, '2025-05-01'),
(2, 1, '2025-05-02'),
(3, 4, '2025-06-01'),
(4, 1, '2025-06-10');
-- Find all orders from May 2025
SELECT * FROM orders WHERE order_date >= '2025-05-01' AND order_date < '2025-06-01';
Expected output: Two rows (the first two orders).
Pro tip: For date ranges, always use
>= start AND < end+1 dayinstead ofBETWEENwhen you care about time components — it avoids missing records on the upper boundary.
Compare options / when to choose what
Different filtering tools in PostgreSQL serve different purposes. Here’s how they stack up:
| Technique | Use case | Example | Performance notes |
|---|---|---|---|
Comparison (=, <, >, etc.) |
Exact or ordered values on any data type | price > 20 |
Can use indexes, fast |
Logical (AND, OR, NOT) |
Combine multiple conditions | category = 'Tools' AND in_stock |
Combine multiple predicates; consider index usage |
Pattern matching (LIKE, ILIKE) |
Partial string matches | name LIKE 'Widget%' |
ILIKE is case-insensitive but slower; left-anchored patterns use indexes |
Range (BETWEEN, IN) |
Value falls in a range or set | price BETWEEN 10 AND 50 |
IN is like multiple =; BETWEEN is inclusive on both ends |
NULL checks (IS NULL, IS NOT NULL) |
Test for missing values | in_stock IS NULL |
Always use IS NULL, never = NULL |
When to choose what
- Use
=for exact matches, but be aware of case sensitivity (useILIKEorLOWER()for case-insensitive). - Use
ANDto narrow results (more conditions = fewer rows). - Use
ORto broaden results, but beware of performance pitfalls if columns aren’t indexed. - For ranges,
BETWEENis readable but inclusive;> AND <gives you more control over bounds. - Never use
= NULL— it always returns no rows. Always useIS NULLorIS NOT NULL.
Troubleshooting & edge cases
Even experienced developers hit these snags. Here’s how to fix them fast.
1. Results are empty when you expect rows
- Check for NULLs: comparisons with NULL produce NULL, which is neither TRUE nor FALSE. Use
IS NULLorIS NOT NULLexplicitly.
SELECT * FROM products WHERE in_stock = NULL; -- returns 0 rows, always
SELECT * FROM products WHERE in_stock IS NULL; -- correct
2. OR caveat
If you mix AND and OR, parentheses are critical:
SELECT * FROM products WHERE category = 'Gadgets' OR category = 'Tools' AND price > 10;
Without parentheses, AND binds tighter than OR, so this returns all Gadgets (even cheap ones) plus expensive Tools. Add parentheses to get the intended result:
SELECT * FROM products WHERE (category = 'Gadgets' OR category = 'Tools') AND price > 10;
3. LIKE special characters
If your search term contains % or _, escape them with a backslash:
SELECT * FROM products WHERE name LIKE '100\%' ESCAPE '\';
4. Performance traps
Applying functions to columns (e.g., LOWER(name) = 'widget') prevents index usage. Instead, store lowercase or use ILIKE for case-insensitive search. Also, leading wildcards (LIKE '%widget') force full scans.
5. Date boundaries
BETWEEN is inclusive: order_date BETWEEN '2025-05-01' AND '2025-05-31' misses orders on May 31 at midnight. Use >= and < next day for correctness with times.
What you learned & what's next
You’ve now mastered filter rows using WHERE conditions — the core skill for precise data retrieval. You can apply comparison operators, combine logic, use pattern matching, and handle NULLs safely. You’re also aware of performance pitfalls and common mistakes.
Your next lesson is likely sorting and limiting results (ORDER BY and LIMIT) to control output order and size — a natural next step after filtering. Or jump into aggregations with GROUP BY to summarize filtered datasets. Either way, you have the foundation to build more complex queries.
Keep practicing: try filtering the products table with your own conditions, experiment with IN and BETWEEN, and don’t forget to test NULL behavior. You’re well on your way to querying with precision.
Practice recap
Create your own table (e.g., employees with name, department, salary) and practice filtering with different conditions. Try combining AND/OR, use LIKE, and test NULL handling. Then challenge yourself: write a query that finds all employees in 'Sales' with salary over 50000, and another for names starting with 'J'.
Common mistakes
- Using
= NULLinstead ofIS NULL— comparisons with NULL always return NULL, so the query silently returns zero rows. - Forgetting parentheses when mixing AND and OR — AND binds before OR, which can filter differently than you intend.
- Applying functions to columns (e.g.,
LOWER(name)) in WHERE, which prevents index usage and slows queries on large tables. - Using
LIKE '%pattern'with a leading wildcard, which causes a full table scan instead of using an index. - Trusting
BETWEENwith timestamps — it's inclusive on both ends, so you might miss rows at the upper boundary.
Variations
- Use
ILIKEfor case-insensitive pattern matching instead of combiningLIKEwithLOWER(). - Use
IN (value1, value2, ...)as a shorthand for multiple=conditions with OR. - Consider
BETWEENfor inclusive ranges, but switch to>=and<for date/time precision.
Real-world use cases
- Filtering user records by signup date to build a 'new users' dashboard
- Retrieving orders where status = 'pending' and amount > 100 for manual review
- Selecting products with a name containing 'pro' (case-insensitive) for a search feature
Key takeaways
- WHERE filters rows before grouping; it comes after FROM to restrict the dataset.
- Use comparison operators, logical AND/OR, and pattern matching to express conditions.
- Always use IS NULL / IS NOT NULL for NULL checks — never = NULL.
- Parentheses control evaluation order when mixing AND and OR.
- Avoid functions on columns for index-friendly queries.
- Use
>=and<for date ranges to handle boundaries correctly.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.