SQL Comparison Operators
Comparison operators evaluate two values and return TRUE, FALSE, or NULL. They’re the building blocks of WHERE clauses, HAVING clauses, and CASE WHEN expressions.
Equality and Inequality
-- Equal toWHERE status = 'active'WHERE quantity = 10WHERE created_at = '2025-06-15'
-- Not equal to (both forms work the same)WHERE status != 'cancelled'WHERE status <> 'cancelled'String comparison: case-sensitive in PostgreSQL, case-insensitive in MySQL by default.
-- PostgreSQL: 'Active' != 'active'WHERE LOWER(status) = 'active' -- case-insensitive in PostgreSQLWHERE status ILIKE 'active' -- alternative for stringsGreater Than and Less Than
-- Numeric comparisonsWHERE price > 100 -- strictly greater thanWHERE price >= 100 -- greater than or equal (includes 100)WHERE price < 500 -- strictly less thanWHERE price <= 499.99 -- less than or equal
-- Date comparisonsWHERE order_date > '2025-01-01'WHERE order_date >= '2025-01-01' -- includes Jan 1stWHERE created_at < NOW()
-- String comparisons (lexicographic/alphabetical)WHERE name > 'M' -- names starting with N or laterWHERE status < 'z' -- technically works but not idiomaticBETWEEN: Range Comparison
BETWEEN is shorthand for >= AND <= — both endpoints are inclusive:
WHERE price BETWEEN 50 AND 200-- Exactly equivalent to:WHERE price >= 50 AND price <= 200
-- Date range (includes both endpoints)WHERE order_date BETWEEN '2025-01-01' AND '2025-12-31'
-- NOT BETWEENWHERE price NOT BETWEEN 50 AND 200Caution with TIMESTAMP: BETWEEN '2025-01-01' AND '2025-01-31' includes only up to midnight on Jan 31 — it won’t catch orders placed during Jan 31. Better to use:
WHERE order_date >= '2025-01-01' AND order_date < '2025-02-01'LIKE and Wildcards
LIKE matches string patterns:
WHERE name LIKE 'A%' -- starts with 'A' (any number of chars after)WHERE name LIKE '%son' -- ends with 'son'WHERE name LIKE '%Smith%' -- contains 'Smith' anywhereWHERE code LIKE 'ABC-___' -- 'ABC-' followed by exactly 3 charactersWHERE email LIKE '__%@%.%' -- basic email pattern% = any number of characters (including zero)
_ = exactly one character
-- NOT LIKEWHERE email NOT LIKE '%@gmail.com'
-- ESCAPE: treat wildcard as literalWHERE filename LIKE '50\%_discount' ESCAPE '\'-- matches '50%_discount' literallyCase sensitivity: LIKE is case-sensitive in PostgreSQL. Use ILIKE for case-insensitive matching:
WHERE name ILIKE '%smith%' -- PostgreSQLWHERE LOWER(name) LIKE '%smith%' -- portable versionIS NULL and IS NOT NULL
NULL represents “unknown value” — not zero, not empty. Standard comparison operators never match NULL:
-- Correct: check for NULLWHERE phone IS NULLWHERE phone IS NOT NULL
-- Wrong: these never matchWHERE phone = NULL -- always FALSE (NULL = NULL is NULL, not TRUE)WHERE phone != NULL -- always FALSE-- Find rows with missing dataSELECT * FROM customers WHERE phone IS NULL;
-- Find rows with complete dataSELECT * FROM customersWHERE phone IS NOT NULL AND email IS NOT NULL;IN: Membership Test
IN checks whether a value appears in a list:
WHERE status IN ('active', 'pending', 'processing')
-- NOT INWHERE country NOT IN ('US', 'CA', 'MX')
-- IN with subqueryWHERE category_id IN (SELECT category_id FROM featured_categories)IN is equivalent to multiple OR conditions but more concise.
NULL behavior with NOT IN: If any value in the list is NULL, NOT IN returns no rows for all outer rows (because x != NULL is NULL). Avoid NOT IN with subqueries that might return NULLs — use NOT EXISTS instead.
Combining Comparison Operators
-- Range with exclusionWHERE price >= 50 AND price < 500 AND price != 99.99
-- Multiple conditionsWHERE category = 'electronics' AND price BETWEEN 100 AND 1000 AND status IN ('active', 'featured') AND last_sold_date IS NOT NULL
-- Negated conditionsWHERE NOT (status = 'cancelled' OR amount < 0)-- Same as:WHERE status != 'cancelled' AND amount >= 0Comparison Operators in CASE WHEN
SELECT product_id, name, price, CASE WHEN price > 1000 THEN 'Premium' WHEN price BETWEEN 100 AND 1000 THEN 'Mid-range' WHEN price < 100 THEN 'Budget' ELSE 'Unknown' END AS tierFROM products;Comparison Operators with Different Data Types
Type | = and != | > and < | BETWEEN | LIKE | IS NULL───────────────────────────────────────────────────────────Numbers | Yes | Yes | Yes | No | YesStrings | Yes | Yes* | Yes* | Yes | YesDates | Yes | Yes | Yes | No | YesTimestamps | Yes | Yes | Yes** | No | YesBoolean | Yes | No | No | No | YesNULL | Never | Never | Never | Never| IS (NOT) NULL
* String comparisons are alphabetical/lexicographic** See BETWEEN timestamp caution abovePractical Examples
Filter by date range:
SELECT * FROM ordersWHERE created_at >= '2025-01-01' AND created_at < '2025-07-01';Find records in a specific status with a minimum amount:
SELECT order_id, customer_id, amountFROM ordersWHERE status IN ('pending', 'processing') AND amount >= 100ORDER BY amount DESC;Exclude test accounts:
SELECT * FROM usersWHERE email NOT LIKE '%@test.com' AND email NOT LIKE '%@example.com' AND is_internal = FALSE;