OPTION FAST N: First Rows Sooner, Total Reads Higher

OPTION FAST N tells SQL Server to plan for the first N rows. The first rows arrive sooner, and the price is paid on the rest of the result. This post measures both sides, so you can see when the trade is worth it.

Gouache painting of three fresh rolls in a vermilion basket on a counter in front of a tray of dough still waiting

What OPTION FAST N Does

OPTION (FAST 100) doesn’t limit the result. The query still returns every row. The hint changes the plan. SQL Server builds a plan that returns the first 100 rows quickly. The whole result can then cost more. That is a row goal, the same idea as TOP. The post on DISABLE_OPTIMIZER_ROWGOAL: How a Row Goal Changes the Plan explains the goal itself.

The usual way to speed up the first rows is a nested loops join. Nested loops finds one match and returns it at once. A hash join first builds a hash table over one whole input, so nothing comes back until that table exists. The demo has two tables. Products holds 100,000 rows and OrderLines holds 300,000, and every order line points at one product.

IF DB_ID(N'FastHintDemo') IS NULL CREATE DATABASE FastHintDemo;
GO
USE FastHintDemo;
GO
DROP TABLE IF EXISTS dbo.OrderLines;
DROP TABLE IF EXISTS dbo.Products;
CREATE TABLE dbo.Products (
    ProductID   int NOT NULL CONSTRAINT PK_Products PRIMARY KEY,
    ProductName nvarchar(40) NOT NULL,
    ListPrice   decimal(10,2) NOT NULL
);
CREATE TABLE dbo.OrderLines (
    LineID    int NOT NULL CONSTRAINT PK_OrderLines PRIMARY KEY,
    ProductID int NOT NULL,
    Qty       int NOT NULL
);
INSERT INTO dbo.Products
SELECT TOP (100000) n, CONCAT(N'Product ', n), n % 50 + 5
FROM (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n
      FROM sys.all_columns AS a CROSS JOIN sys.all_columns AS b) AS x;
INSERT INTO dbo.OrderLines
SELECT TOP (300000) n, n % 100000 + 1, n % 7 + 1
FROM (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n
      FROM sys.all_columns AS a CROSS JOIN sys.all_columns AS b) AS x;

Count the Reads

The next script runs the join three times. Each run pours the result into a temp table, so the output stays small. STATISTICS IO reports the logical reads for each table. The Messages tab shows one line per table and per statement. The table below lists the two numbers that matter.

SET NOCOUNT ON;
CREATE TABLE #Sink (LineID int NOT NULL, ProductName nvarchar(40) NOT NULL);
SET STATISTICS IO ON;
INSERT INTO #Sink SELECT ol.LineID, p.ProductName FROM dbo.OrderLines AS ol INNER JOIN dbo.Products AS p ON p.ProductID = ol.ProductID;
TRUNCATE TABLE #Sink;
INSERT INTO #Sink SELECT ol.LineID, p.ProductName FROM dbo.OrderLines AS ol INNER JOIN dbo.Products AS p ON p.ProductID = ol.ProductID OPTION (FAST 100);
TRUNCATE TABLE #Sink;
INSERT INTO #Sink SELECT ol.LineID, p.ProductName FROM dbo.OrderLines AS ol INNER JOIN dbo.Products AS p ON p.ProductID = ol.ProductID OPTION (FAST 1);
SET STATISTICS IO OFF;
HintProducts readsOrderLines reads
None647784
FAST 100918,759784
FAST 1918,759784

Without the hint, SQL Server hashes Products once. It reads 647 pages. With either hint it switches to nested loops and seeks into Products for every order line. That is 300,000 seeks. Each costs about three page reads, because the index on a table of this size has three levels. Reads on Products grew about 1,400 times, and the total about 640 times.

These counts come from the test server. A server that runs the plain query in parallel reads a few more pages, so the ratios drop a little.

The Messages tab also tells you which join ran. The plain run reports a scan count of 1 for Products, a single pass over the table. Both hinted runs report a scan count of 0 for Products, which is how seeks report. The actual plan shows the same change as a Hash Match turning into Nested Loops. Check for that change whenever you add a hint like this one.

Two plans of the same insert from the join: the default plan with Parallelism and Hash Match, and the OPTION (FAST 1) plan with Nested Loops.

The same join on a Products table of 500 rows and 150,000 order lines behaved differently. There FAST 100 kept the hash join, and only FAST 1 forced the loops. The size of the table decides where the plan flips, so test with your own sizes.

