Operator Costs in an Execution Plan: Read Them Without Being Fooled

The plan points at a large percentage, and everyone wants to fix that operator. Operator costs describe estimated work rather than measured elapsed time. Read the execution evidence before choosing your target.

Hands holding a huge airy meringue and a small dense fruitcake, the hand with the fruitcake sinking lower.

Understand Operator Costs Above Each Icon

I ask whether a plan percentage came from execution timing before accepting it as the bottleneck. The percentage comes from the optimizer's cost model. It isn't a stopwatch result.

The optimizer estimates CPU and I/O components while choosing a plan. Those internal costs help compare alternatives. Their units aren't a direct prediction of milliseconds on your hardware.

The percentage is relative to estimated costs within the displayed plan. A ninety-percent operator in a small plan can perform little actual work. Another operator with a smaller percentage can become expensive when its estimate is wrong.

The figures remain estimates even when SSMS displays an actual execution plan. Runtime information is added to the plan. The estimated cost model doesn't turn into measured cost after execution.

Operator costs are still useful context. They explain where the optimizer expected work. Treat them as expectations to check, not measured results to quote.

Distinguish Operator Costs From Subtree Cost

Estimated operator cost describes one modeled operator's contribution. Estimated subtree cost includes its descendants' modeled contributions. Read the property name before comparing two numbers.

A parent above several expensive branches naturally has a larger subtree cost. That doesn't mean the parent itself performed all the work. Confusing those scopes sends tuning in the wrong direction.

Likewise, plan percentages are relative within their comparison context. Don't compare percentages from two unrelated plans as absolute resource measurements. Their denominators differ.

Estimated row counts feed the cost calculation. An error in those counts changes the expected work downstream. A lookup estimated for a small population can become much larger at runtime.

I follow that row-count error toward the earliest useful source. Fixing the resulting expensive operator alone can leave the root estimate wrong. The next execution then repeats the same surprise.

Build a Plan You Can Inspect

Run the following example in an ordinary SSMS connection. Enable Include Actual Execution Plan before execution. The temporary sample doesn't change an application table.

The generated inputs make repeated customer groups. They aren't measured row counts from a production workload. COUNT_BIG reports the loaded population if you need it for the test notes.

The aggregation and ordering give the optimizer choices about processing the data. The exact operators depend on the loaded sample and server. Don't expect one guaranteed icon arrangement.

STATISTICS IO and TIME provide statement-level evidence. Save that output with the actual plan. The plan's estimated percentage alone isn't a substitute for either result.

Run the same query with the same input when comparing a change. A different requested result makes the comparison harder to interpret. Correctness belongs beside the resource observations.

CREATE TABLE #CostSalesDemo
(SaleId int NOT NULL PRIMARY KEY, CustomerId int NOT NULL, Amount decimal(12,2) NOT NULL);
INSERT #CostSalesDemo(SaleId, CustomerId, Amount)
SELECT n, n % 100, 10.00
FROM (SELECT TOP (20000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n
      FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b) AS s;
SELECT COUNT_BIG(*) AS LoadedRows FROM #CostSalesDemo;
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SELECT CustomerId, SUM(Amount) AS CustomerAmount
FROM #CostSalesDemo
GROUP BY CustomerId ORDER BY CustomerAmount DESC;
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
Expected work versus observed work: a diagram about the operator costs

Check Operator Costs Against Actual Rows

Select an operator and inspect its estimated rows. Then inspect actual rows and actual executions. The number of executions matters for repeated inner work.

A nested loops inner branch can run once for each outer row. A small per-execution result therefore adds up. Read total actual work and execution counts together.

Look for the first substantial divergence between expected and observed rows. A later sort receiving too many rows is a consequence. The earlier predicate or join deserves examination.

Check parameter values and statistics used for compilation. A different parameter distribution can explain a reused plan's surprise. An actual plan describes its particular execution context.

What row estimate made the optimizer prefer this access path? Answer that before replacing the operator with a hint. The shape is the result of several prior decisions.

Read Runtime Properties With Their Scope

Recent actual plans expose elapsed time, CPU and read information through supported runtime properties. Look at actual logical reads on access operators where supplied. Use statement STATISTICS IO as a cross-check.

Not every operator exposes every metric under every profiling path. Lightweight profiling provides rows with less timing detail in some contexts. An absent property isn't proof that an operator consumed no resources.

Elapsed and CPU time mean different things. Waiting increases elapsed time without equivalent CPU. Parallel workers can accumulate CPU beyond the wall-clock elapsed duration.

Operator timings also have scope and overlap considerations. Row-mode and batch-mode reporting aren't identical. Don't sum every displayed elapsed figure into a supposed statement duration.

Use the statement's QueryTimeStats for overall timing where available. Then interpret operator metrics within that execution's reporting model. An attractive total created from overlapping measurements isn't evidence.

Inspect Reads Beyond the Percentage

A selective lookup provides another useful comparison. The following query filters the same temporary table by customer, so run it in the same session. Inspect its access operator and actual read information.

Without an index on CustomerId, the optimizer has a limited set of efficient choices. Adding one on the sample supplies another access path. Compare the actual work without assuming the index must win.

The included Amount makes the second test capable of reading the required value from that index. It doesn't change the selected business rows. Save both result and plan comparisons.

A scan can still be appropriate when the qualifying fraction is large. Don't label every scan a defect. The actual reads and workload requirement settle the question.

Operator costs can help explain why an alternative was chosen. Actual rows and reads show whether that choice served this execution. Keep both forms of evidence in the review.

SET STATISTICS IO ON;
SELECT SaleId, Amount FROM #CostSalesDemo WHERE CustomerId = 10;
CREATE INDEX IX_CostSalesDemo_Customer ON #CostSalesDemo(CustomerId) INCLUDE(Amount);
SELECT SaleId, Amount FROM #CostSalesDemo WHERE CustomerId = 10;
SET STATISTICS IO OFF;

Fix the Work the Execution Demonstrates

Spill warnings, unexpected rows and repeated lookups deserve attention even with small cost percentages. Check the associated memory grant and predicates. Those properties explain actual work more directly.

I preserve the original plan when testing a change. I compare the same representative parameters and concurrency conditions. A quiet single call doesn't represent every application execution.

Report measured timings and reads only after collecting them on your server. The examples here supply the experiment rather than its outcome. The largest printed percentage doesn't receive an automatic repair ticket.

Use operator costs to understand the optimizer's expectation. Use actual execution evidence to choose the next investigation. That keeps the plan review grounded in the work users experienced.

Related reading on this blog: Why Query Cost Percentages in a Plan Mislead You and Number of Rows Read: Execution Plan.

What a big plan percentage proves: a checklist on the operator costs

A plan percentage is not elapsed time, it is a relative estimate that needs execution evidence.

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

Execution Plan, SQL Operator, SQL Performance, SQL Server
Previous Post
SQL SERVER – Turning Off Priority Boost Server Configuration Option on SSMS 18 Onwards
Next Post
Parallel Plans: Reading Gather Streams and Repartition Streams

Related Posts

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.