Skip to main content

Course Progress

Loading...

Advanced WHERE Clause Filtering

Duration: 80 minutes
Module 3: Advanced 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
MySQL uses short-circuit evaluation:
  • For AND: If first condition is FALSE, second isn't evaluated
  • For OR: If first condition is TRUE, second isn't evaluated
  • Place most selective conditions first for better performance

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 represents unknown or missing data. Key points:
  • NULL != NULL (always false)
  • Use IS NULL or IS NOT NULL for comparisons
  • NULL in arithmetic = NULL
  • NULL OR TRUE = TRUE, NULL AND FALSE = FALSE
-- ========================================
-- 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
Write WHERE clauses for these scenarios:
  1. Find orders placed on weekends with total > $500
  2. Find users with gmail/yahoo email, registered last year
  3. Products with 20% discount OR featured (not both)
  4. Find potential duplicate customers (same name sound)
  5. Orders with NULL discount or discount < 10%
  6. Find all variations of "McDonald" (McDonald's, Mcdonald, etc)
  7. Valid phone numbers in multiple formats
  8. Products without images or descriptions
💡 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), '') = '';

Additional Resources