Are Filtered Statistics Useful with Parameterization?

This post is based on a question I received last week…

If we have a filtered index that was not a match for a query due to parameterization (or a variation of which the parameter is not known at compile time), we can see the unmatched filtered index name in the UnmatchedIndexes element of SHOWPLAN_XML output.

The question I received on this subject was, “what about for filtered statistics?”  Can those still be used for parameterized scenarios? The following is a quick test to see if the stats also don’t qualify for matching… I’ll start by showing the filtered statistics matching behavior using the following statistics object:

USE [Credit];
GO

CREATE STATISTICS fstat_charge_provider_no
ON [dbo].[charge] ([member_no], [category_no])
WHERE [provider_no] = 484;
GO

The following query will use the statistics, and I’ve included the trace flags to show the usage from a query optimizer perspective:

SELECT DISTINCT
        1.[member_no],
        1.[category_no],
        1.[provider_no]
FROM    [dbo].[charge] AS c
WHERE   1.[provider_no] = 484 AND
        1.[member_no] = 9527 AND
        1.[category_no] = 2
OPTION  (QUERYTRACEON 3604, QUERYTRACEON 9204);
GO

This returns the following message output based on an initial compilation:

Stats loaded: DbName: Credit, ObjName: dbo.charge, IndexId: 2, ColumnName: category_no, EmptyTable: FALSE

Stats loaded: DbName: Credit, ObjName: dbo.charge, IndexId: 3, ColumnName: provider_no, EmptyTable: FALSE

Stats loaded: DbName: Credit, ObjName: dbo.charge, IndexId: 6, ColumnName: member_no, EmptyTable: FALSE

Filtered stats loaded: DbName: Credit, ObjName: dbo.charge, IndexId: 7, ColumnName: member_no, 
Expr: ([provider_no]=(484)), EmptyTable: FALSE

Filtered stats loaded: DbName: Credit, ObjName: dbo.charge, IndexId: 7, ColumnName: member_no, 
Expr: ([provider_no]=(484)), EmptyTable: FALSE

Filtered stats loaded: DbName: Credit, ObjName: dbo.charge, IndexId: 7, ColumnName: member_no, 
Expr: ([provider_no]=(484)), EmptyTable: FALSE

Now let’s use a local variable instead for the provider_no predicate (used for the filtered statistics):

DECLARE @provider_no INT = 484;

SELECT DISTINCT
        1.[member_no],
        1.[category_no],
        1.[provider_no]
FROM    [dbo].[charge] AS c
WHERE   1.[provider_no] = @provider_no AND
        1.[member_no] = 9527 AND
        1.[category_no] = 2
OPTION  (QUERYTRACEON 3604, QUERYTRACEON 9204);
GO

This time we see the following trace flag 9204 output:

Stats loaded: DbName: Credit, ObjName: dbo.charge, IndexId: 2, ColumnName: category_no, EmptyTable: FALSE

Stats loaded: DbName: Credit, ObjName: dbo.charge, IndexId: 3, ColumnName: provider_no, EmptyTable: FALSE

Stats loaded: DbName: Credit, ObjName: dbo.charge, IndexId: 6, ColumnName: member_no, EmptyTable: FALSE

Even though the provider_no value matched “484”, the filtered stats were not loaded as the value was not known at compile time. This is one specific scenario – so if you have a different one on this subject, please feel free to share on the comments of this post.

Thanks!

Redundant Query Plan Branches

Consider the following “basic_member” view definition from the Credit database:

CREATE VIEW [dbo].[basic_member]
AS
SELECT  [member].[member_no],
[member].[lastname],
[member].[firstname],
[member].[middleinitial],
[member].[street],
[member].[city],
[member].[state_prov],
[member].[mail_code],
[member].[phone_no],
[member].[region_no],
[member].[expr_dt],
[member].[member_code]
FROM    [dbo].[member]
WHERE   [member].[member_no] NOT IN (SELECT    [corp_member].[member_no]
FROM      [dbo].[corp_member]);

GO

A simple SELECT from this view returns 8,498 rows and has the following plan shape (and I’m boxing in an “areas of interest” via SQL Sentry Plan Explorer’s rendering of the plan):

image thumb4 Redundant Query Plan Branches

We see that the view has a predicate on member_no NOT IN the corp_member table.  But what happens if the original report writer doesn’t look at the view definition and decides they need this same predicate applied at the the view reference scope (not realizing this was already taken care of)?  For example:

