Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Beginner

Complex WHERE — Combining Conditions

Mastering brackets, operator precedence, AND vs OR logic, multi-layer filters, and building WHERE clauses that express any business rule precisely

10–14 min April 2026
Section 3 · Filtering & Logic
Filtering & Logic · 3 modulesModule 17

// Part 01

When Simple Conditions Are Not Enough

Module 06 taught you the WHERE clause with a single condition. Module 07 introduced AND, OR, and NOT. By now you have used them across dozens of queries. But real production queries combine all of these — often with four, five, or six conditions across multiple operators — and the way you structure those conditions determines whether your query is correct, readable, and maintainable.

This module is about mastering the WHERE clause at the level required for production analytics work. Not just making queries that return results — making queries that return exactly the right results, are immediately readable by anyone on your team, and do not break when data or requirements change.

The three skills this module builds:

01
Precedence mastery
Know exactly what the database evaluates first — and use parentheses to make the order explicit and correct.
02
Condition grouping
Group related conditions with parentheses to express multi-dimensional business logic in a single WHERE clause.
03
Readable structure
Format complex WHERE clauses so they read like natural language and are easy to audit, debug, and modify.

// Part 02

Precedence — The Exact Evaluation Order

SQL evaluates operators in a fixed precedence order — just like arithmetic. Understanding this order is not optional: it is the difference between a query that is correct and one that silently returns wrong data.

PriorityOperator / CategoryExamplesEvaluated
1st (highest)Parentheses(a OR b)Always first — overrides everything
2ndArithmetic+ - * / %Before comparisons
3rdComparisons= <> < > <= >= LIKE BETWEEN IN IS NULLProduce TRUE/FALSE/NULL
4thNOTNOT conditionNegates the immediately following condition
5thANDcond1 AND cond2Evaluated before OR
6th (lowest)ORcond1 OR cond2Evaluated last — loosest binding

The AND-before-OR rule in practice

This is the rule that causes the most production bugs. AND binds more tightly than OR. When you write A OR B AND C, the database reads it as A OR (B AND C) — not (A OR B) AND C. These produce completely different results.

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

Run both queries and compare the row counts. The first returns more rows — the extra rows are Gold customers from cities other than Seattle that should not appear. The second returns only what was intended.

⚠️ Important
The AND-before-OR precedence bug is a silent data quality issue — the query runs without error and returns results that look plausible. The only way to catch it is to understand precedence rules and verify row counts against expectations. Always use parentheses when mixing AND and OR.

// Part 03

Parentheses — The Tool for Explicit Grouping

Parentheses override all precedence rules. Anything inside parentheses is evaluated as a complete unit before being combined with conditions outside. They are the most important formatting tool for complex WHERE clauses — not just for correctness, but for readability.

Single level of grouping

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

Multiple levels of grouping

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

🎯 Pro Tip

A useful mental model: read your WHERE clause in English first, identify every "and" and "or" in the business rule, then map them to parenthesised groups. "I want Gold or Platinum customers from Seattle who joined after 2021" breaks down as: (Gold OR Platinum) AND Seattle AND joined after 2021. Write the parentheses to match the English groupings, not to match the left-to-right order of the words.

// Part 04

AND vs OR — Narrowing vs Broadening

The mental model for AND and OR is simple but important to internalise deeply for complex queries.

AND narrows. Every AND condition you add reduces the number of rows returned. A row must satisfy ALL AND conditions to be included. Adding more AND conditions can only keep or reduce the result — never increase it.

OR broadens. Every OR condition you add potentially increases the number of rows returned. A row needs to satisfy ANY ONE OR condition to be included. Adding more OR conditions can only keep or increase the result — never reduce it.

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

Using the narrowing/broadening model to debug

When your query returns too many rows, look for OR conditions that should be AND. When it returns too few rows, look for AND conditions that should be OR — or for missing parentheses that are causing an AND to steal from your OR group.

// Part 05

