SQL Syllabus

Week 1 ยท SQL Basics / Day 2

Day 2: WHERE, every operator, NULL, LIKE

Wed 30 September 2026 ยท 44 queries ยท run on AdventureWorks2022

Goal: filter rows with any condition an interviewer could name, and never be surprised by NULL again.

Read: SQL Where, SQL And Or Not, SQL Null Values, SQL Like, SQL Wildcards, SQL In, SQL Between, SQL Operators.

On this page

  1. Drill 24
  2. Stretch 4
  3. Stretch (continued) 11
  4. Self-test 5

Drill

#1

1. Equality and not-equal, both spellings

SELECT Name, Color FROM Production.Product WHERE Color = 'Red';

What this query does

WHERE filters rows: only rows where the condition is true are returned. Color = 'Red' keeps the 38 red products. Text values go inside single quotes.

Result in SSMS: 38 rows
#2

1. Equality and not-equal, both spellings (continued)

SELECT Name, Color FROM Production.Product WHERE Color <> 'Red';

What this query does

<> means 'not equal to'. It returns every product whose colour is something other than red: 218 rows.

Result in SSMS: 218 rows
#3

1. Equality and not-equal, both spellings (continued)

SELECT Name, Color FROM Production.Product WHERE Color != 'Red';

What this query does

!= is the second spelling of 'not equal'. It gives exactly the same 218 rows as <>. Notice that 38 + 218 = 256, not 504: the 248 products whose Color is NULL appear in neither result, because comparing NULL with anything gives 'unknown', not true.

<> and != are the same operator and return the same row count. Neither one returns products whose Color is NULL.

Result in SSMS: 218 rows
#4

2. Comparison operators

SELECT Name, ListPrice FROM Production.Product WHERE ListPrice > 1000;

What this query does

> means strictly greater than. Products priced exactly 1000 would not be included.

Result in SSMS: 86 rows
#5

2. Comparison operators (continued)

SELECT Name, ListPrice FROM Production.Product WHERE ListPrice >= 1431.50;

What this query does

>= means greater than or equal to, so products priced exactly 1431.50 (the HL Road Frames) are included.

Result in SSMS: 55 rows
#6

2. Comparison operators (continued)

SELECT Name, ListPrice FROM Production.Product WHERE ListPrice < 10;

What this query does

< means strictly less than. Most of the 213 rows have ListPrice 0: they are components, not products for sale. That is why 'WHERE ListPrice > 0' is often the first filter you add.

Includes the components with ListPrice 0.

Result in SSMS: 213 rows
#7

2. Comparison operators (continued)

SELECT Name, ListPrice FROM Production.Product WHERE ListPrice <= 9.50;

What this query does

<= means less than or equal to. Products priced exactly 9.50 are included, products at 9.99 are not.

Result in SSMS: 212 rows
#8

3. BETWEEN is inclusive

SELECT Name, ListPrice FROM Production.Product WHERE ListPrice BETWEEN 500 AND 2000;

What this query does

BETWEEN a AND b selects a range. It is inclusive: products priced exactly 500 or exactly 2000 would be included.

Result in SSMS: 101 rows
#9

3. BETWEEN is inclusive (continued)

SELECT Name, ListPrice FROM Production.Product WHERE ListPrice >= 500 AND ListPrice <= 2000;

What this query does

The long way of writing the same range. It returns the same 101 rows as BETWEEN, proving that BETWEEN includes both ends. Always put the smaller value first: BETWEEN 2000 AND 500 returns nothing.

Same row count as BETWEEN, which proves BETWEEN includes both end values.

Result in SSMS: 101 rows
#10

4. OR then IN then NOT IN

SELECT Name, Color FROM Production.Product
WHERE Color = 'Red' OR Color = 'Black' OR Color = 'Silver';

What this query does

OR returns a row if any one of the conditions is true, so this returns products that are red, black or silver: 174 rows.

Result in SSMS: 174 rows
#11

4. OR then IN then NOT IN (continued)

SELECT Name, Color FROM Production.Product WHERE Color IN ('Red', 'Black', 'Silver');

What this query does

IN (list) is shorthand for a chain of ORs on the same column. It returns the same 174 rows and is easier to read and extend.

IN is a shorter way to write a list of ORs. Same row count.

Result in SSMS: 174 rows
#12

4. OR then IN then NOT IN (continued)

