On this page

Filtering & Sorting

The Three Tools

ClauseJobExample
WHEREKeep only the rows you needEmployees in Pune
ORDER BYPut the result in a fixed orderHighest salary first
LIKEMatch text by pattern instead of exact valueEmails ending in @gmail.com

Think of a stack of job applications. WHERE throws out the ones that don't qualify, ORDER BY sorts the rest by score, and LIKE finds the ones whose name starts with "A" when you don't remember the full name.

Sample Table

We'll use a bigger version of the Employees table from the JOINs page.

Employees

employee_idnameemaildepartment_idsalaryhire_datecity
1Aakashaakash@company.com101850002022-03-15Bengaluru
2Priyapriya.hr@company.com102600002023-07-01Pune
3Rahulrahul@company.com101950002021-11-20Bengaluru
4Nehaneha_s@company.comNULL550002024-01-10Delhi
5Arjunarjun@gmail.com103720002024-06-05Pune
6Ananyaananya@company.com101950002020-09-12Hyderabad

The Order SQL Server Runs a Query

You write SELECT first, but SQL Server processes the clauses in a different order:

11. FROM / JOIN → pick the tables
22. WHERE → filter rows
33. GROUP BY → make groups
44. HAVING → filter groups
55. SELECT → choose columns, create aliases
66. ORDER BY → sort
77. TOP / OFFSET → cut the result

This explains two rules:

  • A column alias made in SELECT can't be used in WHERE (WHERE runs earlier), but can be used in ORDER BY (runs later).
  • WHERE runs before sorting, so filtering early means fewer rows to sort.

WHERE: The Basics

-- comparison
SELECT name, salary FROM Employees WHERE salary >= 85000;
-- several values: IN is shorter than many ORs
SELECT name FROM Employees WHERE city IN ('Pune', 'Delhi');
-- range: BETWEEN includes both ends
SELECT name FROM Employees WHERE salary BETWEEN 60000 AND 85000;
-- NULL: use IS NULL, never = NULL
SELECT name FROM Employees WHERE department_id IS NULL; -- Neha

AND runs before OR. Use parentheses to say what you mean:

-- read as: Pune OR (Bengaluru AND salary > 80000)
SELECT name FROM Employees
WHERE city = 'Pune' OR city = 'Bengaluru' AND salary > 80000;
-- Aakash, Priya, Rahul, Arjun (Pune employees with any salary slip in)
-- what we meant: (Pune OR Bengaluru) AND salary > 80000
SELECT name FROM Employees
WHERE (city = 'Pune' OR city = 'Bengaluru') AND salary > 80000;
-- Aakash, Rahul

Two NULL traps:

-- 1. = NULL never matches. Comparing anything with NULL gives UNKNOWN, not true.
SELECT name FROM Employees WHERE department_id = NULL; -- 0 rows
-- 2. NOT IN with a NULL in the list returns NOTHING
SELECT name FROM Employees WHERE department_id NOT IN (102, NULL); -- 0 rows
-- each row is checked as: department_id <> 102 AND department_id <> NULL
-- "<> NULL" is UNKNOWN, so no row passes
-- safe version with a subquery: NOT EXISTS
SELECT e.name
FROM Employees e
WHERE NOT EXISTS (
SELECT 1 FROM Departments d
WHERE d.department_id = e.department_id
AND d.department_name = 'HR'
);

WHERE Optimization

Index Seek vs Index Scan

Assume these indexes exist:

CREATE INDEX IX_Employees_hire_date ON Employees (hire_date);
CREATE INDEX IX_Employees_email ON Employees (email);
CREATE INDEX IX_Employees_dept_salary
ON Employees (department_id, salary)
INCLUDE (name); -- name stored in the index too, so no table lookup

In the execution plan (the steps SQL Server chose), look for:

Plan stepMeaningSpeed on a big table
Index SeekJumps straight to matching rows using the indexFast
Index Scan / Table ScanReads the whole index or tableSlow
Key LookupFound the row in the index, then goes back to the table for missing columnsOK for few rows, slow for many

Write SARGable Conditions

A condition is SARGable (from Search ARGument able) when SQL Server can use an index to seek for it. The main rule: keep the column alone on one side. If you wrap the column in a function or calculation, SQL Server must compute it for every row, so it scans.

Not SARGable (scan)SARGable (seek)
WHERE YEAR(hire_date) = 2024WHERE hire_date >= '2024-01-01' AND hire_date < '2025-01-01'
WHERE salary * 12 > 1000000WHERE salary > 1000000 / 12.0
WHERE LEFT(name, 2) = 'Aa'WHERE name LIKE 'Aa%'
WHERE ISNULL(department_id, 0) = 101WHERE department_id = 101
WHERE UPPER(email) = 'ARJUN@GMAIL.COM'WHERE email = 'arjun@gmail.com' (default collation ignores case)
WHERE CAST(employee_id AS VARCHAR(10)) = '5'WHERE employee_id = 5
WHERE email LIKE '%@gmail.com'Leading wildcard can't seek; see the LIKE section
-- scan: YEAR() runs on every row
SELECT name FROM Employees
WHERE YEAR(hire_date) = 2024;
-- seek: the column is left alone, the range is on the other side
SELECT name FROM Employees
WHERE hire_date >= '2024-01-01'
AND hire_date < '2025-01-01'; -- Neha, Arjun