De Morgan's Laws — Rewriting NOT Conditions

De Morgan's Laws are logical identities that let you rewrite NOT expressions. They are useful when you want to simplify a complex NOT condition or convert between equivalent forms.

De Morgan's Laws in SQL
-- Law 1: NOT (A OR B) = NOT A AND NOT B
NOT (city = 'Seattle' OR city = 'New York')
-- is identical to:
city <> 'Seattle' AND city <> 'New York'

-- Law 2: NOT (A AND B) = NOT A OR NOT B
NOT (in_stock = true AND unit_price > 200)
-- is identical to:
in_stock = false OR unit_price <= 200

-- Practical use:
-- NOT IN is De Morgan applied: NOT IN (a, b, c) = <> a AND <> b AND <> c
-- That is why NOT IN with NULL fails (AND with NULL = NULL)
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

All three produce the same result. Choose whichever reads most clearly for the specific condition. NOT IN is usually the most readable for lists. NOT (A OR B) is clearest when you want to emphasise "not any of these." The De Morgan equivalent is useful when you want to write positive conditions rather than negative ones.

// Part 06

Translating Business Rules Into WHERE Clauses

The most valuable skill in this module is the ability to read a business requirement written in plain English and translate it directly into a correctly structured WHERE clause. Here is the systematic process.

Step 1 — identify the entities and their conditions

Read the requirement and underline every condition. Each condition becomes a comparison expression. The words "and", "or", and "not" become AND, OR, and NOT. The words "either...or", "any of", and "one of" indicate OR. The words "both", "all of", "must also" indicate AND.

Step 2 — identify the groupings

Look for conditions that belong together as a unit — connected by OR within a larger AND structure. These become parenthesised groups. Read the requirement in English and identify where the "and" applies to the whole phrase versus just the last part.

Step 3 — write the WHERE clause with explicit parentheses

Write each group in parentheses. Connect groups with AND or OR. Always use parentheses even when precedence would give the correct result — explicit is always better than implicit.

Worked examples

Business Rule 01

"Find all delivered orders paid by UPI OR Card, where the total is above ₹500, placed in January or February 2024."

Groups: (UPI OR Card) AND total > 500 AND (Jan OR Feb 2024)

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

Business Rule 02

"Find products that either: (a) are out of stock AND have a margin above 25%, OR (b) are in stock AND priced below ₹30."

Groups: (out-of-stock AND high-margin) OR (in-stock AND cheap)

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

Business Rule 03

"Find employees who are either Store Managers with salary above ₹50,000, OR Cashiers in Seattle stores, OR any employee hired before 2020 earning more than ₹60,000."

Three OR groups, each group has internal AND conditions

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

// Part 07

Formatting Complex WHERE Clauses for Readability

A WHERE clause that is logically correct but unreadable is a maintenance liability. Six months from now, a colleague — or you — will need to modify it, and an unreadable WHERE clause gets misread and broken. Professional SQL formatting makes complex conditions immediately scannable.

The conventions

Professional formatting for complex WHERE clauses
-- Convention 1: AND/OR at the START of each line
-- Makes it easy to comment out individual conditions
WHERE order_status = 'Delivered'
  AND payment_method = 'UPI'
  AND total_amount > 500

-- Convention 2: Indent grouped conditions
-- The indentation shows which conditions are inside which group
WHERE (order_status = 'Cancelled' OR order_status = 'Returned')
  AND total_amount > 300

-- Convention 3: Align opening parentheses with the operator before them
WHERE (city = 'Seattle' AND loyalty_tier = 'Platinum')
   OR (city = 'Austin' AND loyalty_tier = 'Gold')

-- Convention 4: Multi-line groups use consistent indentation
WHERE (
    (city = 'Seattle' AND loyalty_tier = 'Platinum')
    OR
    (city = 'Austin' AND loyalty_tier = 'Gold')
  )
  AND joined_date >= '2022-01-01'