SELECT Name, Color FROM Production.Product WHERE Color NOT IN ('Red', 'Black', 'Silver');

What this query does

NOT IN returns rows whose value is not in the list: 82 rows. 174 + 82 = 256, so again the 248 NULL-colour products are missing. To include them you must add OR Color IS NULL.

NOT IN does not return the products whose Color is NULL.

Result in SSMS: 82 rows
#13

5. AND with a date

SELECT SalesOrderID, OrderDate, Status, TotalDue
FROM Sales.SalesOrderHeader
WHERE Status = 5 AND OrderDate > '2013-06-01';

What this query does

AND requires both conditions to be true. Status 5 means 'shipped'. Dates are written as text in 'YYYY-MM-DD' format and SQL Server converts them to dates automatically. Result: shipped orders placed after 1 June 2013.

Result in SSMS: 23,900 rows
#14

6. NULL the right way and the wrong way

SELECT Name, Color FROM Production.Product WHERE Color IS NULL;

What this query does

IS NULL is the correct way to find missing values: 248 products have no colour.

Result in SSMS: 248 rows
#15

6. NULL the right way and the wrong way (continued)

SELECT Name, Color FROM Production.Product WHERE Color IS NOT NULL;

What this query does

IS NOT NULL returns rows that do have a value: 256 rows. 248 + 256 = 504, every product.

Result in SSMS: 256 rows
#16

6. NULL the right way and the wrong way (continued)

SELECT Name, Color FROM Production.Product WHERE Color = NULL;  -- zero rows, always

What this query does

The wrong way. NULL means 'unknown', so Color = NULL asks 'is this colour equal to an unknown value?' The answer is unknown, never true, so no rows come back. There is no error message, which makes this mistake easy to miss.

NULL means unknown, so Color = NULL is never true. Always use IS NULL or IS NOT NULL.

Result in SSMS: 0 rows
#17

7. AND/OR precedence trap

SELECT Name, Color, ListPrice FROM Production.Product
WHERE Color = 'Red' AND ListPrice > 1000 OR Color = 'Black';

What this query does

No brackets. AND is evaluated before OR, so SQL Server reads this as (Red AND price > 1000) OR Black. Result: 113 rows, including cheap black products.

Result in SSMS: 113 rows
#18

7. AND/OR precedence trap (continued)

SELECT Name, Color, ListPrice FROM Production.Product
WHERE Color = 'Red' AND (ListPrice > 1000 OR Color = 'Black');

What this query does

Brackets around the OR change the meaning: the product must be red, and then either expensive or black. A red product can never be black, so this only returns expensive red products: 20 rows.

Result in SSMS: 20 rows
#19

7. AND/OR precedence trap (continued)

SELECT Name, Color, ListPrice FROM Production.Product
WHERE (Color = 'Red' AND ListPrice > 1000) OR Color = 'Black';

What this query does

Brackets around the AND make the default order explicit. Same 113 rows as the first query, which proves AND binds before OR. Always use brackets when you mix AND and OR, so the reader does not have to remember the rule.

The first and third queries return the same rows because AND binds before OR. The second query returns a different count. Always use brackets.

Result in SSMS: 113 rows
#20

8. LIKE starts with

SELECT Name FROM Production.Product WHERE Name LIKE 'Mountain%';

What this query does

LIKE does pattern matching on text. % means 'any number of characters, including none'. 'Mountain%' matches every name that starts with Mountain.

Result in SSMS: 38 rows
#21

9. LIKE contains, ends with

SELECT Name FROM Production.Product WHERE Name LIKE '%Frame%';

What this query does

A % on both sides means 'Frame appears anywhere in the name': 79 products.

Result in SSMS: 79 rows
#22

9. LIKE contains, ends with (continued)

SELECT Name FROM Production.Product WHERE Name LIKE '%Black, 42';

What this query does

A % only at the start means 'ends with'. This finds the 5 products whose name ends in 'Black, 42' (black, size 42).

Result in SSMS: 5 rows
#23

10. Exact length with underscores

SELECT DISTINCT FirstName FROM Person.Person WHERE FirstName LIKE '____';

What this query does

_ (underscore) matches exactly one character. Four underscores match names that are exactly four letters long. DISTINCT removes repeated names.

Each _ matches exactly one character, so this returns four-letter names.

Result in SSMS: 144 rows
#24

10. Exact length with underscores (continued)

