Advanced WHERE Clause Filtering
Learning Objectives
- Master complex conditional logic with WHERE
- Optimize WHERE clauses for performance
- Handle edge cases and NULL values properly
- Use advanced pattern matching and regular expressions
- Implement dynamic filtering strategies
- Understand index usage in WHERE clauses
- Apply real-world filtering patterns
WHERE Clause Evaluation Order
Understanding how MySQL evaluates WHERE conditions is crucial for writing efficient queries! ⚡
Short-Circuit Evaluation
Complex Conditional Logic
graph TB
WHERE[WHERE Clause] --> AND[AND Logic]
WHERE --> OR[OR Logic]
WHERE --> NOT[NOT Logic]
WHERE --> XOR[XOR Logic]
AND --> NESTED1[Nested Conditions]
OR --> NESTED2[Nested Conditions]
NESTED1 --> PARENS[Use Parentheses!]
NESTED2 --> PARENS
style WHERE fill:#dcfce7
style AND fill:#dbeafe
style OR fill:#fef3c7
style NOT fill:#fee2e2
style XOR fill:#e9d5ff
style PARENS fill:#fce7f3,stroke:#ec4899,stroke-width:3px
Advanced Conditional Logic Examples
-- ========================================
-- COMPLEX AND/OR COMBINATIONS
-- ========================================
-- Wrong: Without parentheses (confusing precedence)
SELECT * FROM products
WHERE category = 'Electronics' OR category = 'Computers'
AND price < 500; -- AND has higher precedence!
-- Correct: With parentheses for clarity
SELECT * FROM products
WHERE (category = 'Electronics' OR category = 'Computers')
AND price < 500;
-- Multiple condition groups
SELECT * FROM orders
WHERE
(status = 'pending' AND priority = 'high')
OR (status = 'processing' AND created_at < DATE_SUB(NOW(), INTERVAL 2 DAY))
OR (customer_type = 'VIP' AND total_amount > 1000);
-- ========================================
-- XOR (Exclusive OR) - One or the other, not both
-- ========================================
-- Find products that are either on sale OR featured, but not both
SELECT * FROM products
WHERE is_on_sale XOR is_featured;
-- Manual XOR implementation
SELECT * FROM products
WHERE (is_on_sale = TRUE AND is_featured = FALSE)
OR (is_on_sale = FALSE AND is_featured = TRUE);
-- ========================================
-- NESTED CONDITIONS WITH SUBQUERIES
-- ========================================
SELECT * FROM customers
WHERE
customer_id IN (
SELECT customer_id FROM orders
WHERE total_amount > 500
)
AND NOT EXISTS (
SELECT 1 FROM complaints
WHERE complaints.customer_id = customers.customer_id
)
AND (
loyalty_points > 1000
OR registration_date < DATE_SUB(NOW(), INTERVAL 2 YEAR)
);
-- ========================================
-- DYNAMIC CONDITION BUILDING
-- ========================================
-- Simulating optional filters (useful for application logic)
SELECT * FROM products
WHERE 1=1 -- Always true, allows easy AND additions
AND (@category IS NULL OR category = @category)
AND (@min_price IS NULL OR price >= @min_price)
AND (@max_price IS NULL OR price <= @max_price)
AND (@search IS NULL OR name LIKE CONCAT('%', @search, '%'));
-- Using COALESCE for optional parameters
SELECT * FROM products
WHERE category = COALESCE(@category, category)
AND price >= COALESCE(@min_price, 0)
AND price <= COALESCE(@max_price, 999999);
-- ========================================
-- COMPLEX DATE FILTERING
-- ========================================
-- Business hours filter (weekdays 9-5)
SELECT * FROM orders
WHERE
DAYOFWEEK(order_date) BETWEEN 2 AND 6 -- Monday to Friday
AND HOUR(order_time) BETWEEN 9 AND 17;
-- Seasonal filtering
SELECT * FROM sales
WHERE
(MONTH(sale_date) IN (12, 1, 2) AND product_category = 'Winter Clothing')
OR (MONTH(sale_date) IN (6, 7, 8) AND product_category = 'Summer Clothing');
-- Relative date ranges with complex logic
SELECT * FROM events
WHERE
(
-- Future events in next 30 days
event_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY)
)
OR
(
-- Recurring annual events
MONTH(event_date) = MONTH(CURDATE())
AND DAY(event_date) BETWEEN DAY(CURDATE()) AND DAY(DATE_ADD(CURDATE(), INTERVAL 7 DAY))
);
-- ========================================
-- BITWISE OPERATIONS FOR FLAGS
-- ========================================
-- Permission flags using bitwise operations
-- 1 = Read, 2 = Write, 4 = Delete, 8 = Admin
SELECT * FROM users
WHERE permissions & 1 = 1; -- Has read permission
SELECT * FROM users
WHERE permissions & 6 = 6; -- Has both write AND delete (2 + 4)
SELECT * FROM users
WHERE permissions & 8 = 8; -- Is admin
-- Multiple permission check
SELECT
username,
CASE
WHEN permissions & 8 = 8 THEN 'Admin'
WHEN permissions & 6 = 6 THEN 'Editor'
WHEN permissions & 1 = 1 THEN 'Viewer'
ELSE 'No Access'
END AS access_level
FROM users;
Advanced Pattern Matching
Advanced Pattern Matching
-- ========================================
-- ADVANCED LIKE PATTERNS
-- ========================================
-- Case-sensitive LIKE (using BINARY)
SELECT * FROM users
WHERE BINARY username LIKE 'John%'; -- Only 'John', not 'john'
-- Multiple LIKE conditions
SELECT * FROM products
WHERE name LIKE '%laptop%'
OR name LIKE '%notebook%'
OR name LIKE '%computer%';
-- LIKE with ESCAPE for special characters
SELECT * FROM files
WHERE filename LIKE '%\_%' ESCAPE '\\'; -- Files with underscore
-- Negated patterns
SELECT * FROM emails
WHERE address NOT LIKE '%@gmail.com'
AND address NOT LIKE '%@yahoo.com'
AND address NOT LIKE '%@hotmail.com';
-- ========================================
-- REGULAR EXPRESSIONS (REGEXP/RLIKE)
-- ========================================
-- Email validation
SELECT * FROM users
WHERE email REGEXP '^[A-Za-z0-9][A-Za-z0-9._%+-]{0,63}@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,}$';
-- Phone number formats (multiple patterns)
SELECT * FROM contacts
WHERE phone REGEXP '^(\\+1|1)?[-. ]?\\(?[0-9]{3}\\)?[-. ]?[0-9]{3}[-. ]?[0-9]{4}$';
-- URL validation
SELECT * FROM links
WHERE url REGEXP '^https?://[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?\\.[a-zA-Z]{2,}(/.*)?$';
-- Credit card patterns
SELECT
card_number,
CASE
WHEN card_number REGEXP '^4[0-9]{12}(?:[0-9]{3})?$' THEN 'Visa'
WHEN card_number REGEXP '^5[1-5][0-9]{14}$' THEN 'MasterCard'
WHEN card_number REGEXP '^3[47][0-9]{13}$' THEN 'American Express'
WHEN card_number REGEXP '^6(?:011|5[0-9]{2})[0-9]{12}$' THEN 'Discover'
ELSE 'Unknown'
END AS card_type
FROM payment_methods;
-- Word boundaries
SELECT * FROM articles
WHERE content REGEXP '[[:<:]]MySQL[[:>:]]'; -- Whole word 'MySQL'
-- Multiple word search
SELECT * FROM posts
WHERE content REGEXP '(marketing|sales|revenue|growth)';
-- ========================================
-- SOUNDS LIKE (SOUNDEX)
-- ========================================
-- Find similar sounding names
SELECT * FROM customers
WHERE SOUNDEX(last_name) = SOUNDEX('Smith'); -- Finds Smith, Smythe, etc.
-- Find potential duplicates
SELECT
c1.customer_id,
c1.first_name,
c1.last_name,
c2.customer_id AS possible_duplicate_id,
c2.first_name AS dup_first_name,
c2.last_name AS dup_last_name
FROM customers c1
JOIN customers c2 ON
c1.customer_id < c2.customer_id
AND SOUNDEX(c1.last_name) = SOUNDEX(c2.last_name)
AND SOUNDEX(c1.first_name) = SOUNDEX(c2.first_name);
-- ========================================
-- FULL-TEXT SEARCH
-- ========================================
-- Create full-text index first
ALTER TABLE articles ADD FULLTEXT(title, content);
-- Natural language search
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('MySQL database' IN NATURAL LANGUAGE MODE);
-- Boolean mode search
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('+MySQL -Oracle' IN BOOLEAN MODE);
-- + means must have, - means must not have
-- With relevance scoring
SELECT
article_id,
title,
MATCH(title, content) AGAINST('web development PHP MySQL') AS relevance
FROM articles
WHERE MATCH(title, content) AGAINST('web development PHP MySQL')
ORDER BY relevance DESC;
-- Query expansion (finds related terms)
SELECT * FROM articles
WHERE MATCH(title, content)
AGAINST('database' WITH QUERY EXPANSION);
NULL Handling Strategies
NULL is Special
-- ========================================
-- NULL HANDLING TECHNIQUES
-- ========================================
-- Basic NULL checks
SELECT * FROM users WHERE middle_name IS NULL;
SELECT * FROM users WHERE middle_name IS NOT NULL;
-- NULL-safe equality operator <=>
SELECT * FROM users WHERE middle_name <=> NULL; -- Same as IS NULL
SELECT * FROM users WHERE middle_name <=> 'John'; -- NULL-safe equals
-- COALESCE - return first non-NULL value
SELECT
username,
COALESCE(phone, email, 'No contact') AS primary_contact
FROM users;
-- IFNULL - replace NULL with value
SELECT
product_name,
IFNULL(discount, 0) AS discount_amount
FROM products;
-- NULLIF - return NULL if values match
SELECT
username,
NULLIF(status, 'inactive') AS active_status -- NULL if inactive
FROM users;
-- ========================================
-- NULL IN CONDITIONS
-- ========================================
-- Three-valued logic with NULL
SELECT * FROM orders
WHERE
-- This handles NULL properly
(discount IS NULL OR discount < 10)
AND status = 'active';
-- NOT IN with NULL (tricky!)
-- This returns NO rows if subquery contains NULL
SELECT * FROM products
WHERE category_id NOT IN (
SELECT category_id FROM discontinued_categories
-- Add WHERE category_id IS NOT NULL to fix
);
-- Safe NOT IN
SELECT * FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM discontinued_categories dc
WHERE dc.category_id = p.category_id
);
-- ========================================
-- NULL ORDERING
-- ========================================
-- Control NULL position in ORDER BY
SELECT * FROM products
ORDER BY
discount IS NULL, -- NULLs last
discount DESC;
-- NULLs first
SELECT * FROM products
ORDER BY
discount IS NOT NULL,
discount DESC;
-- ========================================
-- AGGREGATE FUNCTIONS AND NULL
-- ========================================
-- Aggregates ignore NULL (except COUNT(*))
SELECT
COUNT(*) AS total_rows,
COUNT(phone) AS rows_with_phone,
AVG(age) AS avg_age_excluding_null,
AVG(IFNULL(age, 0)) AS avg_age_null_as_zero
FROM users;
-- GROUP BY with NULL
SELECT
IFNULL(category, 'Uncategorized') AS category_name,
COUNT(*) AS product_count
FROM products
GROUP BY category; -- NULL becomes its own group
Performance Optimization
WHERE Clause Performance Tips
-- ========================================
-- INDEX-FRIENDLY WHERE CLAUSES
-- ========================================
-- Bad: Function on indexed column
SELECT * FROM users
WHERE YEAR(created_at) = 2024; -- Can't use index
-- Good: Range condition
SELECT * FROM users
WHERE created_at >= '2024-01-01'
AND created_at < '2025-01-01'; -- Uses index
-- Bad: Leading wildcard
SELECT * FROM products
WHERE name LIKE '%laptop'; -- Full table scan
-- Good: Trailing wildcard
SELECT * FROM products
WHERE name LIKE 'laptop%'; -- Can use index
-- Bad: OR with different columns
SELECT * FROM users
WHERE username = 'john' OR email = 'john@example.com'; -- Hard to optimize
-- Good: UNION for OR conditions
SELECT * FROM users WHERE username = 'john'
UNION
SELECT * FROM users WHERE email = 'john@example.com';
-- ========================================
-- SARGABLE PREDICATES
-- ========================================
-- Non-SARGable (Search ARGument ABLE)
SELECT * FROM orders
WHERE total_amount + tax > 1000; -- Can't use index
-- SARGable
SELECT * FROM orders
WHERE total_amount > 1000 - tax; -- Can use index
-- Non-SARGable: Implicit conversion
SELECT * FROM users
WHERE CONCAT(first_name, ' ', last_name) = 'John Doe';
-- SARGable: Direct comparison
SELECT * FROM users
WHERE first_name = 'John' AND last_name = 'Doe';
-- ========================================
-- OPTIMIZE COMPLEX CONDITIONS
-- ========================================
-- Put most selective condition first
SELECT * FROM orders
WHERE
customer_id = 12345 -- Very selective
AND status = 'completed' -- Less selective
AND order_date >= '2024-01-01'; -- Least selective
-- Use covering index
CREATE INDEX idx_covering
ON orders(customer_id, status, order_date, total_amount);
SELECT customer_id, status, total_amount
FROM orders
WHERE customer_id = 12345
AND status = 'completed'; -- All from index
-- ========================================
-- EXPLAIN ANALYSIS
-- ========================================
-- Check query execution plan
EXPLAIN SELECT * FROM users WHERE email = 'user@example.com';
-- Extended explain
EXPLAIN EXTENDED
SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.status = 'pending';
-- JSON format for detailed analysis
EXPLAIN FORMAT=JSON
SELECT * FROM products
WHERE category_id = 5
AND price BETWEEN 100 AND 500;
-- ========================================
-- QUERY HINTS
-- ========================================
-- Force index usage
SELECT * FROM users USE INDEX (idx_email)
WHERE email = 'user@example.com';
-- Ignore specific index
SELECT * FROM users IGNORE INDEX (idx_created)
WHERE created_at > '2024-01-01';
-- Force index for ORDER BY
SELECT * FROM products
FORCE INDEX FOR ORDER BY (idx_price)
WHERE category_id = 5
ORDER BY price;
Practice Exercise
Complex Filtering Challenge
💡 Click for Solutions
-- 1. Orders placed on weekends with total > $500
SELECT * FROM orders
WHERE DAYOFWEEK(order_date) IN (1, 7) -- Sunday = 1, Saturday = 7
AND total_amount > 500;
-- 2. Gmail/Yahoo users registered last year
SELECT * FROM users
WHERE (email LIKE '%@gmail.com' OR email LIKE '%@yahoo.com')
AND YEAR(created_at) = YEAR(CURDATE()) - 1;
-- 3. Products with discount XOR featured
SELECT * FROM products
WHERE (discount_percent >= 20) XOR (is_featured = TRUE);
-- Alternative XOR
SELECT * FROM products
WHERE (discount_percent >= 20 AND is_featured = FALSE)
OR (discount_percent < 20 AND is_featured = TRUE);
-- 4. Find potential duplicate customers
SELECT
c1.*,
c2.customer_id AS potential_dup_id,
c2.first_name AS dup_first,
c2.last_name AS dup_last
FROM customers c1
JOIN customers c2 ON
c1.customer_id < c2.customer_id
AND SOUNDEX(c1.first_name) = SOUNDEX(c2.first_name)
AND SOUNDEX(c1.last_name) = SOUNDEX(c2.last_name);
-- 5. Orders with NULL discount or discount < 10%
SELECT * FROM orders
WHERE discount_percent IS NULL
OR discount_percent < 10;
-- Using COALESCE
SELECT * FROM orders
WHERE COALESCE(discount_percent, 0) < 10;
-- 6. Find variations of "McDonald"
SELECT * FROM customers
WHERE last_name REGEXP '^M[cC]?[dD]onald[s\'']?$'
OR SOUNDEX(last_name) = SOUNDEX('McDonald');
-- 7. Valid phone numbers (multiple formats)
SELECT * FROM contacts
WHERE phone REGEXP '^(\\+?1[-. ]?)?\\(?[0-9]{3}\\)?[-. ]?[0-9]{3}[-. ]?[0-9]{4}$'
OR phone REGEXP '^[0-9]{10}$' -- Simple 10 digits
OR phone REGEXP '^[0-9]{3}-[0-9]{3}-[0-9]{4}$'; -- 123-456-7890
-- 8. Products without images or descriptions
SELECT * FROM products
WHERE (image_url IS NULL OR image_url = '')
OR (description IS NULL OR TRIM(description) = '');
-- Alternative using COALESCE
SELECT * FROM products
WHERE COALESCE(TRIM(image_url), '') = ''
OR COALESCE(TRIM(description), '') = '';