-- Convention 5: Comment the business rule above the WHERE
-- "Active high-value delivered orders from metro stores"
WHERE order_status = 'Delivered'
  AND total_amount > 1000
  AND store_id IN ('ST001', 'ST002', 'ST005')

Before and after — the same logic, two readability levels

Before — correct but unreadable
SELECT * FROM orders WHERE order_status='Delivered' AND payment_method IN ('UPI','Card') AND total_amount>500 AND store_id IN ('ST001','ST005','ST008') OR order_status='Processing' AND total_amount>200 ORDER BY total_amount DESC;
After — same logic, professional formatting
-- Delivered high-value digital orders from key stores,
-- OR any processing order above ₹200
SELECT *
FROM orders
WHERE (
    order_status = 'Delivered'
    AND payment_method IN ('UPI', 'Card')
    AND total_amount > 500
    AND store_id IN ('ST001', 'ST005', 'ST008')
  )
  OR (
    order_status = 'Processing'
    AND total_amount > 200
  )
ORDER BY total_amount DESC;

The second version takes 5 seconds longer to write and saves 5 minutes every time someone reads or modifies it. In a shared codebase or data team, the formatted version is the only acceptable standard.

// Part 08

Debugging Complex WHERE Clauses

When a complex WHERE returns wrong results — too many rows, too few rows, or rows that should not be there — here is the systematic debugging process.

Step 1 — verify the row count expectation

Before you can say a result is wrong, you need to know what is right. Run COUNT(*) with a mental estimate: "I expect about 8 rows from this filter." If you get 15, the filter is too broad. If you get 2, it is too narrow.

Step 2 — test each condition group in isolation

Replace the full WHERE with each group independently and check how many rows each group returns. This identifies which group is misbehaving.

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

Step 3 — add SELECT columns to see what is being included

Remove any LIMIT and look at the actual rows being returned. Compare them against your expectation. A row that should not be there tells you which condition is too permissive.

Step 4 — add conditions one at a time

Start with the simplest version of the WHERE clause and add conditions one by one, checking the row count after each addition. The count should narrow monotonically as you add AND conditions. If it unexpectedly broadens, you have accidentally used OR when you meant AND.

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

🎯 Pro Tip

The fastest debugging technique for a complex WHERE: temporarily replace the SELECT columns with SELECT *, remove LIMIT, and scan the results for any row that should obviously not be there. A Gold customer from Delhi appearing in a "Platinum Seattle customers" query tells you immediately that the loyalty_tier condition or the city condition has a precedence bug. One wrong row is usually enough to identify the broken condition.

// Part 09

Short-Circuit Evaluation and Performance

SQL databases use short-circuit evaluation for WHERE conditions — once the result of an expression is determined, remaining conditions may not be evaluated. This has performance implications for complex WHERE clauses.

AND short-circuit — FALSE stops evaluation

For AND conditions, if any condition is FALSE, the entire AND expression is FALSE — remaining conditions do not need to be evaluated. The database typically evaluates the cheapest or most selective condition first (when conditions are independent), using query optimiser statistics to determine which condition eliminates the most rows.

OR short-circuit — TRUE stops evaluation

For OR conditions, if any condition is TRUE, the entire OR expression is TRUE — remaining conditions are skipped. Put the most commonly satisfied condition first in an OR chain.

Ordering conditions for performance

While the query optimiser handles most reordering automatically, these manual guidelines help when the optimiser cannot reorder (due to side effects or complex expressions):

Condition ordering for performance
-- For AND chains: put the most selective condition first
-- Most selective = eliminates the most rows
WHERE order_id = 1007          -- PK lookup: eliminates all but 1 row (BEST FIRST)
  AND order_status = 'Delivered' -- secondary condition on surviving row

-- For OR chains: put the most commonly satisfied condition first
-- Most common = TRUE for the most rows
WHERE order_status = 'Delivered'   -- ~60% of rows (most common → check first)
   OR order_status = 'Processing'  -- ~20% of rows
   OR order_status = 'Cancelled'   -- ~15% of rows
   OR order_status = 'Returned'    -- ~5% of rows (least common → check last)

