Query Hint Basics: Why a Variable Scans and OPTIMIZE FOR Seeks

Query hint basics start with one idea: a query hint is an instruction you add to one statement. It goes in the OPTION clause and overrides a choice the optimizer would make. It helps when the optimizer works from the wrong information. It doesn’t help when the plan is already right.

Table of four ways to ask for the 10 Front tickets. A literal value gets an index seek with a key lookup and 32 reads. A variable with no hint gets a clustered index scan, an estimate of 25,005 rows and 1,435 reads. OPTIMIZE FOR and RECOMPILE both return to the seek and 32 reads

The Sample Table

A Tickets table with 50,010 rows. Ten of them are in the Front category, and the rest are Standard. A 200 character Note column makes each row wide, and an index on Category supports the search. Run this in any test database. Every number in this post comes from SQL Server 2025.

CREATE TABLE Tickets (TicketID INT IDENTITY(1,1) PRIMARY KEY, Category VARCHAR(10) NOT NULL, Note CHAR(200) NOT NULL DEFAULT '');
GO

WITH n AS (SELECT TOP (50010) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS i FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b CROSS JOIN sys.all_objects AS c)
INSERT INTO Tickets (Category)
SELECT CASE WHEN i <= 10 THEN 'Front' ELSE 'Standard' END FROM n;
GO

CREATE INDEX IX_Tickets_Category ON Tickets (Category);
GO

Check the counts.

SELECT Category, COUNT(*) AS Tickets FROM Tickets GROUP BY Category ORDER BY Category;

Ten Front rows and 50,000 Standard rows. A search for Front is selective, and a search for Standard isn’t. Turn on the actual plan with Ctrl+M to see the row counts. SET STATISTICS IO ON adds the page reads.

SET STATISTICS IO ON;

A Value Gets a Seek

Ask for the rare category with the value written in the query.

SELECT TicketID, Category, Note FROM Tickets WHERE Category = 'Front';

The plan is an Index Seek on the Category index, plus a key lookup for the Note column. The optimizer estimated 10 rows and found 10. The query read 32 pages.

A Variable Gets a Scan

Now put the same value in a variable and run both lines in one batch.

DECLARE @Type VARCHAR(10) = 'Front';
SELECT TicketID, Category, Note FROM Tickets WHERE Category = @Type;

The plan changes to a Clustered Index Scan. The estimate is 25,005 rows, although the query returns 10, and it reads 1,435 pages.

The optimizer compiles the whole batch before the variable gets its value. So it can’t look up Front in the statistics. It uses the average instead: 50,010 rows over 2 values is 25,005. Reading 25,005 rows through a seek and a lookup costs more than one scan, so it scans.

The OPTION Clause

A query hint goes in an OPTION clause at the end of the statement. It applies to the whole statement. OPTIMIZE FOR tells the optimizer to build the plan as if the variable held a value you name.

DECLARE @Type VARCHAR(10) = 'Front';
SELECT TicketID, Category, Note FROM Tickets WHERE Category = @Type
OPTION (OPTIMIZE FOR (@Type = 'Front'));

The seek and the key lookup are back. The estimate is 10 rows, and the query reads 32 pages.

Bar chart of logical reads. Front as a value reads 32 pages, Front in a variable with no hint 1,435, Standard in a variable with no hint 1,435, and Standard with the hint OPTIMIZE FOR Front 150,138

Another Way: RECOMPILE

OPTION (RECOMPILE) asks for a fresh plan at every run, built with the real value of the variable. The price is a compile each time.

DECLARE @Type VARCHAR(10) = 'Front';
SELECT TicketID, Category, Note FROM Tickets WHERE Category = @Type
OPTION (RECOMPILE);

The plan is the same seek and lookup, with 10 estimated rows and 32 reads.

A Hint Is a Bet