SELECT DISTINCT FirstName FROM Person.Person WHERE FirstName LIKE 'A___';

What this query does

Combines a fixed letter with underscores: names that start with A and are exactly four letters long (Adam, Alex, Anna...).

Result in SSMS: 13 rows

Stretch

#25

1. Always true, always false

SELECT COUNT(*) FROM Production.Product WHERE 1 = 1;  -- always true

What this query does

1 = 1 is true for every row, so the filter keeps everything: 504. Developers often start dynamic queries with WHERE 1 = 1 so every real condition can be added as 'AND ...'.

Result in SSMS: 1 row (the count)
#26

1. Always true, always false (continued)

SELECT COUNT(*) FROM Production.Product WHERE 1 = 0;  -- always false

What this query does

1 = 0 is never true, so no rows pass and the count is 0. A common trick to get a table's column structure without any data.

Result in SSMS: 1 row (the count)
#27

2. Case insensitivity. Lowercase still matches.

SELECT Name FROM Production.Product WHERE Color = 'red';

What this query does

Lowercase 'red' still finds the 38 'Red' products, because the database comparison rules (the collation) ignore case.

Result in SSMS: 38 rows
#28

2. Case insensitivity. Lowercase still matches. (continued)

SELECT SERVERPROPERTY('Collation');  -- CI = case-insensitive, AS = accent-sensitive

What this query does

SERVERPROPERTY('Collation') shows the server's default comparison rules: SQL_Latin1_General_CP1_CI_AS. CI = case-insensitive ('red' = 'Red'). AS = accent-sensitive ('e' and 'รฉ' are different).

CI in the result means case-insensitive, so 'red' matches 'Red'. AS means accent-sensitive, so 'e' and 'รฉ' are treated as different characters.

Result in SSMS: 1 row

Stretch (continued)

#29

3. Implicit conversion. First works, second errors.

SELECT Name FROM Production.Product WHERE ListPrice > '1000';

What this query does

'1000' is text, but ListPrice is a number (money). SQL Server converts the text to a number automatically (implicit conversion) and the query works.

Result in SSMS: 86 rows
#30

3. Implicit conversion. First works, second errors. (continued)

SELECT Name FROM Production.Product WHERE Name > 100;

What this query does

Now the comparison goes the other way. Numbers have higher priority than text, so SQL Server tries to convert every Name into an integer. 'Adjustable Race' is not a number, so the query fails with error 245 (conversion failed). Always compare a column with a value of the same type.

Error 245: SQL Server tries to turn every Name into a number and fails.

Result in SSMS: Error (expected): Conversion failed when converting the nvarchar value 'Adjustable Race' to data type int.
#31

4. Bracket wildcards, T-SQL only

SELECT Name FROM Production.Product WHERE Name LIKE '[HM]%';

What this query does

[HM] matches one character that is either H or M. So this returns names starting with H or M. Square-bracket wildcards are a SQL Server (T-SQL) feature; standard SQL does not have them.

Result in SSMS: 193 rows
#32

4. Bracket wildcards, T-SQL only (continued)

SELECT Name FROM Production.Product WHERE Name LIKE '[^A-M]%';

What this query does

[A-M] is a range of letters and ^ inside the brackets means NOT. So this returns names whose first letter is not between A and M, i.e. names starting with N to Z (or a digit or symbol).

Result in SSMS: 145 rows
#33

4. Bracket wildcards, T-SQL only (continued)

SELECT Name FROM Production.Product WHERE Name LIKE '%[0-9]%';

What this query does

[0-9] matches any single digit. With % on both sides this returns every product name that contains at least one number, such as sizes (58, 44) or model numbers (Road-150).

Result in SSMS: 316 rows
#34

5. Escape a literal underscore

SELECT Name FROM Production.Product WHERE Name LIKE '%\_%' ESCAPE '\';

What this query does

_ is normally a wildcard. To search for a real underscore you 'escape' it: ESCAPE '\' tells SQL Server that a character after \ should be taken literally. No product names contain an underscore, so the correct answer here is 0 rows. Without the escape, '%_%' would match every name with at least one character.

Result in SSMS: 0 rows
#35

6. Date columns with a time part

SELECT TOP 5 ModifiedDate FROM Sales.SalesOrderHeader WHERE ModifiedDate = '2011-06-07';

What this query does