-- Avoid expensive functions on the left side of AND
-- Put cheap comparisons first, expensive calculations after
WHERE customer_id = 42            -- index lookup, cheap (FIRST)
  AND ROUND(salary / 12.0, 2) > 5000  -- calculation, slightly more expensive (AFTER)
💡 Note
Modern query optimisers (PostgreSQL, MySQL) automatically reorder independent WHERE conditions for optimal performance based on table statistics. Manual ordering matters most for conditions that cannot be reordered — those with side effects, functions that cannot be pushed down, or when you are writing conditions the optimiser cannot statistically evaluate. When in doubt, write conditions in the order that is most readable, and use EXPLAIN ANALYZE to verify the actual execution plan.

// Part 10

Real Production WHERE Clauses

Here are complete, production-quality complex WHERE clauses with real business context.

Fraud detection — suspicious order pattern

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

Campaign targeting — multi-segment customer list

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

Inventory alert — multi-condition product filter

Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…

// Part 11

What This Looks Like at Work

You are an analyst at Venmo, India's largest UPI payments platform. The risk team sends a request: they need a list of transactions matching a specific suspicious pattern for manual review. The pattern has multiple conditions across three dimensions — amount, timing, and transaction type.

2:00 PM
Risk team requirement arrives
The risk manager describes the pattern in plain English: "We want transactions that are either: (1) UPI transactions above ₹50,000 at any merchant, (2) transactions between ₹10,000 and ₹50,000 where the same user made more than 3 transactions in a single day, or (3) any wallet transfer to a new account where the receiver joined in the last 30 days AND the amount is above ₹5,000."
2:20 PM
You translate the requirement
You identify three OR groups, each with internal AND conditions. You adapt this to the FreshCart schema for illustration.
Loading FreshCart DB…
Ctrl + Enter to run
Loading FreshCart database in your browser…
2:45 PM
Query verified and delivered
You verify each group in isolation before combining them — group 1 returns 3 rows, group 2 returns 4 rows, group 3 returns 6 rows. Combined with OR they produce 11 rows (some overlap). You add a CASE WHEN to flag which risk pattern triggered each row. The risk team gets an immediately actionable list with risk labels — no manual categorisation needed.

// Part 12

Interview Prep — 5 Questions With Complete Answers

Q: What is operator precedence in SQL WHERE clauses and why does it cause bugs?

Operator precedence determines the order in which SQL evaluates logical operators when multiple operators appear in the same WHERE clause without explicit parentheses. The order is: parentheses first (highest), then arithmetic operators, then comparison operators (=, <>, LIKE, BETWEEN, IN, IS NULL), then NOT, then AND, and finally OR (lowest). This means AND binds more tightly than OR.

The most common precedence bug arises when mixing AND and OR without parentheses. WHERE tier = 'Gold' OR tier = 'Platinum' AND city = 'Seattle' is evaluated as WHERE tier = 'Gold' OR (tier = 'Platinum' AND city = 'Seattle') — because AND is evaluated before OR. This returns all Gold customers from any city, plus Platinum customers only from Seattle. The intended query — Gold or Platinum customers from Seattle — requires explicit parentheses: WHERE (tier = 'Gold' OR tier = 'Platinum') AND city = 'Seattle'.

The bug is insidious because the query runs without error and returns results that look plausible — it just returns more rows than intended. Without knowing the exact expected count, the bug is invisible. The prevention is simple: always use explicit parentheses when mixing AND and OR in the same WHERE clause. Even when precedence would give the correct result, parentheses make the intent clear and prevent future bugs when conditions are modified.

Q: How do you translate a complex business rule into a correctly structured WHERE clause?

