DEV Community

Cover image for SQL Filtering: The Five Operators That Let You Ask Smarter Questions
Navas Herbert
Navas Herbert

Posted on

SQL Filtering: The Five Operators That Let You Ask Smarter Questions

Last week we covered the core SQL keywords - SELECT, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. Students could retrieve data, summarise it, sort it.

But there was a problem. Every WHERE clause we wrote used exactly one condition. price > 100. stock_level < 30. Clean, simple, single.

Real questions are never that simple.

"Show me all Dairy products that cost less than KES 70." That's two conditions. "Show me every product name that starts with the letter M." That's a pattern, not a value. "Show me products whose price is somewhere between 60 and 200 shillings." That's a range.

This session was about teaching how to ask those questions - and the five families of operators that make them possible.


The Dataset: Mama Nia's Duka

Same table we've been using all month - duka.duka_products. Mama Nia's shop. Products, categories, suppliers, prices, stock levels. A dataset that feels like a real place, which is exactly why the queries feel like real questions.

Five operator categories on the board before any code:

Category Operators
Comparison =, !=, >, <, >=, <=
Logical AND, OR, NOT
Range BETWEEN, NOT BETWEEN
Membership IN, NOT IN
Search LIKE, ILIKE

"By the end of today, you will use all five in the same query. Let's go one at a time."


Section 1: Comparison Operators - The Foundation

These are the ones students already half-know from mathematics. The only one that catches people is != - not equal to. The rest feel immediately natural.

Three examples, three different operators:

-- Example 1: Products above 150 shillings
SELECT product_name, price
FROM duka.duka_products
WHERE price > 150
ORDER BY price;

-- Example 2: Low on stock - less than or equal to 30 units
SELECT product_name, stock_level
FROM duka.duka_products
WHERE stock_level <= 30
ORDER BY stock_level;

-- Exact match
SELECT product_name
FROM duka.duka_products
WHERE price = 65.00;
Enter fullscreen mode Exit fullscreen mode

I told the class: "Comparison operators are the vocabulary of filtering. Everything else we learn today uses these as building blocks."

The one worth pausing on: >= and <=. In English, "products with stock level 30 or below" translates directly to stock_level <= 30. The boundary value - 30 - is included. Students who meet BETWEEN later in the session sometimes forget that < alone excludes the boundary. A small distinction with meaningful consequences in inventory and financial queries.


Section 2: Logical Operators - AND, OR, NOT

AND - Both Must Be True

-- Dairy products that cost more than 70 shillings
SELECT product_name, product_category, price
FROM duka.duka_products
WHERE product_category = 'Dairy' AND price > 70;
Enter fullscreen mode Exit fullscreen mode

AND is the strict filter. Both conditions must be true for a row to pass. Think of it as two bouncers at the door - the row has to get past both.

OR - At Least One Must Be True

-- Products that are either Beverages OR Household
SELECT product_name, product_category
FROM duka.duka_products
WHERE product_category = 'Beverages' OR product_category = 'Household';
Enter fullscreen mode Exit fullscreen mode

OR is more relaxed. One condition being true is enough. The row passes if it satisfies either condition - or both.

NOT - Negate Everything

-- Everything that is NOT in Grains & Cereals
SELECT product_name, product_category
FROM duka.duka_products
WHERE NOT product_category = 'Grains & Cereals'
ORDER BY product_category;
Enter fullscreen mode Exit fullscreen mode

NOT flips the result. Sometimes cleaner than writing the negative condition directly, especially when the positive condition is complex.


The Trap Nobody Sees Coming: AND Before OR

This is the moment of Section 2, and I engineer it deliberately every cohort.

PostgreSQL evaluates AND before OR - the same way multiplication happens before addition in mathematics. You learned BODMAS in school. SQL has its own version, and most beginners don't know it.

Here's the trap. Mama Nia wants Dairy products where price is either below KES 70 OR above KES 85:

-- WRONG — no parentheses
SELECT product_name, product_category, price
FROM duka.duka_products
WHERE product_category = 'Dairy' AND price < 70 OR price > 85;
Enter fullscreen mode Exit fullscreen mode