Using >= and < for date ranges also avoids a BETWEEN bug: with a datetime column, BETWEEN '2024-01-01' AND '2024-12-31' misses anything after midnight on 31 December.

Other Tips

TipWhy
Match data typesComparing a varchar column with an nvarchar value (like N'abc', the default string type in many drivers, including Node.js mssql) makes SQL Server convert the column on every row, which can turn a seek into a scan. Pass parameters with the column's exact type
Select only needed columnsSELECT * forces Key Lookups; with only indexed or INCLUDEd columns, the index answers the query alone (a covering index)
OR across different columnsWHERE email = 'x' OR city = 'Pune' often scans; two queries joined with UNION ALL can each seek
Composite index column order(department_id, salary) helps WHERE department_id = 101 AND salary > 80000, but not WHERE salary > 80000 alone (leftmost prefix rule)
Measure, don't guessRun SET STATISTICS IO, TIME ON; and compare logical reads before and after a change
SET STATISTICS IO, TIME ON;
SELECT name, salary
FROM Employees
WHERE department_id = 101 AND salary > 80000;
-- Messages tab: "logical reads 2" = good; thousands of reads = scanning
-- Execution plan (Ctrl + M in SSMS): Index Seek on IX_Employees_dept_salary

ORDER BY

ORDER BY sorts the result. ASC (ascending, smallest first) is the default; DESC is largest first.

Without ORDER BY, the row order is not guaranteed. It may look stable today and change after an index change or a bigger dataset.

-- highest salary first; same salary -> alphabetical by name
SELECT name, salary
FROM Employees
ORDER BY salary DESC, name ASC;
namesalary
Ananya95000
Rahul95000
Aakash85000
Arjun72000
Priya60000
Neha55000

The second column, name, only matters when the first is tied: Ananya and Rahul both earn 95000.

Sorting by Aliases and Expressions

-- alias works in ORDER BY (it runs after SELECT)
SELECT name, salary * 12 AS annual_salary
FROM Employees
ORDER BY annual_salary DESC;
-- but NOT in WHERE: error "Invalid column name 'annual_salary'"
-- SELECT name, salary * 12 AS annual_salary FROM Employees WHERE annual_salary > 1000000;
-- custom order with CASE: Bengaluru first, then Pune, then the rest
SELECT name, city
FROM Employees
ORDER BY
CASE city
WHEN 'Bengaluru' THEN 1
WHEN 'Pune' THEN 2
ELSE 3
END,
name;

NULLs in ORDER BY

In SQL Server, NULL sorts first in ascending order and last in descending order. There's no NULLS LAST keyword (PostgreSQL has one), so use a CASE:

-- default: Neha (NULL) comes first
SELECT name, department_id FROM Employees ORDER BY department_id;
-- push NULLs to the end
SELECT name, department_id
FROM Employees
ORDER BY
CASE WHEN department_id IS NULL THEN 1 ELSE 0 END,
department_id;

TOP, WITH TIES, and Pagination

-- top 2 earners
SELECT TOP (2) name, salary
FROM Employees
ORDER BY salary DESC; -- Ananya, Rahul
-- WITH TIES: also return rows tied with the last one
SELECT TOP (1) WITH TIES name, salary
FROM Employees
ORDER BY salary DESC; -- Ananya AND Rahul (both 95000)
-- pagination: page 2, 3 rows per page (OFFSET requires ORDER BY)
SELECT name, salary
FROM Employees
ORDER BY employee_id
OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY; -- Neha, Arjun, Ananya

TOP without ORDER BY returns any rows, not the "first" ones. For deep pages, OFFSET gets slow, because SQL Server still reads and skips every earlier row. Keyset pagination fixes this (WHERE employee_id > @last_id ORDER BY employee_id), as covered on the Pagination page.

Sorting Cost

Sorting many rows is expensive: it uses memory, and if the sort doesn't fit, it spills to tempdb on disk. An index already in the right order removes the Sort step from the plan:

1WHERE department_id = 101 ORDER BY salary DESC
2 with index (department_id, salary) → Index Seek, rows already in salary order, no Sort
3 without it → Scan → Sort → slower

LIKE

LIKE matches text against a pattern. Patterns use wildcards: special characters that stand for unknown characters.

WildcardMatchesExampleMatches in our table
%Any number of characters (including none)'A%'Aakash, Arjun, Ananya
_Exactly one character'_r%'Priya, Arjun (second letter is r)
[ ]One character from a set or range'[AR]%'Aakash, Rahul, Arjun, Ananya
[^ ]One character not in the set'[^A]%'Priya, Rahul, Neha