The systematic process: first, read the business rule in plain English and identify every condition. Each condition becomes a comparison expression. The words "and", "both", "must also" map to AND. The words "or", "either...or", "any of" map to OR. The word "not" maps to NOT.

Second, identify the groupings — which conditions belong together as a unit before being combined with others. In the rule "Gold or Platinum customers from Seattle who joined after 2021," the "from Seattle" and "joined after 2021" apply to the combined (Gold OR Platinum) group — not just to Platinum. The parenthesised structure is: (Gold OR Platinum) AND Seattle AND after 2021.

Third, write the WHERE clause with explicit parentheses matching the English groupings: WHERE (loyalty_tier = 'Gold' OR loyalty_tier = 'Platinum') AND city = 'Seattle' AND joined_date >= '2022-01-01'. Fourth, verify by running COUNT(*) with each condition group in isolation, then combined, and checking that each count makes sense. A quick sanity check: does each row in the result satisfy all the conditions described in the business rule? Sample 5 rows and verify manually. This four-step process — identify, group, write, verify — works for any business rule regardless of complexity.

Q: What are De Morgan's Laws and how are they useful in SQL?

De Morgan's Laws are logical identities that describe how NOT distributes over AND and OR. Law 1: NOT (A OR B) equals NOT A AND NOT B. Law 2: NOT (A AND B) equals NOT A OR NOT B. These identities mean you can always rewrite a NOT expression over a group by distributing the NOT into each sub-expression and flipping OR to AND (or AND to OR).

In SQL, De Morgan's Laws explain the equivalence between different ways of writing exclusion conditions. NOT IN (a, b, c) is exactly De Morgan applied: it expands to NOT (col = a OR col = b OR col = c), which by De Morgan equals col <> a AND col <> b AND col <> c. This also explains why NOT IN fails when the list contains NULL — the col <> NULL term always evaluates to NULL, making the entire AND expression NULL for every row.

Practical uses in SQL: simplifying double negatives (NOT (NOT condition) = condition), rewriting NOT IN as explicit AND chains for clarity or portability, and understanding why NOT with OR behaves differently than expected. When debugging a NOT condition that excludes more rows than expected, apply De Morgan to rewrite it in positive terms — the positive form is usually easier to reason about. WHERE NOT (status = 'Cancelled' OR status = 'Returned') rewritten as WHERE status <> 'Cancelled' AND status <> 'Returned' makes it immediately clear which values are excluded.

Q: How do you debug a WHERE clause that returns the wrong number of rows?

The systematic debugging process starts with establishing the expected count. Before declaring the result wrong, calculate what you expect: "This table has 30 orders. About 18 are delivered. Of those, maybe 8 were paid by UPI. Of those, maybe 4 were above ₹500." If the query returns 12 instead of 4, the filter is too broad.

Next, test each condition group in isolation. Replace the full WHERE with just the first group and run COUNT(*). Then test the second group alone. Then combine them. For AND chains, each added condition should reduce or maintain the count. If adding a condition increases the count, you accidentally wrote OR. For OR chains, each added alternative should increase or maintain the count. If adding an OR alternative reduces the count, you have a precedence issue where AND is stealing from your OR group.

The most effective visual debugging technique: SELECT * from the full query (remove LIMIT) and scan the results for any row that obviously should not be there. A Gold customer from Delhi in a "Platinum Seattle customers" result immediately identifies a missing parenthesis around the loyalty tier OR condition. One wrong row tells you more than a count discrepancy because you can see exactly which condition failed for that specific row. Once identified, fix the condition, rerun, and verify the count matches the expectation before closing the debug session.

Q: When should you split a complex WHERE into a subquery or CTE instead of keeping it in one WHERE clause?

A single WHERE clause is appropriate when all conditions apply directly to columns in the available tables, the conditions are logically parallel (all ANDs, or a flat list of ORs), and the number of conditions is manageable — typically fewer than six to eight conditions. Single-level WHERE clauses with good formatting remain readable and maintainable up to this complexity.