This looks right. It isn't. SQL reads it as:

-- What PostgreSQL actually runs:
WHERE (product_category = 'Dairy' AND price < 70) OR price > 85
Enter fullscreen mode Exit fullscreen mode

The AND binds first. The result: every product above KES 85 comes through — regardless of category - because the OR on the right has no category condition attached to it.

The fix is one pair of brackets:

-- CORRECT - parentheses group the OR together
SELECT product_name, product_category, price
FROM duka.duka_products
WHERE product_category = 'Dairy' AND (price < 70 OR price > 85);
Enter fullscreen mode Exit fullscreen mode

We ran both versions live and compared the row counts. Different numbers. Visibly, provably different. That proof lands better than any explanation.

I told the class: "Any query that mixes AND and OR - use brackets. Not because you are unsure of the precedence, but because the next person reading your query definitely isn't. Brackets are documentation."


Section 3: BETWEEN - Ranges Made Readable

Without BETWEEN, a range filter needs two conditions joined by AND:

-- The long way
SELECT product_name, price
FROM duka.duka_products
WHERE price >= 60 AND price <= 200
ORDER BY price;
Enter fullscreen mode Exit fullscreen mode

With BETWEEN:

-- The short way
SELECT product_name, price
FROM duka.duka_products
WHERE price BETWEEN 60 AND 200
ORDER BY price;
Enter fullscreen mode Exit fullscreen mode

Same result. The BETWEEN version reads more like English - "price between 60 and 200" — which is exactly what we mean.

BETWEEN also works in reverse:

-- Products with stock level in range 20–40
SELECT product_name, stock_level
FROM duka.duka_products
WHERE stock_level BETWEEN 20 AND 40
ORDER BY stock_level;

-- Flip it: products OUTSIDE the 20–40 range
SELECT product_name, stock_level
FROM duka.duka_products
WHERE stock_level NOT BETWEEN 20 AND 40
ORDER BY stock_level;
Enter fullscreen mode Exit fullscreen mode

Critical detail I repeat twice: BETWEEN is inclusive. BETWEEN 60 AND 200 includes products priced exactly 60 and exactly 200. If you want to exclude the boundaries, use > 60 AND < 200 instead.

This matters most in financial and date-range queries. A billing report BETWEEN two dates either includes or excludes those dates - and the answer matters. Know your boundaries.


Section 4: IN and NOT IN - Membership Checks

Without IN, filtering for multiple specific values means chaining OR:

-- The long way
SELECT product_name, product_category
FROM duka.duka_products
WHERE product_category = 'Dairy' OR product_category = 'Household';
Enter fullscreen mode Exit fullscreen mode

With IN:

-- The short way
SELECT product_name, product_category
FROM duka.duka_products
WHERE product_category IN('Dairy', 'Household');
Enter fullscreen mode Exit fullscreen mode

Same result. One tenth of the typing. Add a third category - just add it to the list. No restructuring the query.

NOT IN gives you the inverse. Mama Nia wants to see all products except those from two specific suppliers:

-- Products NOT from these two suppliers
SELECT product_name, supplier
FROM duka.duka_products
WHERE supplier NOT IN('Kenya Grain Millers', 'Brookside Dairy');
Enter fullscreen mode Exit fullscreen mode

IN also works on specific product names:

-- Check prices for specific products by name
SELECT product_name, price
FROM duka.duka_products
WHERE product_name IN('Sukari', 'Mkate', 'Maharagwe');
Enter fullscreen mode Exit fullscreen mode

One honest warning about NOT IN: it behaves unexpectedly when any value in the list is NULL. If the column being checked has NULL values, NOT IN may return zero rows - silently, with no error. For this dataset all our values are populated, so we noted the warning and moved on. Worth knowing before you hit it in a real project.


Section 5: LIKE and ILIKE - Pattern Matching

Sometimes you don't know the exact value. You know it starts with something, or contains something, or follows a certain shape. That's what LIKE and ILIKE are for.

Two wildcard characters:

Symbol Meaning
% Matches any number of characters (including zero)
_ Matches exactly one character