[ ] and [^ ] are SQL Server features; they don't exist in standard SQL or PostgreSQL's LIKE.

-- starts with
SELECT name FROM Employees WHERE name LIKE 'A%'; -- Aakash, Arjun, Ananya
-- ends with
SELECT name FROM Employees WHERE name LIKE '%a'; -- Priya, Neha, Ananya
-- contains
SELECT name, email FROM Employees WHERE email LIKE '%.hr@%'; -- Priya
-- exact length: 5 characters
SELECT name FROM Employees WHERE name LIKE '_____'; -- Priya, Rahul, Arjun
-- exclude a pattern
SELECT name, email FROM Employees WHERE email NOT LIKE '%@company.com'; -- Arjun

Searching for a Literal % or _

_ is a wildcard, so '%_%' matches every non-empty email. To search for a real underscore, put it in brackets or use ESCAPE:

-- wrong: _ means "any one character"
SELECT email FROM Employees WHERE email LIKE '%_%'; -- all 6 rows
-- right: [_] means a literal underscore
SELECT email FROM Employees WHERE email LIKE '%[_]%'; -- neha_s@company.com
-- same with ESCAPE: the character after ! is taken literally
SELECT email FROM Employees WHERE email LIKE '%!_%' ESCAPE '!';

Case Sensitivity

Whether LIKE (and =) ignores case depends on the column's collation (the rules for comparing and sorting text). The common default, SQL_Latin1_General_CP1_CI_AS, is CI = case-insensitive, so 'a%' also matches Aakash. To force a case-sensitive match:

SELECT name FROM Employees
WHERE name LIKE 'a%' COLLATE Latin1_General_CS_AS; -- CS = case-sensitive: 0 rows

Applying COLLATE to the column has the same effect as a function: SQL Server can't seek on the index.

LIKE and Indexes

PatternCan use an index seek?Why
'Aak%' (fixed start)YesThe index is sorted, so SQL Server jumps to "Aak" and reads forward
'%@gmail.com' (leading %)NoMatches could be anywhere in the sorted order, so it scans
'%rah%' (contains)NoSame reason

For searching words inside large text, like product descriptions or reviews, LIKE '%word%' doesn't scale. SQL Server's Full-Text Search (CONTAINS, FREETEXT) uses a special word index built for that.

LIKE From Application Code

When the search text comes from a user, parameterize it, and escape their wildcards so a typed % or _ is treated as text:

-- @search comes from the app as a parameter, never glued into the SQL string
DECLARE @search NVARCHAR(50) = 'rah';
SELECT name
FROM Employees
WHERE name LIKE @search + '%'; -- starts with: index-friendly
-- escape user wildcards before using them
SET @search = REPLACE(REPLACE(REPLACE(@search, '[', '[[]'), '%', '[%]'), '_', '[_]');

Interview Angle

How to Explain it in an Interview

The 30-Second Spoken Pitch
"WHERE filters rows before anything else, so I filter as early and precisely as possible. For performance, I keep conditions SARGable, meaning the column stays alone on one side so SQL Server can do an index seek. For example, instead of YEAR of hire date equals 2024, I write a date range. I also match parameter types to column types, since a varchar column compared with an nvarchar value forces a conversion and a scan, and I check the plan and logical reads to confirm. For sorting, ORDER BY is the only way to guarantee order. SQL Server puts NULLs first in ascending order, so I use a CASE to move them, and TOP WITH TIES or OFFSET FETCH for top-N and pagination. An index in the right order removes the sort step. With LIKE, percent matches any characters, underscore matches exactly one, and brackets match a set. A fixed prefix like Aak percent can use an index, but a leading percent can't, so for real text search I'd use full-text search."

Common mistakes:

  • WHERE column = NULL instead of IS NULL.
  • NOT IN with a list or subquery that can contain NULL.
  • Wrapping indexed columns in functions (YEAR(), ISNULL(), UPPER()).
  • Expecting a fixed order without ORDER BY, or using TOP without it.
  • Forgetting that _ is a wildcard in LIKE.

Summary

1Query order → FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → TOP/OFFSET
2WHERE → AND before OR (use parentheses), IS NULL, avoid NOT IN with NULLs
3SARGable → column alone on one side → Index Seek
4 → no functions/maths on the column, match data types, ranges for dates
5Measure → execution plan (Seek vs Scan) + SET STATISTICS IO
6ORDER BY → only way to guarantee order, ASC default, ties broken by next column
7 → NULLs first in ASC (use CASE to move), alias allowed here
8TOP / OFFSET → need ORDER BY; WITH TIES includes equal values
9LIKE → % any, _ one, [ ] set, [^ ] not in set
10 → [_] or ESCAPE for literal wildcards
11 → 'abc%' can seek, '%abc' can't → Full-Text Search for word search

Next, GROUP BY and HAVING show how to summarise rows, like average salary per department, and how HAVING filters those groups.

Next TopicJoins & Aggregation