SELECT  [basic_member].[member_no],
[basic_member].[lastname],
[basic_member].[firstname],
[basic_member].[middleinitial],
[basic_member].[street],
[basic_member].[city],
[basic_member].[state_prov],
[basic_member].[mail_code],
[basic_member].[phone_no],
[basic_member].[region_no],
[basic_member].[expr_dt],
[basic_member].[member_code]
FROM    [dbo].[basic_member]
WHERE   [basic_member].[member_no] NOT IN
(SELECT   [corp_member].[member_no]
FROM     [dbo].[corp_member]);

Like the previous query against the view, we see 8,498 rows.  But unlike the previous query, we see the following plan:

image thumb5 Redundant Query Plan Branches

Notice the redundancy – even though the result set is identical between the two versions.  And the tables I’m using are small, but you can still see the difference in scan count and logical reads.

Query Against View

Table ‘member’. Scan count 2, logical reads 305, physical reads 2, read-ahead reads 294, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table ‘corporation’. Scan count 1, logical reads 8, physical reads 1, read-ahead reads 6, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Query with Redundant Predicate

Table ‘member’. Scan count 3, logical reads 325, physical reads 2, read-ahead reads 294, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table ‘corporation’. Scan count 2, logical reads 16, physical reads 1, read-ahead reads 6, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

And there is additional I/O overhead associated with the second plan of course.  For the Credit database, the scale is small, but imagine the difference for a very large table.

You cannot always count on easily identifying redundant areas.  The Query Optimizer may find an optimal plan in spite of the way it was written – but if you do see repeating branches in a query execution tree associated with a performance issue, you may want to explore the possibility of overlapping/redundant logic further.

SQLIntersection: Unashamed Database Engine Coverage