-- Products starting with 'M'
SELECT product_name
FROM duka.duka_products
WHERE product_name LIKE 'M%';

-- Products containing 'ai' anywhere in the name
SELECT product_name
FROM duka.duka_products
WHERE product_name LIKE '%ai%';

-- The _ wildcard - exactly one character before 'kate'
SELECT product_name
FROM duka.duka_products
WHERE product_name LIKE '_kate';

-- ILIKE - case-insensitive search
-- 'SUKARI' matches 'Sukari', 'SUKARI', 'sukari'
SELECT product_name
FROM duka.duka_products
WHERE product_name ILIKE 'SUKARI';
Enter fullscreen mode Exit fullscreen mode

LIKE is case-sensitive - LIKE 'sukari' won't match 'Sukari'. ILIKE is PostgreSQL's case-insensitive version.

The three % placement patterns, left on the board for the rest of the session:

LIKE 'M%'    →  starts with M
LIKE '%a'    →  ends with a
LIKE '%ai%'  →  contains 'ai' anywhere
Enter fullscreen mode Exit fullscreen mode

The _ underscore matches exactly one character. LIKE '_kate' matches 'Mkate' - one character then 'kate'. Useful for product codes with fixed-length formats.

Rule of thumb I gave the room: "If a human is typing the search term into any kind of input box, use ILIKE. They'll type 'SUKARI', 'sukari', 'Sukari' - and you want all of them to match."


All Five Together - The Session Closer

SELECT product_name, product_category, price, supplier, stock_level
FROM duka.duka_products
WHERE product_category IN('Dairy', 'Beverages')          -- membership
AND price BETWEEN 50 AND 200                              -- range
AND supplier ILIKE '%brook%'                              -- pattern
AND product_name NOT IN('Maziwa')                         -- exclusion
AND stock_level > 20                                      -- comparison
ORDER BY price;
Enter fullscreen mode Exit fullscreen mode

The Cheat Sheet: When to Use What

You want to… Use
Match an exact value =
Exclude a specific value !=
Both conditions required AND
Either condition is enough OR
Negate a condition NOT
Mix AND and OR Brackets — always
Number or date range BETWEEN
Exclude a range NOT BETWEEN
One of several specific values IN
Exclude several specific values NOT IN
Pattern match, case-sensitive LIKE
Pattern match, case-insensitive ILIKE

Practice Problems

Easy:

-- 1. Show all products where stock_level is between 25 and 60
-- 2. Show products from 'Dairy' OR 'Beverages' category
-- 3. Show product names that start with the letter 'S'
Enter fullscreen mode Exit fullscreen mode

Medium:

-- Mama Nia wants to review specific products:
-- Show 'Sukari', 'Mkate', and 'Maharagwe' only if price is above 50
-- Order by price descending

SELECT product_name, price
FROM duka.duka_products
WHERE product_name IN('Sukari', 'Mkate', 'Maharagwe')
AND price > 50
ORDER BY price DESC;
Enter fullscreen mode Exit fullscreen mode

Challenge:

-- Restock priority report:
-- Products NOT supplied by 'Kenya Grain Millers' or 'Brookside Dairy'
-- AND whose name contains the letter 'a' anywhere (case-insensitive)
-- AND whose stock level is NOT BETWEEN 30 AND 80
-- Order by stock_level ascending (most urgent first)
-- Your query here...
Enter fullscreen mode Exit fullscreen mode

What's Next: JOINs

Every query we have written so far has touched exactly one table. Next session, Mama Nia's duka gets a second table - and we start connecting them.

-- A preview
SELECT p.product_name, p.price, s.contact_person, s.phone
FROM duka_products p
JOIN suppliers s ON p.supplier_id = s.id
WHERE p.stock_level < 30;
Enter fullscreen mode Exit fullscreen mode

One query. Two tables. The database starts feeling like a real system. See you then.


Try It Yourself

Focus your practice on the AND + OR parentheses trap - write a query that gives the wrong answer without brackets, confirm the wrong row count, then fix it. That one exercise teaches more about SQL logic than a dozen clean examples.


Top comments (0)