ModifiedDate is a datetime, so it stores a time as well as a date. '2011-06-07' means midnight (00:00:00.000) on that day. The query only matches rows modified at exactly midnight, so any row with a later time on that day is missed. In AdventureWorks every ModifiedDate happens to be at midnight, so here it still finds rows; on real data with times it would silently miss most of them.

Result in SSMS: 5 rows
#36

6. Date columns with a time part (continued)

SELECT TOP 5 ModifiedDate FROM Sales.SalesOrderHeader
WHERE CAST(ModifiedDate AS DATE) = '2011-06-07';

What this query does

CAST(ModifiedDate AS DATE) removes the time part, so every row from 7 June 2011 matches, whatever the time. The faster way on large tables is a range: ModifiedDate >= '2011-06-07' AND ModifiedDate < '2011-06-08', because it lets SQL Server use an index.

CAST(... AS DATE) drops the time, so every row from that day matches whatever its time.

Result in SSMS: 5 rows
#37

7. Crude email check. Second should be zero.

SELECT EmailAddress FROM Person.EmailAddress WHERE EmailAddress LIKE '%@%.%';

What this query does

The pattern means: some characters, an @, some characters, a dot, some characters. Every real email address matches this rough shape. It is not full validation, just a quick sanity check.

Result in SSMS: 19,972 rows
#38

7. Crude email check. Second should be zero. (continued)

SELECT EmailAddress FROM Person.EmailAddress WHERE EmailAddress NOT LIKE '%@%.%';

What this query does

NOT LIKE returns the rows that fail the check. 0 rows means every email address in the table has the expected shape.

Result in SSMS: 0 rows
#39

8. Five conditions

SELECT Name, Color, ListPrice, Class
FROM Production.Product
WHERE (Color IN ('Black', 'Silver') OR Color IS NULL)
  AND ListPrice BETWEEN 100 AND 2000
  AND Name NOT LIKE '%Frame%'
  AND Class = 'H';

What this query does

In English before running: 'high-class (H) products that are black, silver or have no colour, priced from 100 to 2000, and are not frames.' The brackets around the colour test matter: without them the OR would apply to the whole WHERE clause and let in unwanted rows.

In English: high-class products that are black, silver or have no colour, priced 100 to 2000, and are not frames.

Result in SSMS: 10 rows

Self-test

#40

Q1. Products priced 800 to 3000, not Red, Black or Silver (include NULL colours)

SELECT Name, Color, ListPrice FROM Production.Product
WHERE ListPrice BETWEEN 800 AND 3000
  AND (Color NOT IN ('Red', 'Black', 'Silver') OR Color IS NULL);

What this query does

NOT IN alone would drop the NULL colours (see drill 4), so OR Color IS NULL adds them back. The brackets keep that OR separate from the price condition.

Result in SSMS: 29 rows
#41

Q2. People with no middle name

SELECT FirstName, MiddleName, LastName FROM Person.Person WHERE MiddleName IS NULL;

What this query does

A missing middle name is stored as NULL, so IS NULL is the only correct test. MiddleName = NULL would return nothing.

Result in SSMS: 8,499 rows
#42

Q3. People whose last name ends in 'ez'

SELECT FirstName, LastName FROM Person.Person WHERE LastName LIKE '%ez';

What this query does

% at the start and nothing after 'ez' means 'ends with ez': Sanchez, Lopez, Gonzalez and so on.

Result in SSMS: 2,131 rows
#43

Q4. Orders in July 2013 with TotalDue above 10000

SELECT SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader
WHERE OrderDate >= '2013-07-01' AND OrderDate < '2013-08-01'
  AND TotalDue > 10000;

What this query does

For a whole month use 'start inclusive, next month exclusive': >= 1 July and < 1 August. This catches every time on 31 July. BETWEEN '2013-07-01' AND '2013-07-31' would miss orders placed after midnight on 31 July.

Start inclusive, next month exclusive. Safer than BETWEEN for dates.

Result in SSMS: 82 rows
#44

Q5. Products whose name contains a comma

SELECT Name FROM Production.Product WHERE Name LIKE '%,%';

What this query does

A comma is not a wildcard, so it can be searched for directly with % on both sides. These are mostly products with a size or colour after the comma, like 'HL Road Frame - Red, 58'.

Result in SSMS: 214 rows
Remember: products with ListPrice = 0 are components, not free items. WHERE ListPrice > 0 is often the first filter you add.