You add the index. The column is right there in the WHERE clause. And the query still crawls, because the execution plan shows an index scan where you expected a seek, SQL Server reading every row in the table to answer a question the index should have settled in a few pages. Most of the time the query itself is the culprit. Something in the WHERE clause hid the column from its own index.
There is a word for this: SARGable, from "search argument." A predicate is SARGable when the optimizer can satisfy it by seeking into an index. Break that, and the engine has no choice left but to check your condition against every row it can find, which is the scan you did not want. The usual way to break it is to do something to the indexed column instead of to the value you are comparing it against.
Dates are the most common example. This looks perfectly reasonable:
SELECT OrderId, CustomerId, Total
FROM dbo.Orders
WHERE YEAR(OrderDate) = 2025;
But YEAR() is a function wrapped around the indexed column, so the engine has to compute YEAR(OrderDate) for every row before it can compare anything, and the index on OrderDate sits there unused. Ask the same question as a range and the column comes back out into the open:
SELECT OrderId, CustomerId, Total
FROM dbo.Orders
WHERE OrderDate >= '2025-01-01'
AND OrderDate < '2026-01-01';
Same answer, but now it is a seek. The index points straight at the rows in that window instead of the engine reading the whole table to find them.
The sneakier version has no function anywhere in sight. Say ProductCode is a varchar column with an index on it, and you query it like this:
SELECT ProductId, Name
FROM dbo.Product
WHERE ProductCode = @code;
Clean. Nothing wrapped around the column. But if @code arrives as an nvarchar, and from a .NET application it usually does, SQL Server has to reconcile the two types, and nvarchar outranks varchar in precedence. So the engine quietly converts the column to match the parameter, not the other way around, and you end up with the equivalent of CONVERT(nvarchar, ProductCode) on every row. A function on the column again, except this time you did not write it, and the query looks innocent while the plan shows a scan.
The fix is to send the parameter as the type the column already is. In raw ADO.NET that means setting the parameter's SqlDbType to VarChar. In Dapper, wrap the value so it crosses as ANSI:
var rows = conn.Query<Product>(
"SELECT ProductId, Name FROM dbo.Product WHERE ProductCode = @code",
new { code = new DbString { Value = code, IsAnsi = true, Length = 20 } });
One property, and the seek comes back. This one is worth knowing because it hides so well: the SQL reads fine, the index exists, and the only visible symptom is a plan that scans a table it should be seeking. I have watched people rewrite a query five different ways looking for the problem when the problem was the parameter type all along.
Wildcards carry the same trap in miniature. LIKE 'John%' can seek, because SQL Server knows where the matching range begins. LIKE '%son' cannot, because a leading wildcard gives the index no starting point to seek to, so it reads everything. A trailing wildcard is an index lookup; a leading one is a full scan. Worth remembering before you build a search feature on a column that way.
The rule underneath all of these is the same. Keep the indexed column by itself on its side of the comparison, and move the work over to the other side. When you genuinely need to filter on a transformation of a column, say a date pulled out of a timestamp or a value extracted from a longer string, SQL Server will let you index the transformation itself through a computed column, so a seek is back on the table. But that is the second move. The first is to check whether you are hiding the column from its own index without meaning to.
All of this shows up the moment you read the actual execution plan. Seek or scan, in plain words, right there on the operator. When a query runs slower than its indexes say it should, that plan is the first place I look, and a scan sitting where a seek belongs almost always traces back to something done to the column that did not need doing.