A subquery or CTE (WITH clause) is better when: you need to filter on the result of an aggregation (WHERE total_orders > 5 — requires counting first), the filter depends on a derived value that appears in multiple conditions (defining margin_pct once and using it in two conditions avoids repeating the expression), or the logic has two clearly distinct phases (first identify the qualifying customers, then get their orders) that would be clearer as separate steps.

A CTE specifically helps when the same complex condition needs to be reused — define it once in WITH and reference it cleanly in the main query. It also helps when the WHERE conditions are so numerous that even well-formatted parentheses become hard to follow. As a practical rule: if you cannot explain what your WHERE clause does in one sentence, consider whether a CTE would make the steps explicit and self-documenting. The goal is always a query that any analyst on your team can read and immediately understand — formatting, parentheses, and CTE decomposition are all tools toward that goal.

// Part 13

Errors You Will Hit — And Exactly Why They Happen

Query returns more rows than expected — Gold customers from other cities appear in a Seattle-only result

Cause: Operator precedence bug: AND evaluated before OR. WHERE tier = 'Gold' OR tier = 'Platinum' AND city = 'Seattle' evaluates as WHERE tier = 'Gold' OR (tier = 'Platinum' AND city = 'Seattle'). The AND only binds to the Platinum condition, not the Gold condition. Gold customers from any city are returned because there is no city restriction on the Gold side of the OR.

Fix: Add parentheses to group the OR conditions first: WHERE (tier = 'Gold' OR tier = 'Platinum') AND city = 'Seattle'. Run COUNT(*) with and without the parentheses to verify the row count difference matches your expectation. As a general rule: any time you mix AND and OR in the same WHERE clause, add parentheses around every OR group regardless of whether precedence would technically give the correct result.

Removing one condition causes the row count to increase — adding AND returns more rows than without it

Cause: You accidentally wrote OR when you meant AND, or you have a parenthesisation error where an AND is inside an OR group that makes it less restrictive than intended. For example: WHERE (status = 'Delivered' AND amount > 500) OR store_id = 'ST001' — removing the AND amount > 500 condition does not change the OR group's store_id = 'ST001' branch, so the overall count depends on what you removed from which branch.

Fix: Test each condition group in isolation to identify which group is behaving unexpectedly. For each group, run SELECT COUNT(*) FROM table WHERE [group] and compare to your mental model. Then verify the combined query's count is consistent with the isolated counts. If adding a condition to an AND chain increases the count, you have an OR hiding inside the chain — check for missing parentheses.

Complex WHERE is correct but nobody on the team understands it — causes a breaking change when modified

Cause: Poorly formatted WHERE clause with no parentheses, no line breaks, and no comments. When conditions are all on one line with mixed AND/OR, even the original author cannot reliably modify it without introducing a bug. This is a code quality issue, not a SQL error — but it causes real production incidents when someone modifies the condition incorrectly.

Fix: Reformat the WHERE clause: one condition per line, AND/OR at the start of each line, explicit parentheses around every OR group, and a comment above the WHERE block describing the business rule in plain English. If the conditions represent logically distinct segments, consider extracting them into a CTE (WITH clause) with descriptive names. A WHERE clause that takes 2 minutes to write correctly and 2 seconds to read is infinitely better than one that takes 30 seconds to write and 5 minutes to reason about.

NOT condition excludes rows that should be included — WHERE NOT (status = 'Cancelled' OR status = 'Returned') misses NULL status rows

Cause: NOT propagates NULL. Rows where order_status IS NULL evaluate as: NOT (NULL = 'Cancelled' OR NULL = 'Returned') = NOT (NULL OR NULL) = NOT NULL = NULL. The WHERE clause discards all NULL results, so rows with NULL status are silently excluded. They are neither Cancelled nor Returned — but they are also not included in the NOT result.