Quick card titled FAST N Hint Checklist: Goal: plan for the first N rows. Gain: first rows in a few milliseconds. Cost: far more reads for the full result. Fits: an app that shows one page first. Limit: not a fix for bad estimates or sniffing. Tip: Time the first row and the last row.

Time the First Row

Reads count work. Timing shows what the hint buys. SET STATISTICS TIME reports the whole run, not the first row. A small Windows PowerShell script reads the result row by row and stops a clock twice. It isn’t T-SQL, so replace the server name with yours.

foreach ($hint in '', ' OPTION (FAST 100)', ' OPTION (FAST 1)') {
    $cn = New-Object System.Data.SqlClient.SqlConnection 'Server=localhost;Database=FastHintDemo;Integrated Security=True;TrustServerCertificate=True'
    $cn.Open()
    $cmd = $cn.CreateCommand()
    $cmd.CommandText = 'SELECT ol.LineID, p.ProductName FROM dbo.OrderLines AS ol INNER JOIN dbo.Products AS p ON p.ProductID = ol.ProductID' + $hint
    $clock = [Diagnostics.Stopwatch]::StartNew()
    $reader = $cmd.ExecuteReader()
    [void]$reader.Read()
    $first = $clock.ElapsedMilliseconds
    while ($reader.Read()) { }
    "{0}: first row {1} ms, all rows {2} ms" -f $hint.Trim(), $first, $clock.ElapsedMilliseconds
    $reader.Close(); $cn.Close()
}
HintFirst rowAll rows
None54 ms217 ms
FAST 1005 ms828 ms
FAST 13 ms897 ms

The first row arrives much sooner, and the whole result takes several times longer. A second run gave the same pattern with different digits, so treat the milliseconds as one run on one server.

Where the Hint Fits

A reader noted that the hint suits applications that cache a result and show it a page at a time. OPTION FAST N fits that case. A screen shows the first 100 rows and lets the user scroll. It wants those rows now. The rest can arrive in the background.

The hint is not a cure for bad plans. OPTIMIZE FOR UNKNOWN sets no row goal, so the plan stays the no-hint plan. A reader doubted the numbers and asked whether the first query only warmed the cache. Logical reads count cached pages too, so the 918,759 holds on a warm cache.

You could argue that a hint is fine, because the first row is what the user sees. It is, until the user scrolls to the end. Here that costs several times the total time and about 640 times the reads. Choose FAST n for one specific screen, measure the full run and don’t spread it across a system. A hint also stops the optimizer from adapting when your data grows.

What to Remember

OPTION FAST N trades total work for first rows. Measure both numbers, with reads for the work and a client timer for the first row. Use it for paging screens. Leave it off for reports, exports and anything that reads the whole result.

Run the cleanup script when you finish the demo.

USE master;
GO
IF DB_ID(N'FastHintDemo') IS NOT NULL
BEGIN
    ALTER DATABASE FastHintDemo SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE FastHintDemo;
END;

A hint is not a speed-up, it is a trade you should be able to measure.

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 Scripts, SQL Server
Previous Post
Reasons for Slow Performance in SQL Server: The Top Five
Next Post
DISABLE_PARAMETER_SNIFFING Hint: Turn Off Sniffing for One Query

Related Posts

5 Comments. Leave new

  • Mustafa ELmasry
    February 11, 2020 3:49 pm

    great notes , i tested and i test Option (OPTIMIZE FOR UNKNOWN ) it show to me the same results of without Fast , i used Statistics Parser to compare the Statistics to know how many pages reads and How many logical read and CPU time
    this means this option is good from performance wise or what
    (OPTIMIZE FOR UNKNOWN )
    Thanks

    Reply
  • Good observation, but this is expected and predictable behavior . The FAST N query hint is designed for applications that may cache a result set and page the results to the application; i.e. if I show the user pages of 100 results at a time, I may want FAST 100. This is not designed to address bad plans due to out of date statistics or parameter sniffing. Note: this is not new to SQL 2019

    Reply
  • You’re running the same data pull three times in a row. Are you sure the times shown in the plan aren’t simply because the first select is caching the data? Your two results make no sense together as the percentages shown in the first result are supposed to indicate what percentage of CPU time each select took.

    Reply
  • I would say his results are correct because they are expected. FAST N creates a row goal by optimizing the query plan for the number given. In the case you want to page 100 rows at a time, FAST N will retrieve the first 100 rows much faster with a nested loop vs having to hash everything in a hash join first. The entire query may take longer, but a .NET application can use those first 100 rows right away while waiting for the query to finish.

    Reply
  • Peter Larsson
    March 1, 2022 12:39 pm

    I suspect there are 242634 rows in your Product table.

    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.