On the week of April 8th, in the MGM Grand in Las Vegas, I’ll be delivering a few sessions at the SQLIntersection conference along with some pretty incredible company, including:

  • Aaron Bertrand, Sr. Consultant, SQL Sentry, Inc. [blog | twitter]
  • Andrew J. Kelly, Mentor, SolidQ [blog | twitter]
  • Bob Ward, Principal Architect Escalation Engineer, Microsoft [blog | twitter]
  • Brent Ozar, Brent Ozar Unlimited [blog | twitter]
  • Conor Cunningham, Principal Architect, SQL Server, Microsoft [blog]
  • Grant Fritchey, Product Evangelist, Red Gate Software [blog | twitter]
  • Jeremiah Peschka, Brent Ozar Unlimited [blog | twitter]
  • Kendra Little, Managing Director, Brent Ozar Unlimited [blog | twitter]
  • Kevin Farlee, SQL Program Manager, Microsoft [blog]
  • Kevin Kline, Director of Engineering Services, SQL Sentry, Inc. [blog | twitter]
  • Kimberly L. Tripp, President/Founder, SQLskills.com [blog | twitter]
  • Paul S. Randal, CEO / Owner, SQLskills.com [blog | twitter]
  • Paul White, SQL Kiwi Limited [blog | twitter
  • Steve Jones, Editor, SQLServerCentral.com [blog | twitter]

One thing that I love about the lineup is its unashamed coverage of Database Engine-centric  topics (box product and Windows Azure SQL Database).  There will be coverage of AlwaysOn Availability Groups, SQL Server I/O patterns, columnstore indexes, database design, performance monitoring & tuning, SQL Server databases on SMB file shares, database encryption, Filestream and Filetable, table partitioning, query execution plans, indexing, virtualization, parallelism, PBM and CMS, statistics, Hekaton, parameter sniffing, and T-SQL practices.  And with that block of database engine topics, there is still a little Hadoop thrown in for good measure (compliments Jeremiah Peschka).

What is also cool about this conference is the inclusion of track “hosts”.  Aaron Bertrand, Brent Ozar and Kimberly L. Tripp will help guide the attendees through the topics and sessions and help build on a cohesive and interactive experience.

This will be a great experience and I hope to see you there!

Ill Be There thumb SQLIntersection: Unashamed Database Engine Coverage

SSC Article – Windows Server 2012 Deduplication – Should you use it with SQL Server Backups?

SQLServerCentral.com just published the following article:

Windows Server 2012 Deduplication – Should you use it with SQL Server Backups?

This new Windows Server functionality is promising, but from a SQL Server DBA perspective there are some areas you should be careful of and I explore some of the considerations in this article.

Cheers!

Columnstore Segment Population Skew

My last post about nonclustered Columnstore indexes was written back on February 25th, 2012 (Exploring Columnstore Index Metadata, Segment Distribution and Elimination Behaviors).  Amazed by how quickly time passes.

Anyhow, this is a quick post on segment population skew based on parallel nonclustered Columnstore index creations.

I’ll use the same 123,695,104 row FactInternetSales table I used almost a year ago to demonstrate.  I’ll create the following nonclustered Columnstore index just on one column, to keep things simple:

CREATE NONCLUSTERED COLUMNSTORE INDEX [NCSI_FactInternetSales]
ON [dbo].[FactInternetSales]
(
[ProductKey]
);
 

The index takes 31 seconds to create on my laptop and it was created using 8 threads (which I can confirm via the SQL Server execution plan in, this case, SQL Sentry Plan Explorer):

image thumb3 Columnstore Segment Population Skew

Adding up the actual rows by thread, we get the 123,695,104 row count.

Now if we look at sys.column_store_segments, we can see that the last few segments were populated with less than the maximum 1,048,576 rows:

SELECT  [partition_id],
[column_id],
[segment_id],
[row_count]
FROM sys.column_store_segments
WHERE [row_count] = 1048576 AND
[column_id] = 2;

image thumb1 Columnstore Segment Population Skew

Now the purpose of this short post is to show what happens if we remove parallelism from the overall Columnstore index build (aside from increasing build time and reducing the memory grant):

 
DROP INDEX [NCSI_FactInternetSales]  ON [dbo].[FactInternetSales];
GO

CREATE NONCLUSTERED COLUMNSTORE INDEX [NCSI_FactInternetSales]
ON [dbo].[FactInternetSales]
(
[ProductKey]
)WITH (DROP_EXISTING = OFF, MAXDOP = 1);
GO

Now instead of running in 31 seconds with 8 schedulers, this serial index build took (not surprisingly) 2 minutes and 10 seconds to build.

How many segments fell beneath the 1,048,576 row count?

image thumb2 Columnstore Segment Population Skew

This time, just one segment, the last one to be populated.  With 117 segments (segment_id 0 through segment_id 117) populated at 1,048,576 per segment, and 123,695,104 rows – our 118th segment has the remaining 1,011,712 rows.

Should the more tightly packed segments provide meaningful performance gains versus the parallel-built, partially filled version?  I haven’t tested this yet, but I will at some point.  Let me know if you get a chance to do so before I do.  My wild guess would be that the benefit would be minor, at best – but as with most things I would like to see for myself.

SQL Server Event Schedule for H1 2013

I’ve had a couple of months at home, focusing on consulting engagements, and now I’m about to enter back in to conference / Immersion Event season.  I’ll still be consulting, but half of the time I’ll be doing it from a hotel room.

While it is nice to interact with people via email, phone and social media – I much prefer getting a chance to see people in person. Here are the public events I’ll be speaking & teaching at the first half of this year:

Hope to see you at one (or more) of these events!

Auto-Update Stats Default Sampling Test

In this post I’ll just share the results of a simple set of tests regarding automatic-update sampling.  Nothing fancy – I just populated the category table from the Credit sample database with varying row counts and then took note of the associated duration and sampling percent.  The query I used to kick of auto-updates after the row inserts was the following:

    
SELECT  [category_no],
[category_desc],
[category_code]
FROM [dbo].[category]
WHERE [category_desc] = 'Meals'
OPTION (RECOMPILE);

I also show the StatMan MAXDOP just for kicks (once true sampling kicks in, it is set to MAXDOP “1”, so not terribly interesting). I executed a few test iterations beyond what I’m showing here, but I just include two test runs per scenario just for comparison:

Test ID Row Count Data Pages StatMan Duration (ms) TABLESAMPLE PERCENT MAXDOP
1 10 1 0 100.0000 16
2 10 1 0 100.0000 16
1 1,000,000 3,228 1228 34.6964 1
2 1,000,000 3,228 1184 34.6964 1
1 2,000,000 6,455 1297 18.0480 1
2 2,000,000 6,455 1248 18.0480 1
1 10,000,000 32,275 1287 4.2324 1
2 10,000,000 32,275 1171 4.2324 1
1 20,000,000 64,550 1481 2.3625 1
2 20,000,000 64,550 1489 2.3625 1
1 40,000,000 129,103 2071 1.3617 1
2 40,000,000 129,103 1890 1.3617 1
1 80,000,000 258,210 2487 0.8129 1
2 80,000,000 258,210 2301 0.8129 1

A few comments:

  • Notice the sample percent is identical between identical row/page tests (each test started with a restore to the same base state)
  • Notice that as the data pages grow, the duration of the stats update doesn’t grow significantly, and then consider the sample percentage multiplied by the total data page value (for example – ~1757 data pages for 40M vs. ~2098 data pages for 80M)
  • Duration is just based on my laptop and I was more interested in the relative duration across test runs

Now what happens if I rebuild the index with a low fill factor? (Just doing this on the 80M row version):

    

USE [Credit]
GO
ALTER INDEX [category_ident] ON [dbo].[category]
REBUILD PARTITION = ALL
WITH (FILLFACTOR = 10);
GO

I’ll do a non-updating update so that I can get the threshold for the column updates incremented and thus kick off an auto-update of statistics:

  
UPDATE TOP (16000500) [category]
SET [category_desc] = [category_desc];
GO

Here are the results (Test ID 3 in bold):

Test ID Row Count Data Pages StatMan Duration (ms) TABLESAMPLE PERCENT MAXDOP
1 80,000,000 258,210 2487 0.8129 1
2 80,000,000 258,210 2301 0.8129 1
3 80,000,000 2,543,516 1399 0.4568 1

Same row count, significantly more pages, lower duration (interesting) and a reduced table sample percent – but if you do the math, more pages than the previous samplings. Lots of other fun things we could try, but that’s all for now…

New Course: “SQL Server: Troubleshooting Query Plan Quality Issues”

My latest online course, “SQL Server: Troubleshooting Query Plan Quality Issues” was published today by Pluralsight.  The short description is as follows… “Learn how to identify, diagnose, and prevent problems where SQL Server chooses the incorrect query plan for your critical queries, applicable to developers, DBAs, and anyone responsible for SQL Server, from SQL Server 2005 onwards.”

This 2 hour and 20 minute course covers the following areas:

  • Understand why query plan quality is so important
  • Learn techniques needed to identify plan quality issues
  • Identify and resolve a variety of plan quality patterns

As of today, this makes 12 13 courses by SQLskills.com in the Pluralsight library.  And there is much more to come…

Detecting Selectivity Guesses

I’ve been mulling over a potential Microsoft Connect item regarding selectivity guess warnings, but I’m uncertain if it would have enough demand to drive product team attention.  Selectivity guesses, which I talked a little about in the “Selectivity Guesses in absence of Statistics” post, are sometimes referred to as “magic numbers”, “heuristics” or just plain old QO guesses.

When a guess occurs, I think it would be interesting\helpful to know when it happens, especially considering the impact on query plan quality.  And while I’ve seen a high number of SQL Server instances over the last 16 years, I couldn’t tell you how many had bad plans due to selectivity guesses.  Assuming that there are supporting statistics – one would hope that selectivity guesses at the leaf-level of a plan should be infrequent.  And yet I feel it would be nice to confirm this through an exposed mechanism when it happens at the non-leaf levels.  For example – just knowing when the Query Optimizer says “ I don’t know what estimate to use for this Filter, so I’ll use a selectivity guess and put a warning in the plan.”

In theory, one could comb a plan’s XML and pull out the estimates vs. cardinality of the child operator (or if the leaf-level, the table cardinality).  Then those numbers could be evaluated for potential matches on known patterns.

And before we begin, I just want to confirm that this is not to be considered an actual real-world solution.  I’ll explain why later – but in the meantime just view these demo queries as a rough draft thrown together for use in exploring a proof-of-concept.

Let’s say you have a table that is missing statistics entirely – and auto-creation of statistics are disabled.  This is an upstream issue that we know has a fix – but in this scenario, it has not been addressed. Now let’s take a sprawling query example that hits a few different patterns that would generate known selectivity guesses in absence of statistics:

SELECT  [charge_no],
        [member_no],
        [provider_no],
        [category_no],
        SUM([charge_amt]) AS [total_charge_amt]
FROM    (SELECT [charge_no],
                [member_no],
                [provider_no],
                [category_no],
                [charge_dt],
                [charge_amt],
                [statement_no],
                [charge_code]
         FROM   dbo.[charge_guess]
         WHERE  [provider_no] = 500
         UNION
         SELECT [charge_no],
                [member_no],
                [provider_no],
                [category_no],
                [charge_dt],
                [charge_amt],
                [statement_no],
                [charge_code]
         FROM   dbo.[charge_guess]
         WHERE  [statement_no] BETWEEN 500 AND 1000
         UNION
         SELECT [charge_no],
                [member_no],
                [provider_no],
                [category_no],
                [charge_dt],
                [charge_amt],
                [statement_no],
                [charge_code]
         FROM   dbo.[charge_guess]
         WHERE  [charge_amt] < 100000
         UNION
         SELECT [charge_no],
                [member_no],
                [provider_no],
                [category_no],
                [charge_dt],
                [charge_amt],
                [statement_no],
                [charge_code]
         FROM   dbo.[charge_guess]
         WHERE  [category_no] LIKE 10) AS t
GROUP BY [charge_no],
        [member_no],
        [provider_no],
        [category_no]
HAVING  SUM([charge_amt]) > 3999.99;
GO

Now that this is in cache, let me extract the different operators and associated attributes into a temporary table:

WITH XMLNAMESPACES 
    (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')    
 SELECT s.[value]('@NodeId', 'int') AS [NodeId], 
		s.[value]('@PhysicalOp', 'nvarchar(128)') AS [PhysicalOp],
		s.[value]('@EstimateRows', 'real') AS [EstimateRows],
		s.[value]('@TableCardinality', 'real') AS [TableCardinality],
		c.[plan_handle], 
		q.[query_plan],
		t.text
 INTO #ProcessResults
 FROM sys.dm_exec_cached_plans AS 1 
 CROSS APPLY sys.dm_exec_query_plan(plan_handle) AS [q] 
 CROSS APPLY query_plan.nodes('//RelOp') AS batch(s) 
 CROSS APPLY sys.[dm_exec_sql_text](c.plan_handle) AS [t] 
 WHERE t.text	LIKE '%provider_no%'
 OPTION(MAXDOP 1, RECOMPILE);
 GO

Next – let me look at a few common selectivity guess percentages:

-- Common guesses - not all inclusive!
SELECT  plan_handle,
		NodeId,
		PhysicalOp,
		EstimateRows AS EstimateRows,
		COALESCE(TableCardinality, 
		         LAG(EstimateRows, 1,0) OVER (ORDER BY NodeId DESC)) 
			AS Cardinality,
		CASE
			CAST(EstimateRows / COALESCE(TableCardinality, 
		         LAG(EstimateRows, 1,0) OVER (ORDER BY YEAR(NodeId)))
				 AS numeric (24,4))
			WHEN 0.0316 THEN 'Potential Selectivity Guess'
			WHEN 0.09 THEN 'Potential Selectivity Guess'
			WHEN 0.30 THEN 'Potential Selectivity Guess'
			WHEN 0.10 THEN 'Potential Selectivity Guess'
			ELSE '-' 
		END AS 'Potential Selectivity Guess?'
FROM [#ProcessResults] AS pr
ORDER BY NodeId DESC;

Looking at the results, I can see a few patterns for applicable operators that may be doing selectivity guesses:

image thumb Detecting Selectivity Guesses

So the demo query found where the guesses were originating at the leaf level for table scans due to four different expression patterns – and also caught a potential selectivity guess for the Filter operator further down towards the root of the plan.

This solution is imperfect though:

  • I’m just pulling a few different known selectivity guess percentages.  I don’t know all the scenarios and I’m definitely missing some of them.  Not to mention the fact that some of the potential predicates would involve more sophisticated evaluation in order to detect potential selectivity guesses.
  • Each Table Scan also had a “Columns With No Statistics” warning (at Node 20, 16, 13, and 10).  I would argue that this would be a good red flag in and of itself.

So why even raise this discussion in the first place?

  • While the “Columns With No Statistics” warning surfaces for my table access, it didn’t surface for the Filter operator (Node 1).
  • There are scenarios in the intermediate levels of a plan where I would love to know when the query optimizer has resorted to pure guesses.

Again, I’m not entirely sure a Connect item would be anything but a symbolic act – and perhaps justly so – but I’m curious to hear from those of you who have confirmed selectivity guess issues for their plans – and most particularly for scenarios where there aren’t other symptoms (for example, no associated warnings that might tip you off to an upstream issue).

Row and Page Count Influence on Sort Memory Grants

My prior post demonstrated the influence of row and page counts on estimated CPU and I/O cost.  Now in this post I’m going to step through the influence of row and page counts on memory grants for a Sort operator.  I’ll be using the MemoryGrantInfo element from the query execution plan to measure the influence.  I wrote about this element in my “Memory Grant Execution Plan Statistics” blog post.

Starting State

I’ll be querying the member table which has 142 data pages and 10,000 rows.  The query itself will have an ORDER BY curr_balance.  The curr_balance column is currently not supported by an existing index and will require a Sort operator in the plan, which then means that a memory grant will be needed:

SELECT  [member].[member_no],
[member].[lastname],
[curr_balance]
FROM    [dbo].[member]
ORDER BY [curr_balance] ASC
OPTION  (RECOMPILE, MAXDOP 1);

Using an actual execution plan, I see the following MemoryGrantInfo data for the baseline query:

<MemoryGrantInfo
SerialRequiredMemory=”512″
SerialDesiredMemory=”1904″
RequiredMemory=”512″
DesiredMemory=”1904″
RequestedMemory=”1904″
GrantWaitTime=”0″
GrantedMemory=”1904″
MaxUsedMemory=”680″ />

I’m forcing a serial plan, so you’ll see that the SerialRequiredMemory matches the RequiredMemory attribute and the SerialDesiredMemory matches the DesiredMemory attribute (and others as well).  I used this hint for this blog post to maintain an apples-to-apples comparison (serial plan vs. serial plan).

The Sort operator has the following CPU and I/O costs (not including subtree costs):

  • Estimated CPU Cost = 0.60342
  • Estimated I/O Cost = 0.0112613

Inflating the page count while maintaining the row count

What happens if we inflate the page count while maintaining the row count?

-- Please don't use this in production - for demonstration purposes only!
UPDATE STATISTICS dbo.[member]
WITH ROWCOUNT = 10000, PAGECOUNT = 149000;

Re-executing the query, I see the following memory grant statistics:

<MemoryGrantInfo
SerialRequiredMemory=”512″
SerialDesiredMemory=”1904″
RequiredMemory=”512″
DesiredMemory=”1904″
RequestedMemory=”1904″
GrantWaitTime=”0″
GrantedMemory=”1904″
MaxUsedMemory=”680″ />

The memory grant statistics for the 149000 page plan are identical to the 142 page plan.

The Sort operator has the following CPU and I/O costs (excluding subtree costs):

  • Estimated CPU Cost = 0.60342
  • Estimated I/O Cost = 0.0112613

So we see identical CPU and I/O costs for the Sort operator even with inflated page counts – even though the leaf-level Clustered Index Scan itself has an inflated estimated I/O cost (110.373).

Inflating the row count while maintaining the page count

What happens if I inflate the row count while maintaining the page count (at the original 142 pages)?

  
UPDATE STATISTICS dbo.[member]
WITH ROWCOUNT = 100000000, PAGECOUNT = 142;

This time we see a difference:

<MemoryGrantInfo
SerialRequiredMemory=”512″
SerialDesiredMemory=”8564592″
RequiredMemory=”512″
DesiredMemory=”8564592″
RequestedMemory=”4579608″
GrantWaitTime=”0″
GrantedMemory=”4579608″
MaxUsedMemory=”680″ />

SerialRequiredMemory and RequiredMemory are the same as prior scenarios (512) – but we see a big spike in SerialDesiredMemory, DesiredMemory, RequestedMemory and GrantedMemory.

The Sort operator has the following CPU and I/O costs (excluding subtree costs):

  • Estimated CPU Cost = 5003.06
  • Estimated I/O Cost = 7875.01

So we see that the Sort operator CPU and I/O cost is sensitive to row counts, but not data page counts (and the leaf-level Clustered Index Scan shows a 0.107569 estimated I/O cost and a 110 estimated CPU cost).

Summary

So recapping what we see when manipulating row and page counts for a query that uses a Sort operator:

  • The memory grant introduced by the Sort operator was sensitive to row counts, but not page counts.
  • The Sort operator’s estimated CPU and I/O costs were both sensitive to row counts, but not page counts.