Fix: Add explicit OR IS NULL handling: WHERE (order_status NOT IN ('Cancelled', 'Returned') OR order_status IS NULL). Or use the De Morgan equivalent: WHERE order_status <> 'Cancelled' AND order_status <> 'Returned' OR order_status IS NULL. To diagnose: run SELECT COUNT(*) FROM orders WHERE order_status IS NULL — if this returns any rows, your NOT condition is excluding them silently.

Query is slow after adding multiple OR conditions — execution time jumps from 0.1s to 45s

Cause: OR conditions prevent index optimisation across the full condition. With a single condition WHERE status = 'Delivered', the database uses an index on status. With multiple OR conditions across different columns — WHERE status = 'Delivered' OR payment_method = 'UPI' OR store_id = 'ST001' — the database cannot use a single index to satisfy all three alternatives and may resort to a full table scan for each OR branch or the entire condition.

Fix: Several approaches depending on the data size: (1) UNION ALL: run each OR branch as a separate query and combine results — each sub-query can use its own index. SELECT * FROM orders WHERE status = 'Delivered' UNION ALL SELECT * FROM orders WHERE payment_method = 'UPI' UNION ALL SELECT * FROM orders WHERE store_id = 'ST001'. (2) Add a composite index that covers the most common combination. (3) Analyse with EXPLAIN ANALYZE to confirm which conditions are causing the scan and whether indexes exist on those columns. OR-based queries are inherently harder to optimise than AND-based queries — the query optimiser has fewer options.

Try It Yourself

The FreshCart operations director needs a priority order list with three tiers. Write a single query that returns orders matching ANY of these three conditions: (1) High-priority: Cancelled or Returned orders above ₹800 from stores ST001, ST005, or ST009. (2) Medium-priority: Processing orders older than order_id 1010 with UPI payment. (3) Low-priority: Delivered orders in January 2024 with total above ₹1,200. Show order_id, store_id, order_date, order_status, payment_method, total_amount, and a priority column using CASE WHEN. Sort by priority ascending then total_amount descending.

🎯 Key Takeaways

  • Operator precedence order: parentheses → arithmetic → comparisons → NOT → AND → OR. AND always evaluates before OR when no parentheses are present.
  • The most common production bug: WHERE tier = 'Gold' OR tier = 'Platinum' AND city = 'Seattle' is not "(Gold OR Platinum) AND Seattle" — AND runs first, making it "Gold (any city) OR (Platinum AND Seattle)."
  • Always use explicit parentheses when mixing AND and OR. Even when precedence gives the correct answer, parentheses communicate intent and prevent future bugs when conditions are modified.
  • AND narrows: every added AND can only reduce or maintain the result count. OR broadens: every added OR can only increase or maintain the result count. If adding AND increases the count, you have an OR where you meant AND.
  • De Morgan's Laws: NOT (A OR B) = NOT A AND NOT B. NOT (A AND B) = NOT A OR NOT B. Use these to rewrite NOT conditions into equivalent positive forms for clarity.
  • Translate business rules systematically: identify every condition, map "and/both/must" to AND and "or/either/any" to OR, identify which conditions group together, then write WITH explicit parentheses matching the English groupings.
  • Format WHERE clauses professionally: one condition per line, AND/OR at the start of each line, indented groups, comments above the WHERE block describing the business rule.
  • Debug by isolation: test each condition group alone with COUNT(*), then combine. For AND chains, count should decrease with each condition added. If it increases, you have an OR bug.
  • NOT conditions silently exclude NULL rows — any NULL in the column being compared evaluates the NOT condition to NULL, which WHERE discards. Add OR column IS NULL explicitly when NULL rows should be included in NOT results.
  • Multiple OR conditions across different columns can prevent index optimisation. If an OR-heavy query is slow, consider UNION ALL (each branch uses its own index) or restructure with EXISTS subqueries.

What comes next

In Module 18, you learn SQL data types — what types exist, which to use for each situation, and how wrong type choices cause silent bugs in calculations, comparisons, and joins.

Module 18 → SQL Data Types
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...