PostgreSQL
Filtering with WHERE
The WHERE clause is where most of the real querying power lives. Basic comparisons like = and > only get you so far. PostgreSQL gives you a rich set of filtering operators that let you match ranges, lists, patterns, and null values with precision. This lesson covers every major WHERE operator in depth with real examples from the Dataplexa Store — by the end you will be able to express any filter condition you need.
BETWEEN — Filtering a Range
BETWEEN checks whether a value falls within a range, inclusive of both endpoints. It works on numbers, dates, and even text. It is cleaner and more readable than writing col >= x AND col <= y.
-- Products priced between $25 and $100 (inclusive)
SELECT name, price FROM products
WHERE price BETWEEN 25 AND 100
ORDER BY price;
-- Orders placed within a date range
SELECT id, customer_id, total_amount, order_date FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-06-30'
ORDER BY order_date;
-- Employees with salaries in a band
SELECT first_name, last_name, salary FROM employees
WHERE salary BETWEEN 50000 AND 80000
ORDER BY salary;
-- NOT BETWEEN — outside the range
SELECT name, price FROM products
WHERE price NOT BETWEEN 25 AND 100
ORDER BY price;
name | price
--------------+-------
Wireless Mouse | 29.99
Desk Lamp | 34.99
Monitor Arm | 49.99
Webcam HD | 79.99
(4 rows)
-- Orders in date range:
id | customer_id | total_amount | order_date
-----+-------------+--------------+------------
101 | 5 | 149.97 | 2024-01-14
102 | 12 | 89.98 | 2024-02-03
IN — Matching a List of Values
IN checks whether a column value matches any item in a list. It is much cleaner than chaining multiple OR conditions and works with text, numbers, and dates. You can also use NOT IN to exclude a list of values.
-- Customers from specific states
SELECT first_name, last_name, state FROM customers
WHERE state IN ('CA', 'NY', 'TX')
ORDER BY state, last_name;
-- Products in certain categories
SELECT name, category, price FROM products
WHERE category IN ('Electronics', 'Accessories')
ORDER BY category, price;
-- Orders with specific statuses
SELECT id, total_amount, status FROM orders
WHERE status IN ('pending', 'processing')
ORDER BY status;
-- NOT IN — exclude certain categories
SELECT name, category FROM products
WHERE category NOT IN ('Furniture', 'Electronics')
ORDER BY category;
first_name | last_name | state
------------+-----------+-------
Bob | Smith | CA
Alice | Morgan | NY
Carol | Davis | TX
(18 rows)
-- Electronics and Accessories:
name | category | price
--------------+-------------+-------
Monitor Arm | Accessories | 49.99
USB Hub | Electronics | 24.99
Webcam HD | Electronics | 79.99
LIKE and ILIKE — Pattern Matching
LIKE matches text against a pattern. The % wildcard matches any sequence of zero or more characters. The _ wildcard matches exactly one character. ILIKE does the same thing but case-insensitively — this is a PostgreSQL extension not found in all databases.
-- Products starting with 'Wire'
SELECT name, price FROM products
WHERE name LIKE 'Wire%';
-- Products with 'desk' anywhere in the name (case-insensitive)
SELECT name, price FROM products
WHERE name ILIKE '%desk%';
-- Customers whose email ends with '@example.com'
SELECT first_name, last_name, email FROM customers
WHERE email LIKE '%@example.com';
-- Customers whose last name is exactly 5 characters
SELECT first_name, last_name FROM customers
WHERE last_name LIKE '_____';
-- Employees in departments starting with 'En'
SELECT first_name, department FROM employees
WHERE department LIKE 'En%';
-- NOT LIKE — names that do NOT start with 'W'
SELECT name FROM products WHERE name NOT LIKE 'W%';
name | price
----------------+-------
Wireless Mouse | 29.99
-- ILIKE '%desk%':
name | price
----------------+--------
Standing Desk | 349.99
-- email LIKE '%@example.com':
first_name | last_name | email
------------+-----------+------------------------
Alice | Morgan | alice.morgan@example.com
Bob | Smith | bob.smith@example.com
IS NULL and IS NOT NULL
NULL in SQL means the absence of a value — unknown or not provided. You cannot compare NULL with = or != because NULL is not equal to anything, not even itself. The correct way to check for NULL is with IS NULL or IS NOT NULL.
-- Customers who have not provided a phone number
SELECT first_name, last_name, email FROM customers
WHERE phone IS NULL;
-- Orders that have no total amount recorded yet
SELECT id, customer_id, order_date FROM orders
WHERE total_amount IS NULL;
-- Employees who have a salary on record
SELECT first_name, last_name, salary FROM employees
WHERE salary IS NOT NULL
ORDER BY salary DESC;
-- Products with no category assigned
SELECT id, name FROM products
WHERE category IS NULL;
-- WRONG way to check for NULL — this never returns results
SELECT * FROM customers WHERE phone = NULL; -- always returns 0 rows
SELECT * FROM customers WHERE phone != NULL; -- also always 0 rows
first_name | last_name | email
------------+-----------+-------------------------
Carol | Davis | carol.davis@example.com
Emma | Wilson | emma.wilson@example.com
(31 rows)
-- WHERE phone = NULL — always empty:
first_name | last_name | email
------------+-----------+-------
(0 rows)
Combining Conditions — AND, OR, NOT
Real queries often need multiple conditions. AND requires all conditions to be true. OR requires at least one to be true. NOT inverts a condition. When mixing AND and OR, use parentheses to make the logic explicit — AND has higher precedence than OR, which can cause unexpected results without brackets.
-- Electronics products priced under $50 with stock available
SELECT name, price, stock_qty FROM products
WHERE category = 'Electronics'
AND price < 50
AND stock_qty > 0;
-- Orders that are either high value or priority
SELECT id, total_amount, status FROM orders
WHERE total_amount > 300
OR status = 'vip-pending';
-- The parentheses make OR logic clear and correct
-- Without them AND binds first, changing the meaning
SELECT name, price, category FROM products
WHERE (category = 'Electronics' OR category = 'Accessories')
AND price < 50;
-- Negate an entire expression with NOT
SELECT first_name, state FROM customers
WHERE NOT (state = 'CA' OR state = 'NY')
ORDER BY state;
name | price | stock_qty
--------------+-------+-----------
Wireless Mouse | 29.99 | 42
USB Hub | 24.99 | 100
-- Electronics or Accessories under $50:
name | price | category
--------------+-------+-------------
Monitor Arm | 49.99 | Accessories
USB Hub | 24.99 | Electronics
Wireless Mouse| 29.99 | Electronics
Filtering Dates — Practical Examples
Date filtering is one of the most common real-world uses of WHERE. PostgreSQL has rich date arithmetic built in — you can subtract intervals, use CURRENT_DATE, extract parts of dates, and more. These patterns come up constantly in reporting and analytics.
-- Orders placed today
SELECT id, customer_id, total_amount FROM orders
WHERE order_date = CURRENT_DATE;
-- Orders placed in the last 30 days
SELECT id, customer_id, total_amount, order_date FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'
ORDER BY order_date DESC;
-- Employees hired in 2023
SELECT first_name, last_name, hire_date FROM employees
WHERE hire_date BETWEEN '2023-01-01' AND '2023-12-31'
ORDER BY hire_date;
-- Customers who joined this year
SELECT first_name, last_name, joined_date FROM customers
WHERE EXTRACT(YEAR FROM joined_date) = EXTRACT(YEAR FROM CURRENT_DATE)
ORDER BY joined_date;
id | customer_id | total_amount | order_date
-----+-------------+--------------+------------
198 | 22 | 312.50 | 2024-05-28
199 | 8 | 74.99 | 2024-05-30
-- Hired in 2023:
first_name | last_name | hire_date
------------+-----------+------------
James | Carter | 2023-03-15
Sofia | Taylor | 2023-07-01
WHERE with Subquery
You can use a subquery inside WHERE to filter based on results from another table. This is a powerful and clean way to express conditions that span multiple tables without needing a JOIN.
-- Customers who have placed at least one order
SELECT first_name, last_name, email FROM customers
WHERE id IN (SELECT DISTINCT customer_id FROM orders);
-- Products that have never been ordered
SELECT name, price FROM products
WHERE id NOT IN (SELECT DISTINCT product_id FROM order_items);
-- Employees earning more than the average salary
SELECT first_name, last_name, salary FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC;
name | price
----------------+-------
Old Keyboard | 12.99
-- Above average salary:
first_name | last_name | salary
------------+-----------+----------
Sofia | Taylor | 92000.00
Marcus | Reed | 85000.00
WHERE Operator Quick Reference
| Operator | Use For | Example |
|---|---|---|
| BETWEEN x AND y | Range check (inclusive) | price BETWEEN 10 AND 50 |
| IN (list) | Match any value in a list | state IN ('CA','NY') |
| LIKE 'pattern' | Case-sensitive pattern match | name LIKE 'Wire%' |
| ILIKE 'pattern' | Case-insensitive pattern match | name ILIKE '%desk%' |
| IS NULL | Column has no value | phone IS NULL |
| IS NOT NULL | Column has a value | salary IS NOT NULL |
| AND | All conditions must be true | price < 50 AND stock > 0 |
| OR | At least one condition must be true | state = 'CA' OR state = 'NY' |
| NOT | Inverts a condition | NOT category = 'Furniture' |
🧪 Practice Questions
Write these queries against the Dataplexa Store database.
1. Write a query to find all products priced between $30 and $100.
2. What is the correct way to check whether a column contains no value in PostgreSQL?
3. Which operator matches a text pattern without caring about uppercase or lowercase?
4. Write a query to get all customers from California, New York, or Texas.
5. When mixing AND and OR in a WHERE clause, what should you always use to make the logic clear and avoid unexpected results?
🎯 Quiz — Test Your Understanding
Q1. You want to find all customers with no phone number recorded. Which condition is correct?
Q2. In a LIKE pattern, what is the difference between % and _ wildcards?
Q3. Which WHERE clause finds products priced from $50 to $200 inclusive?
Q4. You want Electronics or Furniture products that cost more than $100. Which WHERE clause is correct?
Q5. Which WHERE clause returns orders placed in the last 30 days?
Next up: Aggregate functions — COUNT, SUM, AVG, MIN, MAX and how to summarise your data.