OPTIMIZE FOR fixes one value into the plan. If the variable later holds Standard, the plan still expects ten rows. These two queries ask for the 50,000 Standard rows, first without the hint and then with it. They assign the columns to variables, so the grid stays empty and the plan stays the same.

DECLARE @Type VARCHAR(10) = 'Standard', @id INT, @cat VARCHAR(10), @note CHAR(200);
SELECT @id = TicketID, @cat = Category, @note = Note FROM Tickets WHERE Category = @Type;

The same query with the hint still saying Front:

DECLARE @Type VARCHAR(10) = 'Standard', @id INT, @cat VARCHAR(10), @note CHAR(200);
SELECT @id = TicketID, @cat = Category, @note = Note FROM Tickets WHERE Category = @Type
OPTION (OPTIMIZE FOR (@Type = 'Front'));

Without the hint the scan reads 1,435 pages. With the hint the seek and lookup run 50,000 times, and the query reads 150,138 pages. The hint made this case read about 105 times as many pages.

Treat a hint as a last resort. Try fresh statistics or a different query first. When you use one, check the plan and the reads before and after. Look at the value again whenever the data changes.

Clean Up

Remove the sample table when you finish.

DROP TABLE Tickets;

What to Remember

A query hint lives in the OPTION clause and covers one statement. A variable gets an average estimate, and a literal value gets the exact one. OPTIMIZE FOR and RECOMPILE fix the estimate in different ways, and each has a cost.

To go further, read OPTIMIZE FOR and RECOMPILE Hints Compared, Multiple OPTIMIZE FOR Hints in One Query: Value or UNKNOWN and OPTION (RECOMPILE) Hint: When a Fresh Plan Pays Off.

Quick reference card: Query hint basics

A query hint is not a speed switch, it is a bet on the value you name.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

Execution Plan, Query Hint, SQL Index, SQL Scripts
Previous Post
Hierarchical Query Using a Recursive CTE: Levels, Order and Loops
Next Post
SQL Error Actions: What SQL Server Does After an Error

Related Posts

3 Comments. Leave new

  • If I have another Customer Type Visiting with almost same row of Business type then Can I add another Hint
    option(Optimize for (@type=’visting’))

    Reply
  • Hi Pinal,
    I have applied the same what you explained in the above, still it is going for scan. I am using Sql server 2005.

    Reply
  • Hi ,
    I am trying to get xml from a location into a table using rowset, the following is the query, but n but its not accepting as valid fiile name. Please help me
    CREATE TABLE #XmlImportTest(
    xmlFileName VARCHAR(300) NOT NULL,
    xml_data XML NOT NULL
    )
    GO
    declare @directory varchar(256) set @directory = ‘”D:\To Folder”‘

    declare @filecount int, @fileindex int, @linecount int, @lineindex int
    declare @filename varchar(255), @arg varchar(512), @contents varchar(8000)
    set @filename = ‘D:\To Folder’
    set @arg = ‘dir ‘ + @directory + ‘\ /A-D /B’
    declare @dir table (id int not null identity(1,1),[filename] varchar(512))
    insert @dir exec master.dbo.xp_cmdshell @arg
    DECLARE @ID int
    DECLARE IDs CURSOR LOCAL FOR select id from @dir
    OPEN IDs
    FETCH NEXT FROM IDs into @ID
    WHILE @@FETCH_STATUS = 0
    BEGIN
    SET @filename = @filename + ‘\’+ (select filename from @dir where id = @ID)

    print @filename
    declare @files table ([xmlFileName] varchar(512), [xml_data] xml null)

    INSERT INTO @files(xmlFileName, xml_data)

    SELECT ”’ + @filename + ”’, xmlData
    FROM(
    SELECT *
    FROM OPENROWSET (BULK ”’ + @filename +”’, SINGLE_BLOB) AS XMLDATA
    ) AS FileImport (XMLDATA)

    FETCH NEXT FROM IDs into @ID
    END
    CLOSE IDs
    DEALLOCATE IDs

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.