SQL Server Execution Plans
SQL Server Execution Plans
All this information makes the execution plan a vitally important part of the toolbelt of
database administrators, developers, report writers, and pretty much anyone who writes
TSQL to access data in a SQL Server database.
My goal with this book was to gather as much useful information as possible on
execution plans into a single location, and to organize it in such a way that it provided
a clear route through the subject. Right from the basics of capturing plans, through to
their interpretation, and then on to how to use them to understand how you might optimize
your SQL queries, improve your indexing strategy, spot common performance issues,
and more.
i
SQL Server
Execution Plans
by Grant Fritchey
First published 2008 by Simple-Talk Publishing
ii
ISBN 978-1-906434-02-1
The right of Grant Fritchey to be identified as the author of this work has been asserted by him in accordance with the
Copyright, Designs and Patents Act 1988
All rights reserved. No part of this publication may be reproduced, stored or introduced into a retrieval system, or
transmitted, in any form, or by any means (electronic, mechanical, photocopying, recording or otherwise) without the
prior written consent of the publisher. Any person who does any unauthorised act in relation to this publication may be
liable to criminal prosecution and civil claims for damages.
This book is sold subject to the condition that it shall not, by way of trade or otherwise, be lent, re-sold, hired out, or
otherwise circulated without the publisher’s prior consent in any form other than which it is published and without a
similar condition including this condition being imposed on the subsequent publisher.
CONTENTS
Contents....................................................................................................................3
About the author .....................................................................................................9
acknowledgements ................................................................................................11
Introduction ...........................................................................................................13
Foreword.................................................................................................................15
Chapter 1: Execution Plan Basics .......................................................................17
What Happens When a Query is Submitted?........................................17
Query Parsing......................................................................................18
The Query Optimizer ........................................................................19
Query Execution.................................................................................20
Estimated and Actual Execution Plans ...........................................21
Execution Plan Reuse.........................................................................21
Why the Actual and Estimated Execution Plans Might Differ ...23
Execution Plan Formats ....................................................................25
Graphical Plans ...................................................................................25
Text Plans.............................................................................................25
XML Plans ...........................................................................................26
Getting Started............................................................................................26
Sample Code........................................................................................26
Permissions Required to View Execution Plans............................27
Working with Graphical Execution Plans ..............................................27
Getting the Estimated Plan...............................................................28
Getting the Actual Plan .....................................................................28
Interpreting Graphical Execution Plans .........................................29
Working with Text Execution Plans........................................................34
Getting the Estimated Text Plan......................................................35
Getting the Actual Text Plan ............................................................35
4
ACKNOWLEDGEMENTS
I wrote this book with a lot of help. Firstly, and most importantly,
thanks to Tony Davis for offering me this project and then supporting
me so well throughout. I couldn't have done it without you. Next, I
want to thank my wife & kids who put up with me when I was getting
cranky because of troubles writing this book. You guys are troopers.
I also want to thank all the people who answer questions over at the
forums at SQL Server Central. I got stuck a couple of times and you
guys helped. Finally, I want to thank my co-workers who refrained from
killing me when I sent them chapters and pushed for comments,
questions and suggestions… repeatedly.
To everyone who helped: you guys get credit for everything that's right
in the book. Anything that's wrong is all my fault.
Cheers!
Grant Fritchey
Introduction 13
INTRODUCTION
Every day, out in the various discussion boards devoted to Microsoft
SQL Server, the same types of questions come up again and again:
• Why is this query running slow?
• Is my index getting used?
• Why isn't my index getting used?
• Why does this query run faster than this query?
• And on and on.
The correct response is probably different in each case, but in order to
arrive at the answer you have to ask the same return question in each
case: have you looked at the execution plan?
Execution plans show you what's going on behind the scenes in SQL
Server. They can provide you with a wealth of information on how your
queries are being executed by SQL Server, including:
• Which indexes are getting used and where no indexes are being
used at all.
• How the data is being retrieved, and joined, from the tables
defined in your query.
• How aggregations in GROUP BY queries are put together.
• The anticipated load, and the estimated cost, that all these
operations place upon the system.
All this information makes the execution plan a fairly important tool in
the tool belt of database administrator, database developers, report
writers, developers, and pretty much anyone who writes TSQL to access
data in a SQL Server database.
Given the utility and importance of the tool, you'd think there'd be huge
swathes of information devoted to this subject. To be sure, fantastic
information is available from various sources, but there really isn't any
one place you can go to for focused, practical information on how to
use and interpret execution plans.
This is where my book comes in. My goal was to gather as much useful
information on execution plans as possible into a single location, and to
organize it in such as way that it provided a clear route through the
subject, right from the basics of capturing plans, through their
interpretation, and then on to how to use them to understand how you
14
might optimize your SQL queries, improve your indexing strategy, and
so on.
Specifically, I cover:
• How to capture execution plans in graphical, as well as text and
XML formats
• A documented method for interpreting execution plans, so that
you can create these plans from your own code and make sense
of them in your own environment
• How SQL Server represents and interprets the common SQL
Server objects – indexes, views, derived tables etc – in execution
plans
• How to spot some common performance issues such as
bookmark lookups or unused/missing indexes
• How to control execution plans with hints, plans guides and so
on, and why this is a double-edged sword
• How XML code appears in execution plans
• Advanced topics such as parallelism, forced parameterization
and plan forcing.
Along the way, I tackle such topics as SQL Server internals,
performance tuning, index optimization and so on. However, my focus
is always on the details of the execution plan, and how these issues are
manifest in these plans. If you are specifically looking for information
on how to optimize SQL, or build efficient indexes, then you need a
book dedicated to these topics. However, if you want to understand
how these issues are interpreted within an execution plan, then this is
the place for you.
Foreword 15
FOREWORD
I have attended many SQL Server conferences since 2000, and I have
spoken with hundreds of people attending them. One of the most
significant trends I have noticed over the past eight years is the huge
number of people who have made the transition from IT Professional
or Developer, to SQL Server Database Administrator. In some cases,
the transition has been planned and well thought-out. In other cases, it
was an accidental transition, when an organization desperately needed a
DBA, and the closest warm body was chosen for the job.
No matter the route you took to get there, all DBAs have one thing in
common: we have had to learn how to become DBAs through self-
training, hard work, and trial and error. In other words, there is no
school you can attend to become a DBA, it is something you have to
learn on your own. Some of us are fortunate to attend a class or two, or
to have a great mentor to help us along. However, in most cases, DBAs
become DBAs the hard way: we are thrown into the water and we either
sink or swim.
One of the biggest components of a DBA's self-learning process is
reading. Fortunately, there are many good books on the basics of being
a DBA that make a good starting point for your learning process. Once
you have read the basic books and have gotten some experience under
your belt, you will soon want to know more of the details of how SQL
Server works. While there are a few good books on the advanced use of
SQL Server, there are still many areas that aren't well covered. One of
those areas of missing knowledge is a dedicated book on SQL Server
execution plans.
That's where Dissecting SQL Server Execution Plans comes into play. It is
the first book available anywhere that focuses entirely on what SQL
Server execution plans are, how to read them, and how to apply the
information you learn from them in order to boost the performance of
your SQL Servers.
This was not an easy book to write because SQL Server execution plans
are not well documented anywhere. Grant Fritchey spent a huge amount
of time researching SQL Server execution plans, and conducting
original research as necessary, in order to write the material in this book.
Once you understand the fundamentals of SQL Server, this book
should be on top of your reading list, because understanding SQL
16
Brad McGehee
Director of DBA Education, Red-Gate Software Cambridge
Cambridge 2008
Chapter 1: Execution Plan Basics 17
Query Parsing
When you pass a T-SQL query to the SQL Server system, the first place
it goes to is the relational engine.1
As the T-SQL arrives, it passes through a process that checks that the T-
SQL is written correctly, that it's well formed. This process is known as
query parsing. The output of the Parser process is a parse tree, or query
tree (or even sequence tree). The parse tree represents the logical steps
necessary to execute the query that has been requested.
If the T-SQL string is not a data manipulation language (DML)
statement, it will be not be optimized because, for example, there is only
one "right way" for the SQL Server system to create a table; therefore,
there are no opportunities for improving the performance of that type
of statement. If the T-SQL string is a DML statement, the parse tree is
passed to a process called the algebrizer. The algebrizer resolves all the
names of the various objects, tables and columns, referred to within the
query string. The algebrizer identifies, at the individual column level, all
the types (varchar(50) versus nvarchar(25) and so on) of the objects
being accessed. It also determines the location of aggregates (such as
GROUP BY, and MAX) within the query, a process called aggregate
binding. This algebrizer process is important because the query may have
aliases or synonyms, names that don't exist in the database, that need to
be resolved, or the query may refer to objects not in the database.
The algebrizer outputs a binary called the query processor tree, which
is then passed on to the query optimizer.
value, taken from 200 data points evenly distributed across the data. It's
this "data about the data" that provides the information necessary for
the optimizer to make its calculations.
If statistics exist for a relevant column or index, then the optimizer will
use them in its calculations. Statistics, by default, are created and
updated automatically within the system for all indexes or for any
column used as a predicate, as part of a WHERE clause or JOIN ON
clause. Table variables do not ever have statistics generated on them, so
they are always assumed by the optimizer to have a single row, regardless
of their actual size. Temporary tables do have statistics generated on
them and are stored in the same histogram as permanent tables, for use
within the optimizer.
The optimizer takes these statistics, along with the query processor tree,
and heuristically determines the best plan. This means that it works
through a series of plans, testing different types of join, rearranging the
join order, trying different indexes, and so on, until it arrives at what it
thinks will be the fastest plan. During these calculations, a number is
assigned to each of the steps within the plan, representing the
optimizer's estimation of the amount of time it thinks that step will
take. This shows what is called the estimated cost for that step. The
accumulation of costs for each step is the cost for the execution plan
itself.
It's important to note that the estimated cost is just that – an estimate.
Given an infinite amount of time and complete, up-to-date statistics, the
optimizer would find the perfect plan for executing the query. However,
it attempts to calculate the best plan it can in the least amount of time
possible, and is obviously limited by the quality of the statistics it has
available. Therefore these cost estimations are very useful as measures,
but may not precisely reflect reality.
Once the optimizer arrives at an execution plan, the actual plan is
created and stored in a memory space known as the plan cache –
unless an identical plan already exists in the cache (more on this shortly,
in the section on Execution Plan Reuse). As the optimizer generates
potential plans, it compares them to previously generated plans in the
cache. If it finds a match, it will use that plan.
Query Execution
Once the execution plan is generated, the action switches to the storage
engine, where the query is actually executed, according to the plan.
Chapter 1: Execution Plan Basics 21
We will not go into detail here, except to note that the carefully
generated execution may be subject to change during the actual execution
process. For example, this might happen if:
• A determination is made that the plan exceeds the threshold for
a parallel execution (an execution that takes advantage of
multiple processors on the machine – more on parallel
execution in Chapter 3).
• The statistics used to generate the plan were out of date, or
have changed since the original execution plan was created by
the optimizer.
The results of the query are returned to you after the relational engine
changes the format to match that requested in your T-SQL statement,
assuming it was a SELECT.
Each plan is stored once, unless the cost of the plan lets the optimizer
know that a parallel execution might result in better performance (more
on parallelism in Chapter 8). If the optimizer sees parallelism as an
option, then a second plan is created and stored with a different set of
operations to support parallelism. In this instance, one query gets two
plans.
Execution plans are not kept in memory forever. They are slowly aged
out of the system using an "age" formula that multiplies the estimated
cost of the plan by the number of times it has been used (e.g. a plan
with a cost of 10 that has been referenced 5 times has an "age" value f
of 50). The lazywriter process, an internal process that works to free all
types of cache (including plan cache), periodically scans the objects in
the cache and decreases this value by one each time.
If the following criteria are met, the plan is removed from memory:
• More memory is required by the system
• The "age" of the plan has reached zero
• The plan isn't currently being referenced by an existing
connection
Execution plans are not sacrosanct. Certain events and actions can cause
a plan to be recompiled. It is important to remember this because
recompiling execution plans can be a very expensive operation. The
following actions can lead to recompilation of an execution plan:
• Changing the structure or schema of a table referenced by the
query
• Changing an index used by the query
• Dropping an index used by the query
• Updating the statistics used by the query
• Calling the function, sp_recompile
• Subjecting the keys in tables referenced by the query to a large
number of inserts or deletes
• For tables with triggers, significant growth of the inserted or
deleted tables
• Mixing DDL and DML within a single query, often called a
deferred compile
• Changing the SET options within the execution of the query
• Changing the structure or schema of temporary tables used by
the query
• Changes to dynamic views used by the query
• Changes to cursor options within the query
Chapter 1: Execution Plan Basics 23
DBCC FREEPROCCACHE
You're going to want to see the objects within the cache in order to see
how the optimizer and storage engine created your plan. With dynamic
management views and dynamic management functions, we can easily
put together a query to get a very complete set of information about the
execution plans on our system:
SELECT [cp].[refcounts]
,[cp].[usecounts]
,[cp].[objtype]
,[st].[dbid]
,[st].[objectid]
,[st].[text]
,[qp].[query_plan]
FROM sys.dm_exec_cached_plans cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st
CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle)
qp ;
With this query we can see the SQL called and the XML plan generated
by the execution of that SQL. You can use the XML directly or open it
as a graphical execution plan.
SELECT *
FROM TempTable;
Graphical Plans
These are quick and easy to read but the detailed data for the plan is
masked. Both Estimated and Actual execution plans can be viewed in
graphical format.
Text Plans
These are a bit harder to read, but more information is immediately
available. There are three text plan formats:
• SHOWPLAN_ALL: a reasonably complete set of data
showing the Estimated execution plan for the query
• SHOWPLAN_TEXT: provides a very limited set of data for
use with tools like osql.exe. It too only shows the Estimated
execution plan
26
XML Plans
XML plans present the most complete set of data available on a plan, all
on display in the structured XML format. There are two varieties of
XML plan:
• SHOWPLAN_XML: The plan generated by the optimizer
prior to execution.
• STATISTICS_XML: The XML format of the Actual
execution plan.
Getting Started
Execution plans are there to assist you in writing efficient T-SQL code,
troubleshooting existing T-SQL behavior or monitoring and reporting
on your systems. How you use them and view them is up to you, but
first you need to understand the information contained within the plans
and how to interpret it. One of the best ways to learn about execution
plans is to see them in action, so let's get started.
Please note that occasionally, especially when we move on to more
complex plans, the plan that you see may differ slightly from the one
presented in the book. This might be because we are using different
versions of SQL Server (different SP levels and hot fixes), that we are
using slightly different versions of the AdventureWorks database, or
because of how the AdventureWorks database has been altered over
time as each of us has played around in it. So while most of the plans
you get should be very similar to what we display here, don't be too
surprised if you try the code and see something different
Sample Code
Throughout the following text, I'll be supplying T-SQL code that you're
encouraged to run for yourself. All of the source code is freely
downloadable from the Simple Talk Publishing website (http://
www.simple-talk.com/).
The examples are written for SQL 2005 sample database,
Adventureworks. You can get hold of get a copy of Adventureworks
from here:
http://www.codeplex.com/MSFTDBProdSamples
Chapter 1: Execution Plan Basics 27
If you are working with procedures and scripts other than those
supplied, please remember that encrypted procedures will not display an
execution plan.
The plans you see may not precisely reflect the plans generated for the
book. Depending on how old a given copy of AdventureWorks may be,
the statistics could be different, the indexes may be different, the
structure and data may be different. So please be aware that you won't
always see the same thing if you run the examples.
The initial execution plans will be simple and easy to read from the
samples presented in the text. As the queries and plans become more
complicated, the book will describe the situation but, in order to easily
see the graphical execution plans or the complete set of XML, it will be
necessary to generate the plans. So, please, read next to your machine, so
that you can try running each query yourself!
Substituting the user name will enable execution plans for that user on
that database.
SELECT *
FROM [dbo].[DatabaseLog];
28
Figure 1
We'll explain what this plan means shortly, but first, let's capture the
Actual execution plan.
Figure 2
In this simple case the Actual plan is identical to the Estimated plan.
Figure 3
Chapter 1: Execution Plan Basics 31
Figure 4
32
Each of the different operators will have a distinct set of data. The
operator in Figure 4 is performing work of a different nature than that
in Figure 3, and so we get a different set of details. First, the Physical
and Logical Operations are listed. The logical operators are the results
of the optimizer's calculations for what should happen when the query
executes. The physical operators represent what actually occurred. The
logical and physical operators are usually the same, but not always –
more on that in Chapter 2.
After that, we see the estimated costs for I/O, CPU, Operator and
Subtree. The Subtree is simply the section of the execution tree that we
have looked at so far, working right to left again, and top to bottom. All
estimations are based on the statistics available on the columns and
indexes in any table.
The I/O Cost and CPU cost are not actual operators, but rather the
cost numbers assigned by the Query Optimizer during its calculations.
These numbers are useful when determining whether most of the cost
is I/O-based (as in this case), or if we're putting a load on the CPU. A
bigger number means more processing in this area. Again, these are not
hard and absolute numbers, but rather pointers that help to suggest
where the actual cost in a given operation may lie.
You'll note that, in this case, the operator cost and the subtree cost are
the same, since the table scan is the only operator. For more complex
trees, with more operators, you'll see that the cost accumulates as the
individual cost for each operator is added to the total. You get the full
cost of the plan from the final operation in the query plan, in this case
the Select operator.
Again we see the estimated number of rows. This is displayed for each
operation because each operation is dealing with different sets of data.
When we get to more complicated execution plans, you'll see the
number of rows change as various operators perform their work on the
data as it passes between each operator. Knowing how the rows are
added or filtered out by each operator helps you understand how the
query is being performed within the execution process.
Another important piece of information, when attempting to
troubleshoot performance issues, is the Boolean value displayed for
Ordered. This tells you whether or not the data that this operator is
working with is in an ordered state. Certain operations, for example, an
ORDER BY clause in a SELECT statement, may require data to be
placed in a particular order, sorted by a particular value or set of values.
Knowing whether or not the data is in an Ordered state helps show
where extra processing may be occurring to get the data into that state.
Chapter 1: Execution Plan Basics 33
Figure 5
34
And:
GO
SELECT *
FROM [dbo].[DatabaseLog];
GO
SET SHOWPLAN_ALL OFF;
GO
When you execute this query, the estimated plan is shown in the results
pane. Here is the first column of the results:
Figure 6
This screen shot was trimmed to keep the text as readable as possible.
The text plan generated roughly parallels the graphical plan. The first
row is the SELECT statement that was submitted. The rows following
are the physical operations occurring within the query plan. In or case
that means one row i.e. the table scan.
As we progress and view more complex text plans, in later chapters,
you'll quickly realize that they are not as readily readable as the graphical
plan. There's also no easy route through the query, such as we have with
the "read it right to left" approach in the graphical plans. You start in
the middle and move outwards, helped by the indentation of the data
and the use of pipe ( | ) to connect the statements parent to child.
In addition to the first column, the details that were hidden in the
ToolTip or in the Properties window are displayed in a series of
columns. Most of the information that you're used to seeing is here,
plus a little more. So, while the NodeId was available in the graphical
plan, because of the nature of the graphical plan, nothing was required
to identify the parent of a given node. In the SHOWPLAN_ALL we
get a column showing the Parent NodeId. As you scan right you'll see
many other familiar columns, such as the TotalSubTreeCost,
EstimateRows and so on. Some of the columns are hard to read, such
as the Defined List (the values or columns introduced by this operation
to the data stream), which is displayed as just a comma-separated list.
Chapter 1: Execution Plan Basics 37
SET SHOWPLAN_XML ON
…
SET SHOWPLAN_XML OFF
GO
SET SHOWPLAN_XML ON;
GO
SELECT *
FROM [dbo].[DatabaseLog];
SET SHOWPLAN_XML OFF;
GO
Figure 7
The link is a pointer to an XML file located here:
http://schemas.microsoft.com/sqlserver/2004/07/showplan/
ANSI_NULLS="false" ANSI_PADDING="false"
ANSI_WARNINGS="false" NUMERIC_ROUNDABORT="false" />
<QueryPlan CachedPlanSize="9">
Not only is there more information than in the text plans, but it's also
much more readily available and easier to read than in either the text
plans or the graphical plans (although the flow through the graphical
plans is much easier to read). For example, a problematic column, like
the Defined List mentioned earlier, that is difficult to read, becomes the
OutputList element with a list of ColumnReference elements, each
containing a set of attributes to describe that column:
<OutputList>
<ColumnReference Database="[AdventureWorks]"
Schema="[dbo]" Table="[DatabaseLog]"
Column="DatabaseLogID" />
<ColumnReference Database="[AdventureWorks]"
Schema="[dbo]" Table="[DatabaseLog]" Column="PostTime" />
<ColumnReference Database="[AdventureWorks]"
Schema="[dbo]" Table="[DatabaseLog]"
Column="DatabaseUser" />
<ColumnReference Database="[AdventureWorks]"
Schema="[dbo]" Table="[DatabaseLog]" Column="Event" />
<ColumnReference Database="[AdventureWorks]"
Schema="[dbo]" Table="[DatabaseLog]" Column="Schema" />
<ColumnReference Database="[AdventureWorks]"
Schema="[dbo]" Table="[DatabaseLog]" Column="Object" />
<ColumnReference Database="[AdventureWorks]"
Schema="[dbo]" Table="[DatabaseLog]" Column="TSQL" />
40
<ColumnReference Database="[AdventureWorks]"
Schema="[dbo]" Table="[DatabaseLog]" Column="XmlEvent" />
</OutputList>
This makes XML not only easier to read, but much more readily
translated directly back to the original query.
Back to the plan, after RelOp element referenced above we have the
table scan element:
Followed by a list of defined values that lays out the columns referenced
by the operation:
<DefinedValues>
<DefinedValue>
<ColumnReference
Database="[AdventureWorks]" Schema="[dbo]"
Table="[DatabaseLog]" Column="DatabaseLogID" />
</DefinedValue>
<DefinedValue>
…<output cropped>……..
In order to actually save an XML plan as XML, you need to first open
the results into the XML window. If you attempt to save to XML
directly from the result window you only get what is on display in the
result window. Another option is to go to the place where the plan is
stored, as defined above, and copy it.
4 Detailed coverage of Profiler is out of scope for this book, but more
information can be found in Books Online
(http://msdn2.microsoft.com/en-us/library/ms173757.aspx).
42
Figure 8
These extra events provide additional information to help put the XML
plan into context. For example, you can see what occurred just before
and after the event you are interested in.
Once Showplan XML is selected, or any of the other XML events, a
third tab appears called Events Extraction Settings. On this tab, you
can choose to output the XML as it's generated to a separate file, for
later use. Not only can you define the file, but also determine whether
or not all the XML will go into a single file or a series of files, unique to
each execution plan.
Figure 9
Click on the "Run" button in order to start the trace. When you capture
the above events, you get a trace like the one shown in Figure 10.
Chapter 1: Execution Plan Basics 45
Figure 10
Notice that I have clicked on the Showplan XML event. Under the
TextData column, you see the actual XML plan code. While you can't
see all of it in the screen shot above, it is all there and can be saved to
an individual file. In the second window, you can see the graphical
execution plan, which is how most people prefer to read and analyze
execution plans. So, in effect, the Showplan XML event available in
Profiler not only shows you the XML plan code, but also the graphical
execution plan.
At this stage, you can also save the code for this particular Showplan
XML event to a separate file. Simply right-click on the Showplan XML
event you want to save, then select "Extract Event Data."
Figure 11
46
This brings up a dialog box where you can enter the path and filename
of the XML code you want to store. Instead of storing the XML code
with the typical XML extension, the extension used is .SQLPlan. By
using this extension, when you double-click on the file from within
Windows Explorer, the XML code will open up in Management Studio
in the form of a graphical execution plan.
Whether capturing Estimated execution plans or Actual execution plans,
the Trace events operate in the same manner as when you run the T-
SQL statements through the query window within Management Studio.
The main difference is that this is automated across a large number of
queries, from ad-hoc to stored procedures, running against the server.
Summary
In this chapter we've approached how the optimizer and the storage
engine work together to bring data back to your query. These operations
are expressed in the estimated execution plan and the actual execution
plan. You were given a number of options for obtaining either of these
plans, graphically, output as text, or as XML. Either the graphical plans
or the XML plans will give you all the data you need, but it's going to be
up to you to decide which to use and when based on the needs you're
addressing and how you hope to address them.
Chapter 2: Reading Graphical Execution Plans for Basic Queries 47
http://msdn2.microsoft.com/en-us/library/ms175913.aspx
SELECT *
FROM Person.Contact
Figure 1
We can see that a clustered index scan operation is performed to retrieve
the required data. If you place the mouse pointer over the Clustered
Index Scan icon, to bring up the ToolTip window, you will see that the
clustered index used was PK_Contact_ContactID and that the
estimated number of rows involved in the operation was 19972.
Indexes in SQL Server are stored in a B-tree (a series of nodes that
point to a parent). A clustered index not only stores the key structure,
like a regular index, but also sorts and stores the data, which is the main
reason why there can be only one clustered index per table.
As such, a clustered index scan is almost the same in concept as a table
scan. The entire index, or a large percentage of it, is being traversed,
row-by-row, in order to identify the data needed by the query.
An index scan often occurs, as in this case, when an index exists but the
optimizer determines that so many rows need to be returned that it is
quicker to simply scan all the values in the index rather than use the keys
provided by that index.
An obvious question to ask if you see an index scan in your execution
plan is whether you are returning more rows than is necessary. If the
number of rows returned is higher than you expect, that's a strong
Chapter 2: Reading Graphical Execution Plans for Basic Queries 51
indication that you need to fine-tune the WHERE clause of your query
so that only those rows that are actually needed are returned. Returning
unnecessary rows wastes SQL Server resources and hurts overall
performance.
SELECT *
FROM Person.Contact
Figure 2
52
Index seeks are completely different from scans, where the engine walks
through the rows to find what it needs. An index seek, clustered or not,
occurs when the optimizer is able to locate an index that it can use to
retrieve the required records. Therefore, it tells the storage engine to
look up the values based on the keys of the given index. Indexes in SQL
Server are stored in a B-tree (a series of nodes that point to a parent). A
clustered index stores not just the key structure, like a regular index,
but also sorts and stores the data, which is the main reason why there
can be only one clustered index per table.
When an index is used in a seek operation, the key values are used to
quickly identify the row, or rows, of data needed. This is similar to
looking up a word in the index of a book to get the correct page
number. The added value of the clustered index seek is that, not only is
the index seek an inexpensive operation as compared to an index scan,
but no extra steps are required to get the data because it is stored in the
index.
In the above example, we have a Clustered Index Seek operation
carried out against the Person.Contact table, specifically on the
PK_Contact_ContactId, which is happens to be both the primary key
and the clustered index for this table.
Note on the ToolTips window for the Clustered Index Seek that the
Ordered property is now true, indicating that the data was ordered by
the optimizer.
SELECT ContactID
FROM Person.Contact
WHERE EmailAddress LIKE 'sab%'
by Microsoft and hopefully will be fixed at some point. No big deal, but
something for you to be aware of.
Figure 3
Like a clustered index seek, a non-clustered index seek uses an index to
look up the rows to be returned directly. Unlike a clustered index seek, a
non-clustered index seek has to use a non-clustered index to perform
the operation. Depending on the query and index, the query optimizer
might be able to find all the data in the non-clustered index, or it might
have to look up the data in the clustered index, slightly hurting
performance due to the additional I/O required to perform the extra
lookups – more on this in the next section.
54
Key LookUp
Let's take our query from the previous sections and alter it so that it
returns just a few more columns:
SELECT ContactID,
LastName,
Phone
FROM Person.Contact
WHERE EmailAddress LIKE 'sab%'
Figure 4
Finally, we get to see a plan that involves more than a single operation!
Reading the plan from right-to-left and top-to-bottom, the first
operation we see is an Index Seek against the
IX_Contact_EmailAddress index. This is a non-unique, non-clustered
index and, in the case of this query, it is non-covering. A non-covering
index is an index that does not contain all of the columns that need to
be returned by a query, forcing the query optimizer to not only read the
index, but to also read the clustered index to gather all the data it needs
so it can be returned.
We can see this in the ToolTips window from the Output List for the
Index Seek, in figure 5, which shows the EmailAddress and
ContactID columns.
Chapter 2: Reading Graphical Execution Plans for Basic Queries 55
Figure 5
The key values are then used in a Key Lookup on the PK_Contact_
ContactID clustered index to find the corresponding rows, with the
56
Figure 6
Chapter 2: Reading Graphical Execution Plans for Basic Queries 57
Figure 7
Typically, a Nested Loops join is a standard type of join and by itself
does not indicate any performance issues. In this case, because a Key
Lookup operation is required, the Nested Loops join is needed to
combine the rows of the Index Seek and Key Lookup. If the Key
Lookup was not needed (because a covering index was available), then
the Nested Loops operator would not be needed and would not appear
in the graphical execution plan.
Table Scan
returned by scanning the table, one row after another. You can see a
table scan operation by executing the following query:
SELECT *
FROM [dbo].[DatabaseLog]
Figure 8
A table scan can occur for several reasons, but it's often because there
are no useful indexes on the table, and the query optimizer has to search
through every row in order to identify the rows to return. Another
common reason why a table scan may occur is when all the rows of a
table are returned, as is the case in this example. When all (or the
majority) of the rows of a table are returned then, whether an index
60
exists or not, it is often faster for the query optimizer to scan through
each row and return them than look up each row in an index. And last,
sometimes the query optimizer determines that it is faster to scan each
row than it is to use an index to return the rows. This commonly occurs
in tables with few rows.
Assuming that the number of rows in a table is relatively small, table
scans are generally not a problem. On the other hand, if the table is
large and many rows are returned, then you might want to investigate
ways to rewrite the query to return fewer rows, or add an appropriate
index to speed performance.
RID LookUp
SELECT *
FROM [dbo].[DatabaseLog]
WHERE DatabaseLogID = 1
Figure 9
To return the results for this query, the query optimizer first performs
an Index Seek on the primary key. While this index is useful in
identifying the rows that meet the WHERE clause criteria, all the
required data columns are not present in the index. How do we know
this?
Chapter 2: Reading Graphical Execution Plans for Basic Queries 61
Figure 10
If you look at the ToolTip above for the Index Seek, we see "Bmk1000"
is in the Output list. This"Bmk1000" is telling us that this Index Seek is
actually part of a query plan that has a bookmark lookup.
Next, the query optimizer performs a RID LookUp, which is a type of
bookmark lookup that occurs on a heap table (a table that doesn't have
a clustered index), and uses a row identifier to find the rows to return.
62
In other words, since the table doesn't have a clustered index (that
includes all the rows), it must use a row identifier that links the index to
the heap. This adds additional disk I/O because two different
operations have to be performed instead of a single operation, which
are then combined with a Nested Loops operation.
Figure 11
In the above ToolTip for the RID Lookup, notice that "Bmk1000" is
used again, but this time in the Seek Predicates section. This is telling us
Chapter 2: Reading Graphical Execution Plans for Basic Queries 63
Table Joins
Up to now, we have worked with single tables. Let's spice things up a bit
and introduce joins into our query. The following query retrieves
employee information, concatenating the FirstName and LastName
columns in order to return the information in a more pleasing manner.
SELECT e.[Title],
a.[City],
c.[LastName] + ', ' + c.[FirstName] AS EmployeeName
FROM [HumanResources].[Employee] e
JOIN [HumanResources].[EmployeeAddress] ed ON e.[EmployeeID]
= ed.[EmployeeID]
JOIN [Person].[Address] a ON [ed].[AddressID] =
[a].[AddressID]
JOIN [Person].[Contact] c ON e.[ContactID] =
c.[ContactID];
Figure 12
With this query there are multiple processing steps occurring, with
varying costs to the processor. The cost accumulates as you move
thorough the execution tree from right to left.
64
From the relative cost displayed below each operator icon, we can
identify the three most costly operations in the plan, in descending
order:
1. The Index Scan against the Person.Address table (45%)
2. The Hash Match join operation between the
HumanResources.EmployeeAddress and Person.Address
(28%)
3. The Clustered Index Seek on the Person.Contact table (18%)
Let's consider each of the operators we see in this plan.
Starting on the right of Figure 12 above, the first thing we see is an
Index Scan against the HumanResources.EmployeeAddress table,
and directly below this is another index scan against the
Person.Address table. The latter was the most expensive operation in
the plan, so let's investigate further. By bringing up the ToolTip, shown
in Figure 13, we can see that the scan was against the index
IX_Address_AddressLine_AddressLine2_City_StateProvinceId_-
PostalCode and that the storage engine had to walk through 19,614
rows to arrive at the data that we needed.
Figure 13
Chapter 2: Reading Graphical Execution Plans for Basic Queries 65
The query optimizer needed to get at the AddressId and the City
columns, as shown by the output list. The optimizer calculated, based
on the selectivity of the indexes and columns in the table, that the best
way to arrive at that data was to walk though the index. Walking through
those 19,614 rows took 45% of the total query cost or an estimated
operator cost of 0.180413. The estimated operator cost is the cost to
the query optimizer for executing this specific operation, which is an
internally calculated number used by the query optimizer to evaluate the
relative costs of specific operations. The lower this number, the more
efficient the operation.
Continuing with the above example, the output of the two index scans
is combined through a Hash Match join, the second most expensive
operation of this execution plan. The ToolTip for this operator is
shown in Figure 14:
Figure 14
66
Before we can talk about what a Hash Match join is, we need to
understand two new concepts: hashing and hash table. Hashing is a
programmatic technique where data is converted into a symbolic form
that makes it easier to be searched for quickly. For example, a row of
data in a table can be programmatically converted into a unique value
that represents the contents of the row. In many ways it is like taking a
row of data and encrypting it. Like encryption, a hashed value can be
converted back to the original data. Hashing is often used within SQL
Server to convert data into a form that is more efficient to work with, or
in this case, to make searching more efficient.
A hash table, on the other hand, is a data structure that divides all of the
elements into equal-sized categories, or buckets, to allow quick access to
the elements. The hashing function determines which bucket an element
goes into. For example, you can take a row from a table, hash it into a
hash value, then store the hash value into a hash table.
Now that we understand these terms, a Hash Match join occurs when
SQL Server joins two tables by hashing the rows from the smaller of
the two tables to be joined, and then inserting them into a hash table,
then processing the larger table one row at a time against the smaller
hashed table, looking for matches where rows need to be joined.
Because the smaller of the tables provides the values in the hash table,
the table size is kept at a minimum, and because hashed values instead
of real values are used, comparisons can be made very quickly. As long
as the table that is hashed is relatively small, this can be a quick process.
On the other hand, if both tables are very large, a Hash Match join can
be very inefficient as compared to other types of joins.
In this example, the data from HumanResources.EmployeeAddress
.AddressId is matched with Person.Address table.
Hash Match joins are often very efficient with large data sets, especially
if one of the tables is substantially smaller than the other. Hash Match
joins also work well for tables that are not sorted on join columns, and
they can be efficient in cases where there are no useable indexes. On the
other hand, a Hash Match join might indicate that a more efficient join
method (Nested Loops or Merge) could be used. For example, seeing a
Hash Match join in an execution plan sometimes indicates:
• a missing or incorrect index
• a missing WHERE clause
• a WHERE clause with a calculation or conversion that makes
it non-sargeable (a commonly used term meaning that the
search argument, "sarg" can't be used). This means it won't use
an existing index.
Chapter 2: Reading Graphical Execution Plans for Basic Queries 67
While a Hash Match join may be the most efficient way for the query
optimizer to join two tables, it does not mean there are not more
efficient ways to join two tables, such as adding appropriate indexes to
the joined tables, reducing the amount of data returned by adding a
more restrictive WHERE clause, or by making the WHERE clause
sargeble. In other words, a seeing a Hash Match join should be a cue for
you to investigate if the join operation can be improved or not. If it can
be improved, then great. If not, then there is nothing else to do, as the
Hash Match join might be the best overall way to perform the join.
Worth noting in this example is the slight discrepancy between the
estimated number of rows returned, 282.216 (proving this is a
calculation since you can't possibly return .216 rows), and the actual
number of rows, 290. A difference this small is not worth worrying
about, but a larger discrepancy indicates that your statistics are out of
date and need to be updated. A large difference can lead to differences
in the Estimated and Actual plans.
The query proceeds from here with another index scan against the
HumanResources.Employee table and another Hash Match between
the results of the first Hash Match and the index scan.
Figure15
Note from the Seek Predicates section in figure 15 above, that the
operation was joining directly between the ContactId column in the
HumanResources.Employee table and the Person.Contact table.
Following the clustered index seek, the data accumulated by the other
operations are joined with the data collected from the seek, through a
Nested Loops Join, as shown in Figure 16.
Chapter 2: Reading Graphical Execution Plans for Basic Queries 69
Figure 16
The nested loops join is also called a nested iteration. This operation
takes the input from two sets of data and joins them by scanning the
outer data set (the bottom operator in a graphical execution plan) once
for each row in the inner set. The number of rows in each of the two
data sets was small, making this a very efficient operation. As long as the
inner data set is small and the outer data set, small or not, is indexed,
this becomes an extremely efficient join mechanism. Unless you have
very large data sets, this is the type of join that you most want to see in
an execution plan.
Compute Scalar
Finally, in the execution plan shown in figure 12, right before the Select
operation, we have a Compute Scalar operation. The Tooltip for this
operator is shown in Figure 19.
70
Figure 19
This is simply a representation of an operation to produce a scalar, a
single defined value, usually from a calculation – in this case, the alias
EmployeeName which combines the columns Contact.LastName
and Contact.FirstName with a comma in between them. While this
was not a zero-cost operation, 0.0000282, it's so trivial in the context of
the query as to be essentially free of cost.
Merge Join
Besides the Hash and Nested Loops Join, the query optimizer can also
perform a Merge Join. To seen an example of a Merge Join, we can run
the following code in the AdventureWorks database:
SELECT c.CustomerID
FROM Sales.SalesOrderDetail od
JOIN Sales.SalesOrderHeader oh
ON od.SalesOrderID = oh.SalesOrderID
JOIN Sales.Customer c
ON oh.CustomerID = c.CustomerID
Figure 17
According to the execution plan, the query optimizer performs a
Clustered Index Scan on the Customer table and a non-clustered Index
Scan on the SalesOrderHeader table. Since a WHERE clause was not
specified in the query, a scan was performed on each table to return all
the rows in each table.
Next, all the rows from both the Customer and SalesOrderHeader
tables are joined using the Merge Join operator. A Merge Join occurs
on tables where the join columns are presorted. For example, in the
ToolTip window for the Merge Join, shown in figure 18, we see that the
join columns are Sales and CustomerID. In this case, the data in the
join columns are presorted in order. A Merge Join is an efficient way to
join two tables, when the join columns are presorted but if the join
columns are not presorted, the query optimizer has the option of a)
sorting the join columns first, then performing a Merge Join, or b)
performing a less efficient Hash Join. The query optimizer considers all
the options and generally chooses the execution plan that uses the least
resources.
72
Figure 18
Once the Merge Join has joined two of the tables, the third table is
joined to the first two using a Hash Match Join, which was discussed
earlier. And finally, the joined rows are returned.
The key to performance of a Merge Join is that the joined columns are
already presorted. If they are not, and the query optimizer chooses to
sort the data before it performs a Merge Join, and this might be an
indication that a Merge Join is not an ideal way to join the tables, or it
might indicate that you need to consider some different indexes.
Chapter 2: Reading Graphical Execution Plans for Basic Queries 73
SELECT e.[Title],
a.[City],
c.[LastName] + ',' + c.[FirstName] AS EmployeeName
FROM [HumanResources].[Employee] e
JOIN [HumanResources].[EmployeeAddress] ed
ON e.[EmployeeID] = ed.[EmployeeID]
JOIN [Person].[Address] a
ON [ed].[AddressID] = [a].[AddressID]
JOIN [Person].[Contact] c
ON e.[ContactID] = c.[ContactID]
WHERE e.[Title] = 'Production Technician - WC20' ;
Figure 20
Starting from the right, we see that the optimizer has used the criteria
from the WHERE clause to do a clustered index scan, using the
primary key. The WHERE clause limited the number of rows to 22,
which you can see by hovering your mouse pointer over the arrow
coming out of the Clustered Index Scan operator (see figure 21).
74
Figure 21
The optimizer, using the available statistics, was able to determine this
up front, as we see by comparing the estimated and actual rows returned
in the ToolTip.
Working with a smaller data set and a good index on the
Person.Contact table, as compared to the previous query, the optimizer
was able to use the more efficient Nested Loop Join. Since the
optimizer changed where that table was joined, it also moved the scalar
calculation right next to the join. Since it's still only 22 rows coming out
of the scalar operation, a clustered index seek and another nested loop
Chapter 2: Reading Graphical Execution Plans for Basic Queries 75
Sort
SELECT *
FROM [Production].[ProductInventory]
ORDER BY [Shelf]
Figure 22
The Clustered Index Scan operator outputs into the Sort operator.
Compared to many of the execution plan icons, the Sort operator is
very straightforward. It literally is used to show when the query
optimizer is sorting data within the execution plan. If an ORDER BY
clause does not specify order, the default order is ascending, as you will
see from the ToolTip for the Sort icon (see figure 23 below).
Chapter 2: Reading Graphical Execution Plans for Basic Queries 77
Figure 23
If you pull up the ToolTip window for the Sort icon (see figure 24),
you'll see that the Sort operator is being passed 1069 rows. The Sort
operator takes these 1069 rows from the Clustered Index Scan, sorts
them, and then passes the 1069 rows back in sorted order.
78
Figure 24
The most interesting point to note is that the Sort operation is 76% of
the cost of the query. There is no index on this column, so the Sort
operation is done within the query execution.
As a rule-of-thumb, I would say that when sorting takes more than 50%
of a query's total execution time, then you need to carefully review it to
ensure that it is optimized. In our case the reason why we are breaking
this rule is fairly straightforward: we are missing a WHERE clause.
Most likely, this query is returning more rows to be sorted than needs to
be returned. However, even if a WHERE clause exists, you need to
ensure that it limits the amount of rows to only the required number of
rows to be sorted, not rows that will never be used.
Other things to consider are:
Is the sort really necessary? If not, remove it to reduce overhead.
Is it possible to have the data presorted so it doesn't have to be sorted?
For example, can a clustered index be used that already sorts the data in
the proper order? This is not always possible, but if it is, you will save
sorting overhead if you create the appropriate clustered index.
If an execution plan has multiple Sort operators, review the query to see
if they are all necessary, or if the code can be rewritten so that fewer
sorts are needed to accomplish the goal of the query.
If we change the query to the following:
SELECT *
FROM [Production].[ProductInventory]
ORDER BY [ProductID]
Figure 25
Although this query is almost identical to the previous query, and it
includes an ORDER BY clause, we don't see a sort operator in the
execution plan. This is because the column we are sorting by has
changed, and this new column has a clustered index on it, which means
that the returned data does not have to be sorted again, as it is already
sorted as a byproduct of it being the clustered index. The query
optimizer is smart enough to recognize that the data is already ordered,
and does not have to order it again. If you have no choice but to sort a
lot of data, you should consider using the SQL Server 2005 Profiler to
see if any Sort Warnings are generated. To boost performance, SQL
Server 2005 attempts to perform sorting in memory instead of disk.
Sorting in RAM is much faster than sorting on disk. But if the sort
operation is large, SQL Server may not be able to sort the data in
memory, instead, having to write data to the tempdb database.
Whenever this occurs, SQL Server generates a Sort Warning event,
which can be captured by Profiler. If you see that your server is
performing a lot of sorts, and many Sort Warnings are generated, then
you may need to add more RAM to your server, or to speed up tempdb
access.
Earlier in this chapter, we took a look at the Hatch Match operator for
joins. This same Hatch Match operator also can occur when
aggregations occur within a query. Let's consider a simple aggregate
query against a single table using the COUNT operator:
SELECT [City],
COUNT([City]) AS CityCount
FROM [Person].[Address]
GROUP BY [City]
Figure 26
The query execution begins with an Index Scan, because all of the rows
are returned for the query. There is no WHERE clause to filter the
rows. These rows then need to be aggregated in order to perform the
requested COUNT aggregate operation. In order for the query
optimizer to count each row for each separate city, it must perform a
Hatch Match operation. Notice that underneath Hatch Match in the
execution plan that the word "aggregate" is put between parentheses.
This is to distinguish it from a Hatch Match operation for a join. As
with a Hatch Match with a join, a Hatch Match with an aggregate causes
SQL Server to create a temporary hash table in memory in order to
count the number of rows that match the GROUP BY column, which
in this case is "City." Once the results are aggregated, then the results
are passed back to us.
Quite often, aggregations with queries can be expensive operations.
About the only way to "speed" the performance of an aggregation via
code is to ensure that you have a restrictive WHERE clause to limit the
number of rows that need to be aggregated, thus reducing the amount
of aggregation that needs to be done.
Filter
SELECT [City],
COUNT([City]) AS CityCount
FROM [Person].[Address]
GROUP BY [City]
HAVING COUNT([City]) > 1
Figure 27
By adding the HAVING clause, the Filter operator has been added to
the execution plan. We see that the Filter operator is applied to limit the
output to those values of the column, City, that are greater than 1. One
useful bit of knowledge to take away from this plan is that the
HAVING clause is not applied until all the aggregation of the data is
complete. We can see this by noting that the actual number of rows in
the Hash Match operator is 575 and in the Filter operator it's 348.
Figure 28
While adding a HAVING clause reduces the amount of data returned,
it actually adds to the resources needed to produce the query results,
because the HAVING clause does not come into play until after the
aggregation. This hurts performance. As with the previous example, if
you want to speed the performance of a query with aggregations, the
only way to do so in code is to add a WHERE clause to the query to
limit the number of rows that need to be selected and aggregated.
82
occur. For example, if any of the following six operators occur, the
rebind and rewind counts are populated:
• Nonclustered Index Spool
• Remote Query
• Row Count Spool
• Sort
• Table Spool
• Table-Valued Function
If the following operators occur, the rebind and rewind counts will only
be populated when the StartupExpression for the physical operation is
set to TRUE, which can vary depending on how the query optimizer
evaluates the query. This is set by Microsoft in code and is something
we have no control over.
• Assert
• Filter
And for all other physical operators, they are not populated. In these
cases, the counts for rebind and rewind will be zero. This zero count
does not mean that zero rebinds or rewinds occurred, just that these
values were not populated. As you can imagine, this can get a little
confusing. This also explains why most of the time you see zero values
for rebind and rewind.
So, what does it mean when you see a value for either rebind or rewind
for the eight operators where rebind and rewind may be populated?
If you see an operator where rebind equals one and rewinds equals zero,
this means that an Init() method was called one time on a physical
operator that is NOT on the inner side of a loop join. If the physical
operator is ON the inner side of a loop join used by an operator, then
the sum of the rebinds and rewinds will equal the number of rows
process on the outer side of a join used by the operator
So how is this helpful to the DBA? Generally speaking, it is ideal if the
rebind and rewind counts are as low as possible, as higher counts
indicate more disk I/O. If the counts are high, it might indicate that a
particular operator is working harder than it needs to, hurting server
performance. If this is the case, it might be possible to rewrite the
query, or modify current indexing, to use a different query plan that uses
fewer rebinds and rewinds, reducing I/O and boosting performance.
84
Insert Statements
Here is a very simple INSERT statement:
This statement generates this rather interesting estimated plan (so that I
don't actually affect the data within the system), shown in Figure 29.
Figure 29
The execution plan starts off, reading right to left, with an operator that
is new to us: Constant Scan. This operator introduces a constant
number of rows into a query. In our case, it's building a row in order for
the next two operators to have a place to add their output. The first of
these is a Compute Scalar operator to call a function called
getidentity. This is the moment within the query plan when an identity
value is generated for the data to follow. Note that this is the first thing
Chapter 2: Reading Graphical Execution Plans for Basic Queries 85
done within the plan, which helps explain why, when an insert fails, you
get a gap in the identity values for a table.
Another scalar operation occurs which outputs a series of placeholders
for the rest of the data and creates the new uniqueidentifier value, and
the date and time from the GETDATE function. All of this is passed
to the Clustered Index Insert operator, where the majority of the cost
of this plan is realized. Note the output value from the INSERT
statement, the Person.Address.StateProvinceId. This is passed to the
next operator, the Nested Loop join, which also gets input from the
Clustered Index Seek against the Person.StateProvince table. In
other words, we had a read during the INSERT to check for referential
integrity on the foreign key of StateProvinceId. The join then outputs
a new expression which is tested by the next operator, Assert. An
Assert verifies that a particular condition exists. This one checks that
the value of Expr1014 equals zero. Or, in other words, that the data that
was attempted to be inserted into the
Person.Address.StateProvinceId field matched a piece of data in the
Person.StateProvince table; this was the referential check.
Update Statements
Consider the following update statement:
UPDATE [Person].[Address]
SET [City] = 'Munro',
[ModifiedDate] = GETDATE()
WHERE [City] = 'Monroe' ;
Figure 30
Let's begin reading this execution plan, from right to left. The first
operator is a non-clustered Index Scan, which retrieves all of the
necessary rows from a non-clustered index, scanning through them, one
row at a time. This is not particular efficient and should be a flag to you
that perhaps the table needs better indexes to speed performance. The
purpose of this operator is to identify all the rows WHERE [City] =
'Monroe', and then send them to the next operator.
86
Delete Statements
What kind of execution plan is created with a DELETE statement?
For example, let's run the following code and check out the execution
plan.
Chapter 2: Reading Graphical Execution Plans for Basic Queries 87
Figure 31
I know this is a bit difficult to read. I just wanted to show how big a
plan is necessary to delete data within a relational database. Remember,
removing a row, or rows, is not an event isolated to the table in question.
Any tables related to the primary key of the table where we are
removing data will need to be checked, to see if removing this piece of
data affects their integrity. To a large degree, this plan looks more like a
SELECT statement than a DELETE statement.
Starting on the right, and reading top to bottom, we immediately get a
Clustered Index Delete operator. There are a couple of interesting
points in this operation. The fact that the delete occurs at the very
beginning of the process is good to know. The second interesting fact is
that the Seek Predicate on this Clustered Index Seek To Delete
operation was:
Prefix: [AdventureWorks].[Person].[Address].AddressID = Scalar
Operator(CONVERT_IMPLICIT(int,[@1],0)).
This means that a parameter, @1, was used to look up the AddressId.
If you'll notice in the code, we didn't use a parameter, but rather used a
constant value, 52. Where did the parameter come from? This is an
indication of the query engine generating a reusable query plan, as per
the rules of simple parameterization.
88
Figure 32
After the delete, a series of Index and Clustered Index Seeks and Scans
are combined through a series of Nested Loop Join operators. These
are specifically Left Semi Joins. These operators return a value if the
join predicate between the two tables in question matches or if there is
no join predicate supplied. Each one returns a value. Finally, at the last
step, an Assert operator, the values returned from each Join, all the
tables related to the table from which we're attempting to delete data,
are checked to see if referential data exists. If there is none, the delete is
completed. If they do return a value, an error would be generated, and
the DELETE operation aborted.
Chapter 2: Reading Graphical Execution Plans for Basic Queries 89
Figure 33
Summary
This chapter represents a major step in learning how to read graphical
execution plans. However, as we discussed at the beginning of this
chapter, we only focused on the most common type of operators and
we only looked at simple queries. So if you decide to analyze a 200-line
query and get a graphical execution plan that is just about as long, don't
expect to be able to analyze it immediately. Learning how to read and
analyze execution plans takes time and effort. But once you gain some
experience, you will find that it becomes easier and easier to read and
analyze, even for the most complex of execution plans.
Chapter 3: Text and XML Execution Plans for Basic Queries 91
7 SQL Server 2005 still offers text-based execution plans, but only for
backward-compatibility.
92
SELECT ContactID,
LastName,
Phone
FROM Person.Contact
WHERE EmailAddress LIKE 'sab%'
Figure 1
Now, we'll capture the equivalent text plan. Remember that turning on
SHOWPLAN_ALL will allow you to collect estimated execution plans.
No T-SQL code submitted after this statement is actually executed, until
you turn SHOWPLAN_ALL off again:
SET SHOWPLAN_ALL ON ;
GO
SELECT ContactID,
LastName,
Phone
FROM Person.Contact
WHERE EmailAddress LIKE 'sab%'
Chapter 3: Text and XML Execution Plans for Basic Queries 93
GO
Figure 2
Row 1 is the parent node, and the StmtText column for this row
contains the text for the TSQL statement that was executed. If you
scroll right through the results, you'll find a column called Type, which
describes the node type.
Figure 3
For the parent node this will contain the type of SQL statement that
was executed (SELECT, in this case). For all other rows the type is
PLAN_ROW. For all PLAN_ROW nodes, the StmtText column
describes the type of operation that took place.
A quick glance at the StmtText column for the remaining rows reveals
that three operations took place: a Nested Loops inner join, an index
seek, and a clustered index seek.
Unlike for graphical plans, there's no easy "right to left and top to
bottom" route through the text-based plans. In order to understand the
flow of operations, we are helped by the indentation of the data and the
use of pipe (|) to connect the statements, parent-to-child. We can also
refer to the NodeID and Parent columns, which indicate the IDs of
8If you right-click in the query window of SSMS, you can select Results
To | Results to Text, which offers a more conventional view of the text
execution plan.
94
the current node and its parent node, respectively. Within each
indentation, or for every row that has the same Parent number, the
operators execute from top to bottom. In this example, the Index Seek
occurs before the Clustered Index Seek.
Moving to the two most-indented rows, we start at row 3 (NodeId 3)
with the Index Seek operation. By extending the StmtText column (or
examining the Argument column) we can see that the index seek was
against the Person.Contact table:
OBJECT:([AdventureWorks].[Person].[Contact].[IX_Contact_EmailAdd
ress]), SEEK:([AdventureWorks].[Person].[Contact].[EmailAddress]
>= N'sab' AND [AdventureWorks].[Person].[Contact].[EmailAddress]
< N'saC'),
WHERE:([AdventureWorks].[Person].[Contact].[EmailAddress] like
N'sab%') ORDERED FORWARD
Notice this time, from the EstimateRows column, that this operation
produces only 1 row. However, if you examine the final column in the
result grid, EstimateExecutions, you'll see that the operator is called
an estimated 19.8 times a while running the query.
Chapter 3: Text and XML Execution Plans for Basic Queries 95
SET SHOWPLAN_ALL ON ;
GO
SELECT c.[LastName],
a.[City],
cu.[AccountNumber],
st.[Name] AS TerritoryName
FROM [Person].[Contact] c
JOIN [Sales].[Individual] i
ON c.[ContactID] = i.[ContactID]
JOIN [Sales].[CustomerAddress] ca
ON i.[CustomerID] = ca.[CustomerID]
JOIN Person.Address a
ON [ca].[AddressID] = [a].[AddressID]
JOIN [Sales].Customer cu
ON cu.[CustomerID] = i.[CustomerID]
JOIN [Sales].[SalesTerritory] st
ON [cu].[TerritoryID] = [st].[TerritoryID]
WHERE st.[Name] = 'Northeast'
AND a.[StateProvinceID] = 55 ;
GO
96
When you execute this query, the estimated plan is shown in the results
pane. Figure 4 shows the StmtTexfirst column of the results.
Figure 4
This is where the indentation of the data, and the use of pipe (|)
character to connect parent to child, really starts to be useful. Tracking
to the most internal set of statements, we see an index seek operation
against IX_Address_StateProvinceId on the Address table.
Figure 5
This is how the plan displays the WHERE clause statement that limits
the number of rows returned.
--Index Seek(
OBJECT:([AdventureWorks].[Person].[Address].[IX_Address_StatePro
vinceID] AS [a]), SEEK:([a].[StateProvinceID]=(55)) ORDERED
FORWARD)
The output from this operator is the AddressId, not a part of the
SELECT list, but necessary for the operators that follow. This operator
starts the query with a minimum number of rows to be used in all the
subsequent processing.
The index seek is followed by a clustered index seek against the
PersonAddress table clustered index, using the AddressId from the
Chapter 3: Text and XML Execution Plans for Basic Queries 97
index seek. Again, in the StmtText column, we see that the clustered
index seek operation is actually a key lookup operation.
--Clustered Index Seek
(OBJECT:([AdventureWorks].[Person].[Address].[PK_Address_Address
ID] AS [a]),
SEEK:([a].[AddressID]=[AdventureWorks].[Person].[Address].[Addre
ssID] as [a].[AddressID]) LOOKUP ORDERED FORWARD)
Stepping out one level, the output from these two operations is joined
via a nested loop join (row 8).
Figure 6
Following the pipes down from this row, we reach row 11, which holds
one of the costliest operations in this query, an index scan against the
entire CustomerAddress index, AK_CustomerAddress_rowguid
--Index Scan(
OBJECT:([AdventureWorks].[Sales].[CustomerAddress].[AK_CustomerA
ddress_rowguid] AS [ca]))
This processes 19,000 + rows in order to provide output for the next
step out, a Hash Match join between Address and CustomerAddress.
Figure 7
Following the pipe characters down from the hash Match, we arrive at a
Computer Scalar operation (row 12). If we step back in for one step, we
see that the Computer Scalar is fed by a Clustered Index Seek operation
against the Pk_Customer_CustomerId. Its output then goes to the
98
Figure 8
Stepping up one more time, we have to compute a scalar for the
AccountNumber column, since it is a calculated column using the
function listed above.
Figure 9
Following down on the same level, using the pipe (|) connectors, the
next operator is the clustered index seek against the
PK_Individual_CustomerId.
Stepping out and up again, the output from these operators is combined
using a Nested Loop, and following the pipes down, through an
increasing number of rows in the text plan's result set from Row 4 to
Row 15, we get a clustered index seek against the Person.Contact table.
Chapter 3: Text and XML Execution Plans for Basic Queries 99
Figure 10
Stepping out again, and back up to Row 3, we see that the output of the
Nested Loop join in row 4 and the Clustered Index seek in row 15 are
combined once again in a Nested Loop join.
Figure 11
Following the pipes down to Row 16, we get a final clustered index seek
on the Sales.SalesTerritory table. Stepping out for the last time, the
last operator performs the final Nested Loop join in this query, with the
final output list showing the columns necessary for generating the actual
result set.
As you see for yourself, reading text-based execution plans is not easy,
and we have only taken a brief look at a couple of very simple queries.
Longer queries generate much more complicated plans, sometimes
running to dozens of pages long. While it's sometimes handy to know
how to read text-based execution plans, I would suggest you focus on
graphical execution plans, unless you have some special need where only
text-based execution plans will meet your needs.
This limitation was removed in SQL Server 2005, with the introduction
of the XML Plan. To most people, an XML Plan is simply a common
file format in which to store a graphical execution plan, so that it can be
shared with other DBAs and developers.
I would imagine that very few people would actually prefer to read
execution plans in XML format. Having said that, here are two reasons
why you might want to do so.
Firstly, there is undocumented data stored in an XML Plan that is not
available when it is displayed as a graphical execution plan. For example,
XML Plans include such additional data as cached plan size, memory
fractions (how memory grant is to be distributed across operators in a
query plan), parameter list with values used during optimization, and
missing indexes information. In most cases, the typical DBA won't be
interested in this information, or there are easier ways to gather this
same information, such as using Database Engine Tuning Wizard to
identify missing indexes.
Secondly, XML plans display a lot of details, and its inherent search
capabilities make it relatively easy for an experienced DBA to track
down specific, potentially problematic aspects of the query, by doing a
search on specific terms, such as "index scan".
XML Plans can also be used in Plan Forcing, covered in Chapter 7,
whereby you essentially dictate to the query optimizer that it should use
only the plan you specify to execute the query.
In the following section, we take a brief look at the structure of XML
Plans.
SET SHOWPLAN_XML ON ;
GO
SELECT c.[LastName],
a.[City],
cu.[AccountNumber],
st.[Name] AS TerritoryName
FROM [Person].[Contact] c
JOIN [Sales].[Individual] i
ON c.[ContactID] = i.[ContactID]
JOIN [Sales].[CustomerAddress] ca
ON i.[CustomerID] = ca.[CustomerID]
JOIN Person.Address a
ON [ca].[AddressID] = [a].[AddressID]
JOIN [Sales].Customer cu
ON cu.[CustomerID] = i.[CustomerID]
JOIN [Sales].[SalesTerritory] st
ON [cu].[TerritoryID] = [st].[TerritoryID]
WHERE st.[Name] = 'Northeast'
AND a.[StateProvinceID] = 55 ;
GO
Click on the link to the XML document, and the plan will open up in a
new tab:
Figure 12
102
The results are far too large to output here, but Figure 12 shows the
opening portion of the resulting XML file. XML data is more difficult
to take in all at once than the graphical execution plans but, with the
ability to expand and collapse elements, using the "+" and "-" nodules
down the left hand side, the hierarchy of the data being processed
quickly becomes clearer.
A review of some of the common elements and attributes and the full
schema is available here:
http://schemas.microsoft.com/sqlserver/2004/07/showplan/
The information at this link is designed for those familiar with XML
and who want to learn more about the schema in order to use the data
programmatically. 9
After the familiar BatchSequence, Batch, Statements and Stmt
Simple elements (described in Chapter 1), the first point of real interest
in the physical attributes of the QueryPlan:
This describes the size of the plan in the cache, along with the amount
of time, CPU cycles and memory used by the plan.
Next in the execution plan, we see an element labeled MissingIndexes.
This contains information about tables and columns that did not have
an index available to the execution plan created by the optimizer. While
the information about missing indexes can sometimes be useful, it is
generally easier to identify missing index using a tool, such as the
Database Engine Tuning Wizards, which not only uses this information,
but uses additional information to identify potentially missing indexes.
<MissingIndexes>
<MissingIndexGroup Impact="30.8535">
<MissingIndex Database="[AdventureWorks]"
Schema="[Sales]" Table="[CustomerAddress]">
<ColumnGroup Usage="EQUALITY">
<Column Name="[AddressID]" ColumnId="2" />
</ColumnGroup>
<ColumnGroup Usage="INCLUDE">
<Column Name="[CustomerID]" ColumnId="1" />
</ColumnGroup>
</MissingIndex>
</MissingIndexGroup>
</MissingIndexes>
The execution plan then lists, via the RelOP nodes, the various physical
operations that it anticipates performing, according to the data supplied
by the optimizer. The first node, with NodeId=0, refers to the final
NestedLoop operation:
The information that is displayed here will be familiar to you from the
ToolTip window for the graphical plans. Notice that, unlike for the text
plans, which just displayed EstimateExecutions, the XML plan the
estimated number of rebinds and rewinds. This can often give you a
more accurate idea of what occurred within the query, such as how
many times the operator was executed.
For example, for NodeId=26, the final clustered index seek, associated
with the Nested Loops join in NodeId=0, we see:
Whereas in the text plan for this query, we simply saw "Estimate
Executions=15.24916".
104
<OutputList>
<ColumnReference Database="[AdventureWorks]"
Schema="[Person]" Table="[Contact]" Alias="[c]"
Column="LastName" />
<ColumnReference Database="[AdventureWorks]"
Schema="[Person]" Table="[Address]" Alias="[a]"
Column="City" />
<ColumnReference Table="[cu]" Column="AccountNumber"
ComputedColumn="1" />
<ColumnReference Database="[AdventureWorks]"
Schema="[Sales]" Table="[SalesTerritory]" Alias="[st]"
Column="Name" />
</OutputList>
This makes XML not only easier to read, but much more readily
translated directly back to the original query. The output described
above is from the references to the schema "Person" and the tables
"Contact" (aliased as "c"), "Address" (aliased as "a") and
"SalesTerritory" (aliased as "st"), in order to output the required
columns (LastName, City, AccountNumber and Name). The names of
the operator elements are the same as the operators you would see in
the graphical plans and the details within the attributes are usually those
represented in the ToolTip windows or in the Properties window.
Finally for Node 0, in the estimated plan, we see some more
information about the Nested Loops operation, such as the table
involved, along with the table's alias.
<NestedLoops Optimized="0">
<OuterReferences>
<ColumnReference Database="[AdventureWorks]"
Schema="[Sales]" Table="[Customer]" Alias="[cu]"
Column="TerritoryID" />
</OuterReferences>
SELECT c.[LastName],
a.[City],
cu.[AccountNumber],
st.[Name] AS TerritoryName
FROM [Person].[Contact] c
JOIN [Sales].[Individual] i
ON c.[ContactID] = i.[ContactID]
JOIN [Sales].[CustomerAddress] ca
ON i.[CustomerID] = ca.[CustomerID]
JOIN Person.Address a
ON [ca].[AddressID] = [a].[AddressID]
JOIN [Sales].Customer cu
ON cu.[CustomerID] = i.[CustomerID]
JOIN [Sales].[SalesTerritory] st
ON [cu].[TerritoryID] = [st].[TerritoryID]
WHERE st.[Name] = 'Northeast'
AND a.[StateProvinceID] = 55 ;
GO
When we look at the Actual plan, we see that the QueryPlan has some
additional information such as the DegreeOfParallelism (more on
parallelism in Chapter 7) and the MemoryGrant, the amount of
memory needed for the execution of the query:
The other major difference between the Actual XML execution plan
and the estimated one is that the actual plan includes an element called
RunTimeInformation, showing the thread, actual rows and number of
executions prior to the same final nested loop information. While this
additional information can sometimes be interesting, it generally is not
relevant to most query performance analysis.
<RunTimeInformation>
<RunTimeCountersPerThread Thread="0" ActualRows="4"
ActualEndOfScans="1" ActualExecutions="1" />
</RunTimeInformation>
<NestedLoops Optimized="0">…
106
Summary
As you can see, trying to read an XML plan not an easy task, and one
that most DBA's won't want to spend their time mastering, unless you
are the kind of DBA who likes to know every internal detail, or who
wants to learn how to access the data programmatically. If this is really
the case, you need to first master XML before you take on learning the
specifics of XML plans.
Instead, DBAs should focus on understanding the benefits of having an
execution plan in a portable format such as an XML plan, and how it
can be shared among other DBAs and applications. This is practical
knowledge that you can use almost every day in your DBA work.
Chapter 4: Understanding More Complex Query Plans 107
Stored Procedures
The best place to get started is with stored procedures. We'll create a
new one for AdventureWorks:
SELECT [st].[SalesTaxRateID],
[st].[Name],
[st].[TaxRate],
[st].[TaxType],
[sp].[Name] AS StateName
FROM [Sales].[SalesTaxRate] st
JOIN [Person].[StateProvince] sp
ON [st].[StateProvinceID] =
[sp].[StateProvinceID]
WHERE [sp].[CountryRegionCode] = @CountryRegionCode
ORDER BY [StateName]
GO
Figure 1
Starting from the right, as usual, we see a Clustered Index Scan
operator, which gets the list of States based on the parameter,
@CountryRegionCode, visible in the ToolTip or the Properties
window. This data is then placed into an ordered state by the Sort
operator. Data from the SalesTaxRate table is gathered using an Index
Seek operator as part of the Nested Loop join with the sorted data
from the States table.
Next, we have a Key Lookup operator. This operator takes a list of
keys, like those supplied from Index Seek on the AK_CountryRegion
_Name index from the SalesTaxRate table, and gets the data from
where it is stored, on the clustered index. This output is joined to the
output from the previous Nested Loop within another Nested Loop
join for the final output to the user.
While this plan isn't complex, the interesting point is that we don't have
a stored procedure in sight. Instead, the T-SQL within the stored
procedure is treated in the same way as if we had written and run the
SELECT statement through the Query window.
Derived Tables
One of the ways that data is accessed through T-SQL is via a derived
table. If you are not familiar with derived tables, think of a derived
table as a virtual table that's created on the fly from within a SELECT
statement.
You create derived tables by writing a second SELECT statement
within a set of parentheses in the FROM clause of an outer SELECT
query. Once you apply an alias, this SELECT statement is treated as a
table. In my own code, one place where I've come to use derived tables
Chapter 4: Understanding More Complex Query Plans 109
frequently is when dealing with data that changes over time, for which I
have to maintain history.
SELECT [p].[Name],
[p].[ProductNumber],
[ph].[ListPrice]
FROM [Production].[Product] p
INNER JOIN [Production].[ProductListPriceHistory] ph
ON [p].[ProductID] = ph.[ProductID]
AND ph.[StartDate] = ( SELECT TOP ( 1 )
[ph2].[StartDate]
FROM [Production].[ProductListPriceHistory]
ph2
WHERE [ph2].[ProductID] = [p].[ProductID]
ORDER BY [ph2].[StartDate] DESC
Figure 2
What appears to be a somewhat complicated query turns out to have a
fairly straightforward execution plan. First, we get the two Clustered
Index Scans against Production.Product and Production.Product
ListPriceHistory. These two data streams are combined using the
Merge Join operator.
110
The Merge Join requires that both data inputs be ordered on the join
key, in this case, ProductId. The data resulting from a clustered index is
always ordered, so no additional operation is required here.
A Merge Join takes a row each from the two ordered inputs and
compares them. Because the inputs are sorted, the operation will only
scan each input one time (except in the case of a many-to-many join;
more on that further down). The operation will scan through the right
side of the operation until it reaches a row that is different from the row
on the left side. At that point it will progress the left side forward a row
and begin scanning the right side until it reaches a changed data point.
This operation continues until all the rows are processed. With the data
already sorted, as in this case, it makes for a very fast operation.
Although we don't have this situation in our example, Merge Joins that
are many-to-many create a worktable to complete the operation. The
creation of a worktable adds a great deal of cost to the operation
although it will generally still be less costly than the use of a Hash Join,
which is the other choice the query optimizer can make. We can see that
no worktable was necessary here because the ToolTips property labeled
Many-To-Many (see figure below) that is set to "False".
Chapter 4: Understanding More Complex Query Plans 111
Figure 3
Next, we move down to the Clustered Index Seek operation in the lower
right. Interestingly enough, this process accounts for 67% of the cost of
the query because the seek operation returned all 395 rows from the
query, only limited to the TOP (1) after the rows were returned. A scan
in this case may have worked better because all the rows were returned.
The only way to know for sure would be to add a hint to the query to
force a table scan and see if performance is better or worse.
The Top operator simply limits the number of returned rows to the
value supplied within the query, in this case "1."
The Filter operator is then applied to limit the returned values to only
those where the dates match the main table. In other words, a join
occurred between the [Production].[Product] table and the
[Production].[ProductListPriceHistory] table, where the column
[StartDate] is equal in each. See the ToolTip in Figure 4:
112
Figure 4
The two data feeds are then joined through a Nested Loops operator, to
produce the final result.
If you are not familiar with the APPLY operator, check out:
http://technet.microsoft.com/en-us/library/ms175156.aspx
Below is an example of the rewritten query. Remember, both queries
return identical data, they are just written differently.
SELECT [p].[Name],
[p].[ProductNumber],
[ph].[ListPrice]
FROM [Production].[Product] p
CROSS APPLY ( SELECT TOP ( 1 )
[ph2].[ProductID],
[ph2].[ListPrice]
FROM
[Production].[ProductListPriceHistory] ph2
WHERE [ph2].[ProductID] =
[p].[ProductID]
ORDER BY [ph2].[StartDate] DESC
) ph
Figure 5
The TOP statement is now be applied row-by-row within the control
of the APPLY functionality, so the second index scan against the
ProductListPriceHistory table, and the merge join that joined the
tables together, are no longer needed. Furthermore, only the Index
Seek and Top operations are required to provide data back for the
Nested Loops operation.
So which method of writing this query is more efficient? One way to
find out is to run each query with the SET STATISTICS IO option set
to ON. With this option set, IO statistics are displayed as part of the
Messages returned by the query.
When we run the first query, which uses the subselect, the results are:
114
Although both queries returned identical result sets, the query with the
subselect query uses fewer logical reads (795) verses the query written
using the derived table (1008 logical reads).
This gets more interesting if we add the following WHERE clause to
each of the previous queries:
When we re-run the original query with the added WHERE clause, we
get the plan shown in Figure 6:
Figure 6
The Filter operator is gone but, more interestingly, the costs have
changed. Instead of index scans and the inefficient (in this case) index
seeks mixed together, we have three, clean Clustered Index Seeks with
an equal cost distribution.
If we add the WHERE clause to the derived table query, we see the
plan shown in Figure 7:
Chapter 4: Understanding More Complex Query Plans 115
Figure 7
The plan is almost identical to the one seen in Figure 5, with the only
change being that the Clustered Index Scan has changed to a
Clustered Index Seek, because the inclusion of the WHERE clause
allows the optimizer to take advantage of the clustered index to identify
the rows needed, rather than having to scan through them all in order to
find the correct rows to return.
Now, let's compare the IO statistics for each of the queries, which
return the same physical row.
When we run the query with the subselect, we get:
(1 row(s) affected)
Table 'ProductListPriceHistory'. Scan count 1, logical reads 4,
physical reads 0, read-ahead reads 0, lob logical reads 0, lob
physical reads 0, lob read-ahead reads 0.
Table 'Product'. Scan count 0, logical reads 2, physical reads
0, read-ahead reads 0, lob logical reads 0, lob physical reads
0, lob read-ahead reads 0.
Now, with the addition of a WHERE clause, the derived query is more
efficient, with only 2 logical reads, versus the subselect query with 4
logical reads.
The lesson to learn here is that in one set of circumstances a particular
T-SQL method may be exactly what you need, and yet in another
circumstance that same syntax impacts performance. The Merge join
made for a very efficient query when we were dealing with inherent
scans of the data, but was not used, nor applicable, when the
116
introduction of the WHERE clause reduced the data set. With the
WHERE clause in place the subselect became, relatively, more costly to
maintain when compared to the speed provided by the APPLY
functionality. Understanding the execution plan makes a real difference
in deciding which of these to apply.
10 For more details on the CTE check out this article in Simple-Talk:
http://www.simple-talk.com/sql/sql-server-2005/sql-server-2005-
common-table-expressions/.
Chapter 4: Understanding More Complex Query Plans 117
ON e.[ContactID] = c.[ContactID]
WHERE e.[EmployeeID] = @EmployeeID
UNION ALL
SELECT e.[EmployeeID], e.[ManagerID], c.[FirstName],
c.[LastName], e.[Title], [RecursionLevel] + 1
-- Join recursive member to anchor
FROM [HumanResources].[Employee] e
INNER JOIN [EMP_cte]
ON e.[EmployeeID] = [EMP_cte].[ManagerID]
INNER JOIN [Person].[Contact] c
ON e.[ContactID] = c.[ContactID]
)
-- Join back to Employee to return the manager name
SELECT [EMP_cte].[RecursionLevel],
[EMP_cte].[EmployeeID], [EMP_cte].[FirstName],
[EMP_cte].[LastName],
[EMP_cte].[ManagerID], c.[FirstName] AS
'ManagerFirstName', c.[LastName] AS 'ManagerLastName'
-- Outer select from the CTE
FROM [EMP_cte]
INNER JOIN [HumanResources].[Employee] e
ON [EMP_cte].[ManagerID] = e.[EmployeeID]
INNER JOIN [Person].[Contact] c
ON e.[ContactID] = c.[ContactID]
ORDER BY [RecursionLevel], [ManagerID], [EmployeeID]
OPTION (MAXRECURSION 25)
END;
Figure 8
118
A Nested Loops join takes the data from Clustered Index Seeks
against HumanResources.Employee and Person.Contact. The
Scalar operator puts in the constant "0" from the original query for the
derived column, RecursionLevel, since this is the core query for the
common table expression. The second scalar, which is only carried to a
later operator, is an identifier used as part of the Concatenation
operation.
This data is fed into a Concatenation operator. This operator scans
multiple inputs and produces one output. It is most-commonly used to
implement the UNION ALL operation from T-SQL.
The bottom right hand section of the plan is displayed in Figure 9:
Figure 9
This is where things get interesting. The recursion methods are
implemented via the Table Spool operator. This operator provides the
mechanism for looping through the records multiple times. As noted in
chapter 2, this operator takes each of the rows and stores them in a
hidden temporary object stored in the tempdb database. Later in the
execution plan, if the operator is rewound (say due to the use of a
Nested Loops operator in the execution plan) and no rebinding is
required, the spooled data can be reused instead of having to rescan the
data again. As the operator loops through the records they are joined to
the data from the tables as defined in the second part of the UNION
ALL definition within the CTE.
If you look up NodeId 19 in the XML plan, you can see the
RunTimeInformation element.
<RunTimeInformation>
<RunTimeCountersPerThread Thread="0" ActualRows="4"
ActualRebinds="1" ActualRewinds="0"
ActualEndOfScans="1" ActualExecutions="1" />
</RunTimeInformation>
Chapter 4: Understanding More Complex Query Plans 119
This shows us that one rebind of the data was needed. The rebind, a
change in an internal parameter, would be the second manager. From
the results, we know that three rows were returned; the initial row and
two others supplied by the recursion through the management chain of
the data within Adventureworks.
The final section of the graphical plan is shown in Figure 10:
Figure 10
After the Concatenation operator we get an Index Spool operator.
This operation aggregates the rows into a work table, within tempdb.
The data gets sorted and then we just have the rest of the query, joining
index seeks to the data put together by the recursive operation.
Views
A view is essentially just a "stored query" – a way of representing data
as if it were a table, without actually creating a new table. The various
uses of views are well documented (preventing certain columns being
selected, reducing complexity for end users, and so on). Here, we will
just focus on what happens within the execution plan when we're
working with a view.
Standard Views
The view, Sales.vIndividualCustomer, provides a summary of
customer data, displaying information such as their name, email address,
physical address and demographic information. A very simple query to
get a specific customer would look something like this:
SELECT *
FROM [Sales].[vIndividualCustomer]
WHERE [CustomerID] = 26131 ;
Figure 11
What happened to the view, vIndividualCustomer, which we
referenced in this query? Remember that while SQL Server treats views
similarly to tables, a view is just a named construct that sits on top of
the component tables that make them up. The optimizer, during
binding, resolves all those component parts in order to arrive at an
execution plan to access the data. In effect, the query optimizer ignores
the view object, and instead deals directly with the eight tables and the
seven joins that are defined within this view.
This is important to keep in mind since views are frequently used to
mask the complexity of a query. In short, while the view makes coding
easier, it doesn't in any way change the necessities of the query
optimizer to perform the actions defined within the view.
Indexed Views
An Indexed View, also called a materialized view, is essentially a "view
plus a clustered index". A clustered index stores the column data as well
as the index data, so creating a clustered index on a view results,
essentially, in a new table in the database. Indexed views can often speed
the performance of many queries, as the data is directly stored in the
indexed view, negating the need to join and lookup the data from
multiple tables each time the query is run.
Creating an indexed view is, to say the least, a costly operation.
Fortunately, this is a one-time operation that can be scheduled to occur
when your server is less busy.
Maintaining an index view is a different story. If the tables in the
indexed view are relatively static, there is not much overhead associated
with maintaining indexed views. On the other hand, if an indexed view
is based on tables that are modified often, there can be a lot of
Chapter 4: Understanding More Complex Query Plans 121
SELECT *
FROM [Person].[vStateProvinceCountryRegion]
Figure 12
From our previous experience with execution plans containing views,
you might have expected to see two tables and the join in the execution
plan. Instead, we see a single Clustered Index Scan operation: rather
than execute each step of the view, the optimizer went straight to the
clustered index that makes this an indexed view.
Since these indexes are available to the optimizer, they can also be used
by queries that don't refer to the indexed view at all. For example, the
following query will result in the exact same execution plan as shown in
Figure 12:
This is because the optimizer recognizes the index as the best way to
access the data.
However, this behavior is neither automatic nor guaranteed as execution
plans become more complicated. For example, take the following query:
SELECT a.[City],
v.[StateProvinceName],
v.[CountryRegionName]
FROM [Person].[Address] a
JOIN [Person].[vStateProvinceCountryRegion] v
ON [a].[StateProvinceID] = [v].[StateProvinceID]
WHERE [a].[AddressID] = 22701 ;
If you expected to see a join between the indexed view and the
Person.Address table, you will be disappointed:
Figure 13
Instead of using the clustered index that defines the materialized view,
as we saw in figure 12, the optimizer performs the same type of index
expansion as it did when presented with a regular view. The query that
defines the view is fully resolved, substituting the tables that make it up
instead of using the clustered index provided with the view. 11
Indexes
A big part of any tuning effort revolves around choosing the right
indexes to include in a database. In most peoples' minds, the importance
of using indexes is already well established. A frequently asked question
however, is "how come some of my indexes are used and others are
not?"
The availability of an index directly affects the choices made by the
query optimizer. The right index leads the optimizer to the selection of
the right plan. However, a lack of indexes or, even worse, a poor choice
of indexes, can directly lead to poor execution plans and inadequate
query performance.
SELECT [sod].[ProductID],
[sod].[OrderQty],
[sod].[UnitPrice]
FROM [Sales].[SalesOrderDetail] sod
WHERE [sod].[ProductID] = 897
This query returns an execution plan, shown in Figure 14, which fully
demonstrates the cost of lookups.
124
Figure 14
The Index Seek operator pulls back the four rows we need, quickly and
efficiently. Unfortunately, the only data available on that index is the
ProductId.
Figure 15
Chapter 4: Understanding More Complex Query Plans 125
As you can see from figure 15, the index seek also outputs columns that
define the clustered index, in this case SalesOrderId and
SalesOrderDetailId. These are used to keep the index synchronized
with the clustered index and the data itself.
We then get the Key LookUp, whereby the optimizer retrieves the other
columns required by the query, OrderQty and UnitPrice, from the
clustered index.
In SQL Server 2000, the only way around this would be to modify the
existing index used by this plan, IX_SalesOrderDetail_ProductId, to
use all three columns. However, in SQL Server 2005, we have the
additional option of using the INCLUDE attribute within the non-
clustered index.
The INCLUDE attribute was added to indexes in SQL Server 2005
specifically to solve problems of this type. It allows you to add a column
to the index, for storage only, not making it a part of the index itself,
therefore not affecting the sorting or lookup values of the index.
Adding the columns needed by the query can turn the index into a
covering index, eliminating the need for the lookup operation. This does
come at the cost of added disk space and additional overhead for the
server to maintain the index, so due consideration must be paid prior to
implementing this as a solution.
In the following code, we create a new index using the INCLUDE
attribute. In order to get an execution plan focused on what we're
testing, we set STATISTICS XML to on, and turn it off when we are
done. The code that appears after we turn STATISTICS XML back
off recreates the original index so that everything is in place for any
further tests down the road.
IF EXISTS ( SELECT *
FROM sys.indexes
WHERE OBJECT_ID =
OBJECT_ID(N'[Sales].[SalesOrderDetail]')
AND name = N'IX_SalesOrderDetail_ProductID' )
DROP INDEX [IX_SalesOrderDetail_ProductID]
ON [Sales].[SalesOrderDetail]
WITH ( ONLINE = OFF ) ;
CREATE NONCLUSTERED INDEX [IX_SalesOrderDetail_ProductID]
ON [Sales].[SalesOrderDetail]
([ProductID] ASC)
INCLUDE ( [OrderQty], [UnitPrice] ) WITH ( PAD_INDEX = OFF,
STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF,
IGNORE_DUP_KEY
= OFF, DROP_EXISTING = OFF, ONLINE = OFF,
ALLOW_ROW_LOCKS = ON,
126
ALLOW_PAGE_LOCKS = ON )
ON [PRIMARY] ;
GO
SELECT [sod].[ProductID],
[sod].[OrderQty],
[sod].[UnitPrice]
FROM [Sales].[SalesOrderDetail] sod
WHERE [sod].[ProductID] = 897 ;
GO
SET STATISTICS XML OFF ;
GO
Figure 16
Chapter 4: Understanding More Complex Query Plans 127
The execution plan is able to use a single operator to find and return all
the data we need because the index is now covering, meaning it includes
all the necessary columns.
Index Selectivity
Let's now move on to the question of which index is going to get used,
and why the optimizer sometimes avoids using available indexes.
First, let's briefly review the definition of the two kinds of available
indexes: the clustered and non-clustered index. A clustered index stores
the data along with the lookup values of the index and it sorts the data,
physically. A non-clustered index sorts the column, or columns, included
in the index, but it doesn't sort the data.
Figure 17
As described in Chapter 1, the utility of a given index is determined by
the statistics generated automatically by the system for that index. The
key indicator of the usefulness of the index is its selectivity.
An index's selectivity describes the distribution of the distinct values
within a given data set. To put it more simply, you count the number of
rows and then you count the number of unique values for a given
column across all the rows. After that, divide the unique values by the
number of rows. This results in a ratio that is expressed as the selectivity
of the index. The better the selectivity, the more useful the index and
the more likely it will be used by the optimizer.
For example, on the Sales.SalesOrderDetail table there is an index,
IX_SalesOrderDetail_ProductID, on the ProductID column. To see
the statistics for that index use the DBCC command,
SHOW_STATISTICS:
DBCC SHOW_STATISTICS('Sales.SalesOrderDetail',
'IX_SalesOrderDetail_ProductID')
128
This returns three result sets with various amounts of data. Usually, the
second result set is the most interesting one:
All density Average Length Columns
------------- -------------- -----------------------------------
--------
0.003759399 4 ProductID
8.242868E-06 8 ProductID, SalesOrderID
8.242868E-06 12 ProductID, SalesOrderID,
SalesOrderDetailID
The Density is inverse to the selectivity, meaning that the lower the
density, the higher the selectivity. So an index like the one above, with a
density of .003759399, will very likely be used by the optimizer. The
other rows refer to the clustered index. Any index in the system that is
not clustered will also have a pointer back to the clustered index since
that's where the data is stored. If no clustered index is present then a
pointer to the data itself, often referred to as a heap, is generated. That's
why the columns of the clustered index are included as part of the
selectivity of the index in question.
Selectivity can affect the use of an index negatively as well as positively.
Let's assume that you've taken the time to create an index on a
frequently-searched field, and yet you're not seeing a performance
benefit. Let's create such a situation ourselves. The business represented
in AdventureWorks has decided that they're going to be giving away
prizes based on the quantity of items purchased. This means a query
very similar to the one from the previous Avoiding BookMark Lookups
section:
SELECT sod.OrderQty,
sod.[SalesOrderID],
sod.[SalesOrderDetailID],
sod.[LineTotal]
FROM [Sales].[SalesOrderDetail] sod
WHERE sod.[OrderQty] = 10
Figure 18
We see a Clustered Index Scan against the entire table and then a
simple Filter operation to derive the final results sets, where OrderQty
= 10.
Chapter 4: Understanding More Complex Query Plans 129
Unfortunately, if you capture the plan again, you'll see that it's identical
to the one shown in Figure 18; in other words, our new index is
completely ignored. Since we know that selectivity determines when, or
if, and index is used, let's examine the new index using DBCC
SHOW_STATISTICS:
All density Average Length Columns
------------- -------------- -----------------------------------
--------
0.02439024 2 OrderQty
2.18055E-05 6 OrderQty, SalesOrderID
8.242868E-06 10 OrderQty, SalesOrderID,
SalesOrderDetailID
We can see that the density of the OrderQty is 10 times less than for
the ProductId column, meaning that our OrderQty index is ten times
less selective. To see this in more quantifiable terms, there are 121317
rows in the SalesOrderDetail table on my system. There are only 41
distinct values for the OrderQty column. This column just isn't, by
itself, an adequate index to make a difference in the query plan.
If we really had to make this query run well, the answer would be to
make the index selective enough to be useful by the optimizer. You
could also try forcing the optimizer to use the index we built by using a
query hint, but in this case, it wouldn't help the performance of the
query (hints are covered in detail in Chapter 5). Remember that adding
an index, however selective, comes at a price during inserts, deletes and
updates as the data within the index is reordered, added or removed
based on the actions of the query being run.
If you're following along in AdventureWorks, you'll want to be sure to
drop the index we created:
DROP INDEX
[Sales].[SalesOrderDetail].[IX_SalesOrderDetail_OrderQty]
130
IF EXISTS ( SELECT *
FROM sys.objects
WHERE object_id = OBJECT_ID(N'[NewOrders]')
AND type in ( N'U' ) )
DROP TABLE [NewOrders]
GO
SELECT *
INTO NewOrders
FROM Sales.SalesOrderDetail
GO
CREATE INDEX IX_NewOrders_ProductID on NewOrders ( ProductID
)
GO
I then capture the estimated plan (in MXL format). After that I run a
query that updates the data, changing the statistics and then run another
query, getting the actual execution plan
-- Estimated Plan
SET SHOWPLAN_XML ON
GO
SELECT [OrderQty]
,[CarrierTrackingNumber]
FROM NewOrders
WHERE [ProductID] = 897
GO
SET SHOWPLAN_XML OFF
GO
BEGIN TRAN
UPDATE NewOrders
SET [ProductID] = 897
WHERE [ProductID] between 800 and 900
GO
-- Actual Plan
SET STATISTICS XML ON
GO
SELECT [OrderQty]
,[CarrierTrackingNumber]
Chapter 4: Understanding More Complex Query Plans 131
FROM NewOrders
WHERE [ProductID] = 897
ROLLBACK TRAN
GO
SET STATISTICS XML OFF
GO
I took the XML output and saved them to files (See Saving XML Plans as
Graphical Plans, in Chapter 1), and then reopened the files in order to get
an easy-to-read graphical plan. Breaking bits and pieces of SQL code
apart and only showing plans for the pieces that you want is a big
advantage to using XML plans. First the estimated plan:
Figure 19
Then the Actual execution plan:
Figure 20
Go to the top and right of Figure 19 to find the Index Seek operator.
Clearly, prior to the updates, the data and statistics within the index were
selective enough that the SELECT could use a seek operation. Then,
because the data being requested is not included in the index itself, a
RID Lookup operation is performed. This is a lookup against a heap
table using the row identifier to bring back the data from the correct
row.
However, after the data is updated, the query is much less selective and
returns much more data, so that the actual plan does not use the index,
but instead retrieves the data by scanning the whole table, as we can see
from the Table Scan operator in Figure 20. The estimated cost is
.243321 while the actual cost is 1.2434. Note that if you recapture the
132
estimated plan, you'll see that the statistics have automatically updated,
and the estimated plan will also show a table scan.
Summary
This chapter introduced various concepts that go a bit beyond the basics
in displaying and understanding execution plans. Stored procedures,
views, derived tables, and common table expressions were all introduced
along with their attendant execution plans.
The most important point to take away from all of the various plans
derived is that you have to walk through them all in the same way,
working right to left, top to bottom, in order to understand the behavior
implied by the plan. The importance of indexes and their direct impact
on the execution of the optimizer and the query engine was introduced.
The most important point to take away from here is that simply adding
an index doesn't necessarily mean you've solved a performance problem.
You need to ensure the selectivity of your data. You also need to make
appropriate choices regarding the addition or inclusion of columns in
your indexes, both clustered and non-clustered.
Chapter 5: Controlling Execution Plans with Hints 133
Query Hints
There are quite a number of query hints and they perform a variety of
different duties. Some may be used somewhat regularly and a few are for
rare circumstances.
Query hints are specified in the OPTION clause. The basic syntax is as
follows:
SELECT ...
OPTION (<hint>,<hint>...)
134
HASH|ORDER GROUP
These two hints – HASH GROUP and ORDER GROUP – directly
apply to a GROUP BY aggregate operation (as well as to DISTINCT
or COMPUTE clauses).They instruct the optimizer to apply either
hashing or grouping to the aggregation, respectively.
In the example below we have a simple GROUP BY query that is called
frequently by the application to display the various uses of Suffix to
people's names.
SELECT [c].[Suffix],
COUNT([c].[Suffix]) AS SuffixUsageCount
FROM [Person].[Contact] c
GROUP BY [Suffix]
The business has instructed you to make this query run as fast as
possible because you're maintaining a high-end shop with lots of queries
from the sales force against an ever-changing set of data. The first thing
you do, of course, is to look at the execution plan, as shown in Figure 1:
Figure 1
As you can see, by "default" the optimizer opts to use hashing. The
unordered data from the clustered index scan is grouped within the
Hash Match (Aggregate) operator. This operator will build a hash table,
select distinct values from the data supplied by the Clustered Index Scan
and then develop the counts based on the matched values. This plan has
a cost of 0.590827, which you can see in the Tool Tip in Figure 2:
Chapter 5: Controlling Execution Plans with Hints 135
Figure 2
Since it's not performing in the manner you would like, you decide that
the best solution would be to try to use the data from the Clustered
Scan in an ordered fashion rather than the unordered Hash Match. So
you add the ORDER GROUP hint to the query:
SELECT [c].[Suffix],
COUNT([c].[Suffix]) AS SuffixUsageCount
FROM [Person].[Contact] c
GROUP BY [Suffix]
OPTION ( ORDER GROUP )
Figure 3
We've told the optimizer to use ordering rather than hashing, via the
ORDER GROUP hint, so instead of the hash table, it's been forced to
136
SELECT [pm1].[Name],
[pm1].[ModifiedDate]
FROM [Production].[ProductModel] pm1
UNION
SELECT [pm2].[Name],
[pm2].[ModifiedDate]
FROM [Production].[ProductModel] pm2
Figure 4
You can see that the Concatenation operation that the optimizer
chooses to use is, in the context of the plan, very cheap, but the Sort
Chapter 5: Controlling Execution Plans with Hints 137
SELECT [pm1].[Name],
[pm1].[ModifiedDate]
FROM [Production].[ProductModel] pm1
UNION
SELECT [pm2].[Name],
[pm2].[ModifiedDate]
FROM [Production].[ProductModel] pm2
OPTION ( MERGE UNION )
Figure 5
You have forced the UNION operation to use the Merge Join instead
of the Concatenation operator. However, since the Merge Join only
works with sorted data feeds, we've also forced the optimizer to add two
Sort operators. The estimated cost for the query has gone from 0.0377
to 0.0548. Clearly this didn't work.
What if you tried the other hint, HASH UNION:
SELECT [pm1].[Name],
[pm1].[ModifiedDate]
FROM [Production].[ProductModel] pm1
UNION
SELECT [pm2].[Name],
[pm2].[ModifiedDate]
FROM [Production].[ProductModel] pm2
OPTION ( HASH UNION )
Figure 6
The execution plan is simplified, with the sort operations completely
eliminated. However, the cost is still higher (0.0497) than for the original
un-hinted query so clearly, for the amount of data involved in this query,
the Hash Match operator doesn't offer a performance enhancement
over the original Concatenation operator.
In this situation, the hints are working to modify the behavior of the
query, but they are not helping you to increase performance of the
query.
LOOP|MERGE|HASH JOIN
This makes all the join operations in a particular query use the method
supplied by the hint. However, note that if a join hint (covered later in
this chapter) is applied to a specific join, then the more granular join
hint takes precedence over the general query hint.
You've found that your system is suffering from poor disk I/O, so you
need to reduce the number of scans and reads that your queries
generate. By collecting data from Profiler and Performance Monitor
you're able to identify the following query as needing some work. Here
is the query and the original execution plan:
Figure 7
As you can see, the query uses a mix of Nested Loop and Hash
Match operators to put the data together. Let's see the I/O output of
the query. This can be done by navigating from the main menu, Query
| Query Options, selecting the advanced tab and activating the "Set
Statistics IO" check box.
Table 'Contact'. Scan count 0, logical reads 1586, …
Table 'Worktable'. Scan count 0, logical reads 0, …
Table 'Address'. Scan count 1, logical reads 216, …
Table 'CustomerAddress'. Scan count 753, logical reads
1624, …
Table 'Store'. Scan count 1, logical reads 103, …
Table 'StoreContact'. Scan count 20, logical reads 42, …
Table 'ContactType'. Scan count 1, logical reads 2, …
From this data, you can see that the scans against the
CustomerAddress table are causing a problem within this query. It
occurs to you that allowing the query to perform all those Hash Join
operations is slowing it down and you decide to change the behavior by
adding the Loop Join hint to the end of the query:
Figure 8
Now the Hash Joins are Loop Joins. This situation could be
interesting. If you look at the operations that underpin the query
140
execution plan you'll see that the second query, with the hint, eliminates
the creation of a work table. While the estimated cost of the second
query is a bit higher than the original, the addition of the query hint
does, in this case, results in a negligible improvement in performance,
going from about 172ms to about 148ms on average. However, when
you look at the scans and reads, they tell a different story:
Table 'ContactType'. Scan count 0, logical reads 1530, …
Table 'Contact'. Scan count 0, logical reads 1586, …
Table 'StoreContact'. Scan count 712, logical reads 1432,
…
Table 'Address'. Scan count 0, logical reads 1477, …
Table 'CustomerAddress'. Scan count 701, logical reads
1512, …
Table 'Store'. Scan count 1, logical reads 103, …
Not only have we been unsuccessful in reducing the reads, despite the
elimination of the work table, but we've actually increased the number
of scans. What if we were to modify the query to use the MERGE
JOIN hint instead? Change the final line of the query to read:
Figure 9
The execution of the plan was about as fast as the original, but did it
solve our problem?
Table 'Worktable'. Scan count 11, logical reads 91, …
Table 'CustomerAddress'. Scan count 1, logical reads 6, …
Table 'StoreContact'. Scan count 1, logical reads 4, …
Table 'ContactType'. Scan count 1, logical reads 2, …
Table 'Store'. Scan count 1, logical reads 103, …
Table 'Address'. Scan count 1, logical reads 18, …
Table 'Contact'. Scan count 1, logical reads 33, …
the HASH JOIN hint to see what it might do. Modify the final line of
the query to read:
Figure 10
We're back to a simplified execution plan using only Hash Join
operations. The execution time was about the same as the original query
and the I/O looked like this:
Table 'Worktable'. Scan count 0, logical reads 0, …
Table 'Contact'. Scan count 1, logical reads 569, …
Table 'Store'. Scan count 1, logical reads 103, …
Table 'Address'. Scan count 1, logical reads 216, …
Table 'CustomerAddress'. Scan count 1, logical reads 67, …
Table 'StoreContact'. Scan count 1, logical reads 4, …
Table 'ContactType'. Scan count 1, logical reads 2, …
For the example above, using the MERGE JOIN hint appears to be the
best bet for reducing the I/O costs of the query, with only the added
overhead of the creation of the worktable.
FAST n
This time, we're not concerned about performance of the database.
This time, we're concerned about perceived performance of the
application. The users would like an immediate return of data to the
screen, even if it's not the complete result set, and even if they have to
wait longer for the complete result set.
The FAST n hint provides that feature by getting the optimizer to focus
on getting the execution plan to return the first 'n' rows as fast as
possible, where 'n' has to be a positive integer value. Consider the
following query and execution plan:
SELECT *
FROM [Sales].[SalesOrderDetail] sod
142
Figure 11
This query performs adequately, but there is a delay before the end users
see any results, so we try to fix this by adding the Fast n hint to return
the first 10 rows as quickly as possible:
OPTION ( FAST 10 )
Figure 12
Instead of the Hash Match operator for the join, the optimizer
attempted to use a Nested Loop. The loop join results in getting the
first rows back very fast, but the rest of the processing was somewhat
slower. So, because the plan will be concentrating on getting the first ten
rows back as soon as possible, you'll see a difference in the cost and the
performance of the query. The total cost for the original query was
1.973. The hint reduced that cost to 0.012 (for the first 10 rows). The
number of logical reads increases dramatically, 1238 for the un-hinted
query to 101,827 for the hinted query, but the actual speed of the
execution of the query increases only marginally, from around 4.2
seconds to 5 seconds. This slight slow-down in performance was
accepted by the end-users since they got what they really wanted, a very
fast display to the screen.
If you want to see the destructive as well as beneficial effects that hints
can have, try applying the LOOP JOIN hint, from the previous section.
It increases the cost by a factor of five!
Chapter 5: Controlling Execution Plans with Hints 143
FORCE ORDER
You've identified a query that is performing poorly. It's a somewhat long
query with a few tables. Normally, the optimizer will determine the
order in which the joins occur but using the FORCE ORDER hint you
can make the optimizer use the order of joins as listed in the query
itself. This would be done if you've got a fairly high degree of certainty
that your join order is better than that supplied by the optimizer. The
optimizer can make incorrect choices when the statistics are not up to
date, when the data distribution is less than optimal or if the query has a
high degree of complexity. Here is the query in question:
Based on your knowledge of the data, you're fairly certain that you've
put the joins in the correct order. Here is the execution plan as it exists:
144
Figure 3
Obviously this is far too large to review on the page. The main point to
showing the graphic is really for you to get a feel for the shape of the
plan. The estimated cost as displayed in the tool tip is 0.5853.
Take the same query and apply the FORCE ORDER query hint:
Figure 4
Don't try to read the plan in Figure 14; simply notice that the overall
shape has changed radically from the execution plan in Figure 13. All
the joins are now in the exact order listed in the SELECT statement of
the query. Unfortunately, our choice of order was not as efficient as the
choices made by the optimizer. The estimated cost for the query
displayed on the ToolTip is 1.1676. The added cost is caused by the fact
that our less efficient join order is filtering less data from the early parts
of the query. Instead, we're force to carry more data between each
operation.
Chapter 5: Controlling Execution Plans with Hints 145
MAXDOP
You have one of those really nasty problems, a query that sometimes
runs just fine, but sometimes runs incredibly slowly. You investigate the
issue, and use SQL Server Profiler to capture the execution of this
procedure, over time, with various parameters. You finally arrive at two
execution plans. The execution plan when the query runs quickly looks
like this:
Figure 5
When the execution is slow, the plan looks this way (note that this image
was split in order to make it more readable):
Figure 6
Here, you're seeing a situation where the parallelism (covered in Chapter
8) that should be helping the performance of your system is, instead,
hurting that performance. Since parallelism is normally turned on and
off at the server level, and other procedures running on the server are
benefiting from it, you can't simply turn it off. That's where the
MAXDOP hint becomes useful.
The MAXDOP query hint can control the use of parallelism within an
individual query, rather than working with the server-wide setting of
"Max Degree of Parallelism".
In order to get this to fire on a system with only a single processor, I'm
going to reset the threshold for my system as part of the query:
SELECT wo.[DueDate],
MIN(wo.[OrderQty]) MinOrderQty,
MIN(wo.[StockedQty]) MinStockedQty,
MIN(wo.[ScrappedQty]) MinScrappedQty,
146
MAX(wo.[OrderQty]) MaxOrderQty,
MAX(wo.[StockedQty]) MaxStockedQty,
MAX(wo.[ScrappedQty]) MaxScrappedQty
FROM [Production].[WorkOrder] wo
GROUP BY wo.[DueDate]
ORDER BY wo.[DueDate]
GO
This will result in an execution plan that takes full advantage of parallel
processing, looking like figure 16 above. The optimizer chooses a
parallel execution for this plan. Look at the properties of the Clustered
Index Scan operator by selecting that icon on the plan in Management
Studio. The property Actual Number of Rows can be expanded by
clicking on the plus (+) icon. It will show three different threads, the
number of threads spawned by the parallel operation.
However, we know that when our query uses parallel processing, it is
running slowly. We have no desire to change the overall behavior of
parallelism within the server itself, so we directly affect the query that is
causing problems by adding this code:
SELECT wo.[DueDate],
MIN(wo.[OrderQty]) MinOrderQty,
MIN(wo.[StockedQty]) MinStockedQty,
MIN(wo.[ScrappedQty]) MinScrappedQty,
MAX(wo.[OrderQty]) MaxOrderQty,
MAX(wo.[StockedQty]) MaxStockedQty,
MAX(wo.[ScrappedQty]) MaxScrappedQty
FROM [Production].[WorkOrder] wo
GROUP BY wo.[DueDate]
ORDER BY wo.[DueDate]
OPTION ( MAXDOP 1 )
Figure 7
Chapter 5: Controlling Execution Plans with Hints 147
OPTIMIZE FOR
You have identified a query that will run at an adequate speed for hours,
or days, with no worries and then suddenly it performs horribly. With a
lot of investigation and experimentation, you find that most of the time,
the parameters being supplied by the application to run the procedure,
result in an execution plan that performs very well. Sometimes, a certain
value, or subset of values, is supplied to the parameter when the plan is
recompiling and the execution plan stored in the cache with this
parameter performs very badly indeed.
The OPTIMIZE FOR hint was introduced with SQL Server 2005. It
allows you to instruct the optimizer to optimize query execution for the
particular parameter value that you supply, rather than for the actual
value of a parameter supplied within the query.
This can be an extremely useful hint. Situations can arise whereby the
data distribution of a particular table, or index, is such that most
parameters will result in a good plan, but some parameters can result in
a bad plan. Since plans can age out of the cache, or events can be fired
that cause plan recompilation, it becomes, to a degree, a gamble as to
where and when the problematic execution plan is the one that gets
created and cached.
In SQL Server 2000, only two options were available:
1. Recompile the plan every time using the RECOMPILE hint
2. Get a good plan and keep it using the KEEPFIXED PLAN
hint
148
SELECT *
FROM [Person].[Address]
WHERE [City] = 'Newark'
SELECT *
FROM [Person].[Address]
WHERE [City] = 'London'
We'll run these at the same time and we'll get two different execution
plans:
Figure 8
If you look at the cost relative to the Batch of each of these queries, the
first query is just a little less expensive than the second, with costs of
0.194 compared to 0.23. This is primarily because the second query is
doing a clustered index scan, which walks through all the rows available.
Chapter 5: Controlling Execution Plans with Hints 149
We'll get a standard execution plan for both queries that looks like this:
Figure 9
It's using the clustered index for both queries now because it's not sure
which of the values available in the table is most likely going to be
passed in as @City.
Let's make one more modification. In the second query, we instruct the
optimizer to optimize for Newark:
Figure 20
The value 'London' has very low level of selectivity (there are a lot of
values equal to 'London') within the index and this is displayed by the
Clustered Index Scan in the first query. Despite the fact that the second
query looks up the same value, it's now the faster of the two queries.
The OPTIMIZE FOR operator was able to focus the optimizer to
create a plan that counted on the fact that the data was highly selective,
even though it was not. The execution plan created was one for the
more selective value, 'Newark', yet that plan helped the performance for
the other value, 'London.'
Use of this hint requires intimate knowledge of the underlying data.
Choosing the wrong value to supply OPTIMIZE FOR will not only
fail to help performance, but could have a very serious negative impact.
You can set as many hints as you use parameters within the query.
PARAMETERIZATION SIMPLE|FORCED
Parameterization, forced and simple, is covered in a lot more detail in
the section on Plan Guides, in Chapter 8. It's covered in that section
because you can't actually use this query hint by itself within a query,
but must use it only with a plan guide.
RECOMPILE
You have yet another problem query that performs slowly in an
intermittent fashion. Investigation and experimentation with the query
Chapter 5: Controlling Execution Plans with Hints 151
leads you to realize that the very nature of the query itself is the
problem. It just so happens that this query is a built-in, ad hoc (using
SQL statements or code to generate SQL statements) query of the
application you support. Each time the query is passed to SQL Server, it
has slightly different parameters, and possibly even a slightly different
structure. So, while plans are being cached for the query, many of these
plans are either useless or could even be problematic. The execution
plan that works well for one set of parameter values may work horribly
for another set. The parameters passed from the application in this case
are highly volatile. Due to the nature of the query and the data, you
don't really want to keep all of the execution plans around. Rather than
attempting to create a single perfect plan for the whole query, you
identify the sections of the query that can benefit from being
recompiled regularly.
The RECOMPILE hint was introduced in SQL 2005. It instructs the
optimizer to mark the plan created so that it will be discarded by the
next execution of the query. This hint might be useful when the plan
created, and cached, isn't likely to be useful to any of the following calls.
For example, as described above, there is a lot of ad hoc SQL in the
query, or the data is relatively volatile, changing so much that no one
plan will be optimal. Regardless of the cause, the determination has
been made that the cost of recompiling the procedure each time it is
executed is worth the time saved by that recompile.
You can also add the instruction to recompile the plan to stored
procedures, when they're created, but the RECOMPILE query hint
offers greater control. The reason for this is that statements within a
query or procedure can be recompiled independently of the larger query
or procedure. This means that if only a section of a query uses ad hoc
SQL, you can recompile just that statement as opposed to the entire
procedure. When a statement recompiles within a procedure, all local
variables are initialized and the parameters used for the plan are those
supplied to the procedure.
If you use local variables in your queries, the optimizer makes a guess as
to what value may work best. This guess is kept in the cache. Consider
the following pair of queries:
Figure 21
With a full knowledge of your system, you know that the plan for the
second query should be completely different because the value passed is
much more selective, and a useful index exists on that column. So, you
modify the queries using the RECOMPILE hint. In this instance, I'm
adding it to both queries so that you can see that the performance gain
in the second query is due to the RECOMPILE leading to a better
plan, while the same RECOMPILE on the first query leads to the
original plan.
OPTION ( RECOMPILE )
Figure 22
Note that the second query is now using our
IX_SalesOrderHeader_SalesPersonID index and accounts for 8% of
the combined cost of both queries, instead of 50%. This is because the
Index Seek and Key Lookup operators with the Nested Loop are
faster and less costly than the Clustered Index Scan since they will only
work with a subset of the rows.
ROBUST PLAN
This hint is used when you need to work with very wide rows. For
example:
1. A row that contains one or more variable length columns set
to very large size or even the MAX size allowed in 2005
2. A row that contains one or more large objects (LOB) such as
BINARY, XML or TEXT data types.
Sometimes, when processing these rows, it's possible for some operators
to encounter errors, usually when creating worktables as part of the
plan. The ROBUST PLAN hint ensures that a plan that could cause
errors won't be chosen by the optimizer. While this will eliminate errors,
it will almost certainly result in longer query times since the optimizer
won't be able to choose the optimal plan over the "robust" plan. This is
a very rare event so this hint should only be used if you actually have a
set of wide rows that cause the error condition.
154
KEEP PLAN
As the data in a table changes, gets inserted or deleted, the statistics
describing the data also change. As these statistics change, queries get
marked for recompile. Setting the KEEP PLAN hint doesn't prevent
recompiles, but it does cause the optimizer to use less stringent rules
when determining the need for a recompile. This means that, with more
volatile data, you can keep recompiles to a minimum. The hint causes
the optimizer to treat temporary tables within the plan in the same way
as permanent tables, reducing the number of recompiles caused by the
temp table. This reduces the time and cost of recompiling a plan, which,
depending on the query, can be quite large.
However, problems may arise because the old plans might not be as
efficient as newer plans could be.
KEEPFIXED PLAN
The KEEPFIXED PLAN query hint is similar to KEEP PLAN, but
instead of simply limiting the number of recompiles, KEEPFIXED
PLAN eliminates any recompile due to changes in statistics.
Use this hint with extreme caution. The whole point of letting SQL
Server maintain statistics is to aid the performance of your queries. If
you prevent these changed statistics from being used by optimizer, it can
lead to severe performance issues.
As with KEEP PLAN, this will keep the plan in place unless the
schema of the tables referenced in the query changes or sp_recompile
is run against the query, forcing a recompile.
EXPAND VIEWS
Your users come to you with a complaint. One of the queries they're
running isn't returning correct data. Checking the execution plan you
find that the query is running against a materialized, or indexed, view.
While the performance is excellent, the view itself is only updated once
a day. Over the day the data referenced by the view ages, or changes,
within the table where it is actually stored. Several queries that use the
view are not affected by this aging data, so changing the refresh times
for the view isn't necessary. Instead, you decide that you'd like to get
directly at the data, but without completely rewriting the query.
The EXPAND VIEWS query hint eliminates the use of the index
views within a query and forces the optimizer to go to the tables for the
data. The optimizer replaces the indexed view being referenced with the
view definition (in other words, the query used to define the view) just
Chapter 5: Controlling Execution Plans with Hints 155
SELECT *
FROM [Person].[vStateProvinceCountryRegion]
Figure 23
An indexed view is simply a clustered index, so this execution plan
makes perfect sense. If we add the query hint, OPTION (EXPAND
VIEWS), things change as we see in Figure 24:
Figure 24
Now we're no longer scanning the clustered index. Within the
Optimizer, the view has been expanded into its definition so we see the
Clustered Index Scan against the Person.CountryRegion and
Person.StateProvince tables. These are then joined using the Merge
Join, after the data in the StateProvince stream is run through a Sort
156
MAXRECURSION
With the addition of the Common Table Expression to SQL Server, a
very simple method for calling recursive queries was created. The
MAXRECURSION hint places an upper limit on the number of
recursions within a query.
Valid values are between 0 and 32,767. Setting the value to zero allows
for infinite recursion. The default number of recursions is 100. When
the number is reached, an error is returned and the recursive loop is
exited. This will cause any open transactions to be rolled back. Using the
option doesn't change the execution plan but, because of the error, an
actual execution plan might not be returned.
USE PLAN
This hint simply substitutes any plan the optimizer may have created
with the XML plan supplied with the hint. This is covered in great detail
in Chapter 8.
Join Hints
A join hint provides a means to force SQL Server to use one of the
three join methods that we've encountered previously, in a given part of
a query. To recap, these join methods are:
• Nested Loop join: compares each row from one table ("outer
table") to each row in another table ("inner table") and returns
rows that satisfy the join predicate. Cost is proportional to the
product of the rows in the two tables. Very efficient for smaller
data sets.
• Merge join: compares two sorted inputs, one row at a time.
Cost is proportional to the sum of the total number of rows.
Requires an equi-join condition. Efficient for larger data sets
• Hash Match join: reads rows from one input, hashes the rows,
based on the equi-join condition, into an in-memory hash table.
Does the same for the second input and then returns matching
rows. Most useful for very large data sets (especially data
warehouses)
Chapter 5: Controlling Execution Plans with Hints 157
By incuding one of the join hints in your T-SQL you will potentially
override the optimizer's choice of the most efficent join method. In
general, this is not a good idea and if you're not careful you could
seriously impede performance 12.
Application of the join hint applies to any query (select, insert, or
delete) where joins can be applied. Join hints are specified between two
tables.
Consider a simple report that lists Product Models, Products and
Illustrations from Adventure works:
SELECT [pm].[Name],
[pm].[CatalogDescription],
p.[Name] AS ProductName,
i.[Diagram]
FROM [Production].[ProductModel] pm
LEFT JOIN [Production].[Product] p
ON [pm].[ProductModelID] = [p].[ProductModelID]
LEFT JOIN [Production].[ProductModelIllustration]
pmi
ON [pm].[ProductModelID] = [pmi].[ProductModelID]
LEFT JOIN [Production].[Illustration] i
ON [pmi].[IllustrationID] = [i].[IllustrationID]
WHERE [pm].[Name] LIKE '%Mountain%'
ORDER BY [pm].[Name] ;
12There is a fourth join method, the Remote join, that is used when
dealing with data from a remote server. It forces the join operation from
your local machine onto the remote server. This has no affects on
execution plans, so we won't be drilling down on this functionality here.
158
Figure 25
This is a fairly straightforward plan. The presence of the WHERE
clause using the LIKE '%Mountain%' condition means that there
won't be any seek on an index; and so the Clustered Index Scan
operators on the Product and ProductModel table make sense.
They're then joined using a Hash Match operator, encompassing 46%
of the cost of the query. Once the data is joined, the ORDER BY
command is implemented by the Sort operator. The plan continues with
the Clustered Index Scan against the ProductModelIllustration table
that joins to the data stream with a Loop operator. This is repeated with
another Clustered Index Scan against the Illustration table and a join to
the data stream with a Loop operator. The total estimated cost for these
operations comes to 0.09407.
What happens if we decide that we're smarter than the optimizer and
that it really should be using a Nested Loop join instead of that Hash
Match join? We can force the issue by adding the LOOP hint to the join
condition between Product and ProductModel:
SELECT [pm].[Name],
[pm].[CatalogDescription],
p.[Name] AS ProductName,
i.[Diagram]
FROM [Production].[ProductModel] pm
LEFT LOOP JOIN [Production].[Product] p
ON [pm].[ProductModelID] = [p].[ProductModelID]
LEFT JOIN [Production].[ProductModelIllustration]
pmi
ON [pm].[ProductModelID] = [pmi].[ProductModelID]
LEFT JOIN [Production].[Illustration] i
ON [pmi].[IllustrationID] = [i].[IllustrationID]
WHERE [pm].[Name] LIKE '%Mountain%'
ORDER BY [pm].[Name] ;
Figure 26
Sure enough, where previously we saw a Hash Match operator, we now
see the Nested Loop operator. Also, the sort moved before the join in
order to feed ordered data into the Loop operation, which means that
the original data is sorted instead of the joined data. This adds to the
overall cost. Also, note that the Nested Loop join accounts for 56% of
the cost, whereas the original Hash Match accounted for only 46%. All
this resulted in a total, higher cost of 0.16234.
If you replace the previous LOOP hint with the MERGE hint, you'll
see the following plan:
Figure 27
The Nested Loop becomes a Merge Join operator and the overall cost
of the plan drops to 0.07647, apparently offering us a performance
benefit.
The Merge Join plus the Sort operator, which is required to make sure it
uses ordered data, turns out to cost less than the Hash Match or the
Nested Loop.
In order to verify the possibility of a performance increase, we can
change the query options so that it shows us the I/O costs of each
query. The output of all three queries is listed, in part, here:
Original (Hash)
Table 'Illustration'. Scan count 1, logical reads 273
Table 'ProductModelIllustration'. Scan count 1, logical
reads 183
Table 'Worktable'. Scan count 0, logical reads 0
Table 'ProductModel'. Scan count 1, logical reads 14
Table 'Product'. Scan count 1, logical reads 15
160
Loop
Table 'Illustration'. Scan count 1, logical reads 273
Table 'ProductModelIllustration'. Scan count 1, logical
reads 183
Table 'Product'. Scan count 1, logical reads 555
Table 'ProductModel'. Scan count 1, logical reads 14
Merge
Table 'Illustration'. Scan count 1, logical reads 273
Table 'ProductModelIllustration'. Scan count 1, logical
reads 183
Table 'Product'. Scan count 1, logical reads 15
Table 'ProductModel'. Scan count 1, logical reads 14
This shows us that the Merge and Loop joins required almost exactly
the same number of reads to arrive at the data set needed as the original
Hash join. The differences come when we see that, in order to support
the Loop join, 555 reads were required instead of 15 for both the Merge
and Hash joins. The other difference, probably the clincher in this case,
is the work table that the Hash creates to support the query. This was
eliminated with the Merge join. This illustrates the point that the
optimizer does not always choose an optimal plan. Based on the
statistics in the index and the amount of time it had to calculate its
results, it must have decided that the Hash join would perform faster. In
fact, as the data changes within the tables, it's possible that the Merge
join will cease to function better over time, but because we've hard
coded the join, no new plan will be generated by the optimizer as the
data changes, as would normally be the case.
Table Hints
Table hints enable you to specifically control how the optimizer "uses" a
particular table when generating an execution plan. For example, you
can force the use of a table scan, or specify a particular index that you
want used on that table.
As with the query and join hints, using a table hint circumvents the
normal optimizer processes and could lead to serious performance
issues. Further, since table hints can affect locking strategies, they
possibly affect data integrity leading to incorrect or lost data. These
must be used judiciously.
Some of the table hints are primarily concerned with locking strategies.
Since some of these don't affect execution plans, we won't be covering
them. The three table hints covered below have a direct impact on the
execution plans. For a full list of table hints, please refer to the Books
Online supplied with SQL Server 2005.
Chapter 5: Controlling Execution Plans with Hints 161
The WITH keyword is not required in all cases, nor are the commas
required in all cases, but rather than attempt to guess or remember
which hints are the exceptions, all hints can be placed within the WITH
clause and separated by commas as a best practice to ensure consistent
behavior and future compatibility. Even with the hints that don't require
the WITH keyword, it must be supplied if more than one hint is to be
applied to a given table.
NOEXPAND
When multiple indexed views are referenced within the query, use of the
NOEXPAND table hint will override the EXPAND VIEWS query
hint and prevent the indexed view to which the table hint applies from
being "expanded" into its underlying view definition. This allows for a
more granular control over which of the indexed views is forced to
resolve to its base tables and which simply pull their data from the
clustered index that defines it.
SQL 2005 Enterprise and Developer editions will use the indexes in an
indexed view if the optimizer determines that index will be best for the
query. This is called indexed view matching. It requires the following
settings for the connection:
• ANSI_NULL set to on
• ANSI_WARNINGS set to on
• CONCAT_NULL_YIELDS_NULL set to on
• ANSI_PADDING set to on
• ARITHABORT set to on
• QUOTED_IDENTIFIERS set to on
• NUMERIC_ROUNDABORT set to off
Using the NOEXPAND hint can force the optimizer to use the index
from the indexed view. In Chapter 4, we used a query that referenced
one of the Indexed Views, vStateProvinceCountryRegion, in
AdventureWorks. The optimizer expanded the view and we saw an
162
SELECT a.[City],
v.[StateProvinceName],
v.[CountryRegionName]
FROM [Person].[Address] a
JOIN [Person].[vStateProvinceCountryRegion] v WITH (
NOEXPAND )
ON [a].[StateProvinceID] = [v].[StateProvinceID]
WHERE [a].[AddressID] = 22701 ;
Now, instead of a 3- table join, we get the following
execution plan:
Figure 28
Now, not only are we using the clustered index defined on the view, but
we're seeing a performance increase, with the estimated cost decreasing
from .00985 to .00657.
INDEX()
The index() table hint allows you to define the index to be used when
accessing the table. The syntax supports either numbering the index,
starting at 0 with the clustered index, if any, and proceeding one at a
time through the rest of the indexes:
You can only have a single index hint for a given table, but you can
define multiple indexes within that one hint.
Let's take a simple query that lists Department Name, Title and
Employee Name:
SELECT [de].[Name],
[e].[Title],
[c].[LastName] + ', ' + [c].[FirstName]
FROM [HumanResources].[Department] de
JOIN [HumanResources].[EmployeeDepartmentHistory]
edh
ON [de].[DepartmentID] = [edh].[DepartmentID]
JOIN [HumanResources].[Employee] e
ON [edh].[EmployeeID] = [e].[EmployeeID]
JOIN [Person].[Contact] c
ON [e].[ContactID] = [c].[ContactID]
WHERE [de].[Name] LIKE 'P%'
Figure 29
We see a series of Index Seek and Cluster Index Seek operations joined
together by Nested Loop operations. Suppose we're convinced that we
can get better performance if we could eliminate the Index Seek on the
HumanResources.Department table and instead use that table's
clustered index, PK_Department_DepartmentID. We could
accomplish this using the INDEX hint, as follows:
SELECT [de].[Name],
[e].[Title],
[c].[LastName] + ', ' + [c].[FirstName]
FROM [HumanResources].[Department] de
WITH ( INDEX ( PK_Department_DepartmentID ) )
JOIN [HumanResources].[EmployeeDepartmentHistory]
edh
ON [de].[DepartmentID] = [edh].[DepartmentID]
JOIN [HumanResources].[Employee] e
ON [edh].[EmployeeID] = [e].[EmployeeID]
164
JOIN [Person].[Contact] c
ON [e].[ContactID] = [c].[ContactID]
WHERE [de].[Name] LIKE 'P%'
Figure 30
We can see the Clustered Index Scan in place of the Index Seek. This
change causes a marginally more expensive query, with the cost coming
in at 0.0739643 as opposed to 0.0739389. While the index seek is
certainly faster than the scan, the difference at this time is small because
the scan is only hitting a few more rows than the seek, in such a small
table. However, using the clustered index didn't improve the
performance of the query as we originally surmised because the query
used it within a scan instead of the more efficient seek operation.
FASTFIRSTROW
Just like the FAST n query hint, outlined above, FASTFIRSTROW
forces the optimizer to choose a plan that will return the first row as fast
as possible for the table in question. Functionally, FASTFIRSTROW is
equivalent to the FAST n query hint, but it is more granular in its
application.
Microsoft recommends against using FASTFIRSTROW as it may be
removed in future versions of SQL Server. Nevertheless, we'll provide a
simple example. The following query is meant to get a summation of
the available inventory by product model name and product name:
GROUP BY [pm].[Name],
[p].[Name] ;
Figure 31
As you can see, an Index Scan operation against the ProductModel
database returns the first stream of data. This is joined against a
Clustered Index Scan operation from the Product table, through a
Hash Match operator. The data from the ProductInventory table can
be retrieved through a Clustered Index Seek and this is then joined to
the other data through a Nested Loop. Finally, the summation
information is built through a Stream Aggregate operator.
If we decided that we thought that getting the Product information a bit
quicker might make a difference in the behavior of the query we could
add the table hint, only to that table:
Figure 32
This makes the optimizer choose a different path through the data.
Instead of hitting the ProductModel table first, it's now collecting the
Product information first. This is being passed to a Nested Loop
operator that will loop through the smaller set of rows from the
Product table and compare them to the larger data set from the
ProductModel table.
The rest of the plan is the same. The net result is that, rather than
building the worktable to support the hash match join, most of the
work occurs in accessing the data through the index scans and seeks,
with cheap nested loop joins replacing the hash joins. The cost estimate
decreases from .101607 in the original query to .011989 in the second.
One thing to keep in mind, though, is that while the performance win
seems worth it in this query, it comes at the cost of a change in the
scans against the ProductModel table. Instead of one scan and two
reads, the second query has 504 scans and 1008 reads against the
ProductModel table. This appears to be less costly than creating the
worktable, but you need to remember these tests are being run against a
server in isolation. I'm running no other database applications or queries
against my system at this time. That kind of additional I/O could cause
this process, which does currently run faster ~130ms vs. ~200ms, to
slow down significantly.
Summary
While the Optimizer makes very good decisions most of the time, at
times it may make less than optimal choices. Taking control of the
queries using Table, Join and Query hints where appropriate can be the
right choice. Remember that the data in your database is constantly
changing. Any choices you force on the Optimizer through these hints
today to achieve whatever improvement you're hoping for may become
a major pain in your future. Test the hints prior to applying them and
remember to document their use in some manner so that you can come
back and test them again periodically as your database grows. As
Chapter 5: Controlling Execution Plans with Hints 167
Simple Cursors
In this example, the cursor is declared with no options, accepting all
defaults, and then it is traversed straight through using the FETCH
NEXT method, returning a list of all the CurrencyCodes used in the
AdventureWorks database. I'm going to continue working with the same
basic query throughout the section on cursors because it returns a small
number of rows and because we can easily see how changes to cursor
properties affect the execution plans.
OPEN CurrencyList
WHILE @@FETCH_STATUS = 0
BEGIN
-- Normally there would be operations here using data
from cursor
CLOSE CurrencyList
DEALLOCATE CurrencyList
GO
Figure 1
Logical Operators
Use the "Display Estimated Execution Plan" option to generate the
graphical estimated execution plan for the above code. The query
consists of six distinct statements and therefore six distinct plans, as
shown in Figure 2:
Chapter 6: Cursor Operations 171
Figure 2
We'll split this plan into it component parts. The top section shows the
definition of the cursor:
Figure 3
This definition in the header includes the select statement that will
provide the data that the cursor uses. This plan contains our first two
cursor-specific operators but, as usual, we'll read this execution plan
starting on the right. First, we have a Clustered Index Scan against the
Sales.Currency table.
172
Figure 4
The clustered index scan retrieves an estimated 14 rows. This is
followed by the Compute Scalar operator, which creates a unique
identifier to identify the data returned by the query, independent of any
unique keys on the table or tables from which the data was selected (see
Figure 5, below).
Chapter 6: Cursor Operations 173
Figure 5
With a new key value, these rows are then inserted into a temporary
clustered index, created in tempdb. This clustered index is the
mechanism by which the server is able to walk through a set of data as a
cursor (Figure 5, below). It's commonly referred to as a "work table".
Figure 6
174
The Fetch Query operation is the one that actually retrieves the rows
from the cursor, the clustered index created above, when the FETCH
command is issued. The ToolTip displays the following, familiar
information (which doesn't provide much that's immediately useful):
Figure 7
Finally, instead of yet another Select Operator, we finish with a
Dynamic operator.
Dynamic
Figure 8
Unlike the DML queries before, we see a view of the direct TSQL that
defined the cursor, rather than the SQL statement after it had been
bound by the optimization process.
Cursor Catchall
The next five sections of our original execution plan, from Figure 2, all
feature a generic icon known as the Cursor Catchall. In general, a
catch-all icon is used for operations that Microsoft determined didn't
need their own special graphic.
In Query 2 and Query 3 we see catchall icons for the OPEN CURSOR
operation, and the FETCH CURSOR operation:
OPEN CurrencyList
Figure 9
Query 4 shows the next time within the T-SQL that the FETCH
CURSOR command was used, and it shows a language element icon,
for the WHILE loop, as a COND or conditional operator.
WHILE @@FETCH_STATUS = 0
BEGIN
--Normally there would be operations here using data from
cursor
FETCH NEXT FROM CurrencyList
END
Figure 10
Finally, Query 5 closes the cursor and Query 6 deallocates it, removing
the cursor from the tempdb.
CLOSE CurrencyList
DEALLOCATE CurrencyList
Chapter 6: Cursor Operations 177
Figure 11
Physical Operators
When we execute the same script, using the "Display Graphical
Execution Plan" option, the actual execution plan doesn't mirror the
estimated plan. Instead we see the following:
Figure 12
This simple plan is repeated fifteen times, once for each row of data
added to the cursor (note the slight discrepancy between the actual
number of rows, fifteen, and the estimated fourteen rows you'll see in
the ToolTip).
One interesting thing to note is that there are no cursor icons present in
the plan. Instead, the one cursor command immediately visible,
FETCH CURSOR, is represented by the generic T-SQL operator icon.
This is because all the physical operations that occur with a cursor are
represented by the actual operations being performed, and the FETCH
is roughly equivalent to the SELECT statement.
Hopefully, this execution plan demonstrates why a dynamic cursor may
be costly to the system. It's performing a clustered index insert, as well
as the reads necessary to return the data to the cursor, as each of the
fifteen separate FETCH statements are called. The same query, outside
a cursor, would return a very simple, one-step execution plan:
178
Figure 13
STATIC Cursor
Unlike the Dynamic cursor, outlined above, the Static cursor is a
temporary copy of the data, created when the cursor is called. This
means that it doesn't get underlying changes to the data over the life of
the cursor. To see this in action, change the cursor declaration as
follows:
Logical Operators
Now generate an estimated execution plan. You should see six distinct
plans again. Figure 14 shows the plan for the first query, which
represents the cursor definition. The remaining queries in the estimated
plan look just like the Dynamic query in Figure 2.
Figure 14
Starting at the top right, as usual, we see an Index Scan to get the data
out of the Sales.Currency table. Data from here is passed to the
Segment operator. The Segment operator divides the input into
segments, based on a particular column, or columns. In this case, as you
can see in the ToolTip, it's based on a derived column called
Segment1006. The derived column splits the data up in order to pass it
to the next operation, which will assign the unique key.
Chapter 6: Cursor Operations 179
Figure 15
Cursors require work tables and to make these tables efficient, SQL
Server creates them as a clustered index with a unique key. This time, in
the Static cursor, it generates the key after the segments are defined. The
segments are passed on to the Compute Scalar operator, which adds a
string valued "1" for the next operation, Sequence Project. This logical
operator represents a physical task that results in a Compute Scalar
operation. It's adding a new column as part of computations across the
set of data. In this case, it's creating row numbers through an internal
function called i4_row_number. These row numbers are used as the
identifiers within the clustered index.
180
Figure 16
The data, along with the new identifiers, is then passed to the Clustered
Index Insert operator and then on to the Population Query cursor
operator.
Population Query
The Population Query cursor operator "populates the work table for a
cursor when the cursor is opened" or in other words, from a logical
stand-point, this is when the data that has been marshaled by all the
other operations is loaded into the work table (the clustered index).
The Fetch Query operation retrieves the rows from the cursor via an
index seek on our tembdb index. Notice that, in this case, the Fetch
Query operation is defined in a separate sequence, independent from
the Population Query. This is because this cursor is static, meaning that
it doesn't update itself as the underlying data updates, again, unlike the
dynamic cursor which reads its data each time it's accessed.
Snapshot
dynamic cursor we were provided by default. The Index Seek and the
Fetch operations show how the data will be retrieved from the cursor.
Physical Operators
If we execute the query and display the Actual Execution plan, we get
two distinct plans. The first plan is the query that loads the data into the
cursor work table, as represented by the clustered index. The second
plan is repeated and we see a series of plans identical to the one shown
for Query 2 below, which demonstrate how the cursor is looped
through by the WHILE statement.
Figure 10
These execution plans accurately reflect what the estimated plan
intended. Note that the cursor was loaded when the OPEN CURSOR
statement was called. We can even look at the Clustered Index Seek
operator to see it using the row identifier created during the population
of the cursor.
182
Figure 11
KEYSET Cursor
The dynamic cursor, our first example, retrieves data every time a
FETCH statement is issued against the cursor, moving through the
data within the cursor in any direction, so that it can "dynamically"
retrieve changes to the data as well as account for inserts and deletes.
The static cursor, as described above, simply retrieves the data set
needed by the cursor a single time.
The Keyset cursor retrieves a defined set of keys as the data defined
within the cursor, but it allows for the fact that data may be updated
during the life of the cursor. This behavior leads to yet another
execution plan, different from the previous two examples.
Let's change the cursor definition again:
Logical Operators
The estimated execution plan should look as shown in Figure 19:
Figure 12
Now that we've worked with cursors a bit, it's easy to recognize the two
paths defined in the estimated plan; one for populating the cursor and
one for fetching the data from the cursor.
The top line of the plan, containing the Population Query operation, is
almost exactly the same as that defined for the Static cursor. The second
Scalar operation is added as a status check for the row. It ends with the
Keyset cursor operator, indicating that the cursor can see updates, but
not inserts.
The major difference is evident in how the Fetch Query works, in order
to support the updating of data after the cursor was built. Figure 20
shows that portion of the plan in more detail:
Figure 20
Going to the right and top of the Fetch Query definition, we find that it
first retrieves the key from the index created in the Population Query.
Then, to retrieve the data, it joins it, through a Nested Loop operation
to the Sales.Currency table. This is how the KeySet cursor manages to
get updated data into the set returned while the cursor is active.
The Constant Scan operator scans an internal table of constants. The
data from the constant scan feeds into the Clustered Index Update
operator, in order to be able to change the data stored, if necessary. This
data is joined to the first set of data through a Nested Loop operation
and finishes with a Compute Scalar representing the row number.
184
Physical Operators
When the cursor is executed for real, we get the plan shown in Figure
21:
Figure 21
Step one contains the OPEN CURSOR operator, and populates the
key set exactly as the estimated plan envisioned.
In Query 2, the FETCH NEXT statements against the cursor activate
the FETCH CURSOR operation fifteen times as the cursor walks
through the data. While this can be less costly than the dynamic cursors,
it's clearly more costly than a static cursor. The performance issues
come from the fact that the cursor queries the data twice, once to load
the key set and a second time to retrieve the row data. Depending on
the number of rows being retrieved into the work table, this can be a
costly operation.
READ_ONLY Cursor
Each of the preceding cursors, except for static, allowed the data within
the cursor to be updated. If we define the cursor as READ_ONLY
and run "Display Estimated Execution Plan":
Figure 22
Chapter 6: Cursor Operations 185
Clearly, this represents the simplest cursor definition plan that we've
examined. Unlike for other types of cursor, there is no branch of
operations within the estimated plan. It simply reads what it needs
directly from the data. In our case, an Index Scan operation against
CurrencyName index shows how this is accomplished. The amount of
I/O, compared to any of the other execution plans, is reduced since
there is not a requirement to populate any work tables. Instead there is a
single step: get the data. The actual execution plan is identical except
that it doesn't have to display the Fast Forward logical operator.
,soh.[TotalDue]
FROM [Sales].[SalesOrderHeader] AS soh
JOIN [Sales].[Store] AS s
ON soh.[CustomerID] = s.[CustomerID]
WHERE soh.[CustomerID] = 17
ORDER BY soh.[OrderDate]
OPEN ChangeData
WHILE @@FETCH_STATUS = 0
BEGIN
-- Normally there would be operations here using data
from cursor
IF @TotalDue < 1000
UPDATE @WorkTable
SET SaleType = 'Poor'
WHERE [DateOrderNumber] = @DateOrderNumber
ELSE
IF @TotalDue > 1000
AND @TotalDue < 10000
UPDATE @WorkTable
SET SaleType = 'OK'
WHERE [DateOrderNumber] = @DateOrderNumber
ELSE
IF @TotalDue > 10000
AND @TotalDue < 30000
UPDATE @WorkTable
SET SaleType = 'Good'
WHERE [DateOrderNumber] =
@DateOrderNumber
ELSE
UPDATE @WorkTable
SET SaleType = 'Great'
WHERE [DateOrderNumber] =
@DateOrderNumber
FETCH NEXT FROM ChangeData INTO @DateOrderNumber,
@TotalDue
END
CLOSE ChangeData
DEALLOCATE ChangeData
SELECT *
FROM @WorkTable
Whether or not you've written a query like this, you've certainly seen
them. The data returned from the query looks something like this:
Number Name OrderDate TotalDue SaleType
1 Trusted Catalog Store 2001-07-01 18830.1112 Good
Chapter 6: Cursor Operations 187
The Estimated Execution Plan (not shown here) displays the plan for
populating the temporary table, and updating the temporary table, as
well as the plan for the execution of the cursor. The cost to execute this
script, as a dynamic cursor, includes not only the query against the
database tables, Sales.OrderHeader and Sales.Store, but the insert
into the temporary table, all the updates of the temporary table, and the
final select from the temporary table. The result is about 27 different
scans and about 113 reads.
Let's take a look at a sub-section of the Actual Execution Plan, which
shows the fetch from the cursor and one of the updates:
Figure 23
We can see that each of the cycles through the cursor accounts for
about 6% of the total cost of the entire script, with the fetch from the
cursor accounting for half that cost, repeated for each of the 12 rows.
Focusing on the top of the plan, where we see the cursor performing,
we can see that 25% of the cost comes from pulling data from the
temporary table. This data is then passed to a Compute Scalar, which
assigns a row id value. This data is then inserted into the clustered index
that is the cursor and finally the FETCH CURSOR operator
represents the actual retrieval of the data from the cursor.
To see which cursor might perform better, we'll change this dynamic
cursor to a static one by modifying the script slightly:
188
Now, with the rest of the code the same, let's re-examine the same sub-
section of the Actual Execution Plan:
Figure 24
Notice that the cursor is now only accounts for 1% of the total cost of
the operation, because the Static cursor only has to access what's
available, not worry about retrieving it from the table again. However
this comes at a cost. The original query ran in approximately 46ms. This
new query is running approximately 75ms. The added time comes from
loading the static data.
Let's see how the Keyset cursor fairs. Change the script so that the
keyset declaration reads:
Figure 25
Again, the cost relative to the overall cost of the script is only 1%, but
unlike the STATIC cursor, the snapshot cursor performs slightly better
Chapter 6: Cursor Operations 189
(30ms) because this time only the key values are moved into the work
table for the cursor.
Let's change the cursor again to see the read only option:
Now, the same sub-section of the plan looks as shown in Figure 26:
Figure 26
Here again, the FETCH from the cursor only accounts for 1% of the
overall cost, but the load of the read only cursor takes a bit longer, so
this one is back up to about 40ms.
If these tests hold relatively true, then the keyset cursor is the fastest at
the moment. Let's see if we can't make it a bit faster. Change the cursor
declaration so that it reads like this:
The resulting execution plan is the same, and the performance isn't
really changed. So, short of tuning other parts of the procedure, the
simple KEYSET is probably the quickest way to access this data.
However, what if we eliminate the cursor entirely? We can rewrite the
script so that it looks like this:
This query returns exactly the same data. But the performance is
radically different. It performs a single scan on SalesOrderHeader
table and about 40 reads between the two tables. The execution time is
recorded as 0ms, which isn't true, but gives you an indication of how
much faster it is than the cursor. Instead of a stack of small execution
plans, we have a single step execution plan:
Figure 27
The plan is actually a bit too large to see clearly here but the key take-
away is that the main cost for this query is the operator at the lower
right. This is a key lookup operation that takes up 54% of the cost.
That's a tuning opportunity, as we saw in the previous chapter.
Eliminating the lookup will make this query even faster.
This example was fairly simple. The amount of data was relatively small
and most of the cursors operated well enough to be within the
performance margins of most large scale systems. However, even with
all that, it was possible to see differences between the types of cursors
Chapter 6: Cursor Operations 191
Summary
More often than not, cursors should be avoided in order to take
advantage of the set-based nature of T-SQL and SQL Server. Set-based
operations just work better. However, when you are faced with the
necessity of a cursor, understanding what you're likely to see in the
execution plans, estimated and actual, will assist you in using the cursor
appropriately.
Don't forget that the estimated plan shows both how the cursor will be
created, in the top part of the plan, and how the data in the cursor will
be accessed, in the bottom part of the plan. The primary differences
between a plan generated from a cursor and one from a set-based
operation are in the estimated execution plans. Other than that, as you
have seen, reading these plans is really no different than reading the
plans from a set-based operation: start at the right and top and work
your way to the left.. There are just a lot more plans generated by the
nature of how cursors work.
Chapter 7: XML in Execution Plans 193
FOR XML
If you want to output the result of a query in XML format, then you
can use the FOR XML clause. You can use the FOR XML clause in
one of the following four modes:
• AUTO – returns results as nested XML elements in a simple
hierarchy (think: table = XML element)
• RAW – transforms each row in the results into an XML
element, with a generic <row /> identifier as the element tag.
• EXPLICIT – allows you to explicitly define the shape of the
resulting XML tree, in the query itself
• PATH – A simpler alternative to EXPLICIT for controlling
elements, attributes and the overall shape of the XML tree.
Each of these methods requires a different type of T-SQL in order to
arrive at the same type of output. These queries have different
performance and maintenance issues associated with them. We will
explore all three options and point out where each has strengths and
weaknesses.
In our first example, the requirement is to produce a simple list of
employees and their addresses. There is no real requirement for any type
of direct manipulation of the XML output and the query is simple and
straight forward, so we'll use XML AUTO mode. Here's the query:
SELECT c.[FirstName],
c.[LastName],
c.[EmailAddress],
c.[Phone],
e.[EmployeeID],
e.[Gender],
a.[AddressLine1],
a.[AddressLine2],
a.[City],
a.[StateProvinceID],
a.[PostalCode]
FROM [Person].[Contact] c
INNER JOIN [HumanResources].[Employee] e
ON c.[ContactID] = e.[ContactID]
INNER JOIN [HumanResources].[EmployeeAddress] ea
ON e.[EmployeeID] = ea.[EmployeeID]
INNER JOIN [Person].[Address] a
ON ea.[AddressID] = a.[AddressID]
FOR XML AUTO
Figure 1
The difference between this execution plan and that for any "normal"
query may be hard to spot. It's at the very end. Instead of a T-SQL
SELECT operation, we see an XML SELECT operation. That is the
only real change. Otherwise it's simply a query.
Let's consider a second, somewhat simpler, query and compare the
output using the various modes. Starting with AUTO mode again:
Figure 2
The estimated cost of the plan is 0.12. The XML output looks as
follows:
</s>
The same results are seen, in this case, if we use XML RAW mode.
XML EXPLICIT mode allows you to exert some control over the
format of the XML generated by the query – for example, if the
application or business requirements may need a very specific XML
definition, rather than the generic one supplied by XML AUTO.
Without getting into a tutorial on XML EXPLICIT, you write the
query in a way that dictates the structure of the XML output, through a
series of UNION operations. Here is a simple example:
SELECT 1 AS Tag,
NULL AS Parent,
s.Name AS [Store!1!StoreName],
NULL AS [Contact!2!ContactID],
NULL AS [Contact!2!ContactTypeID]
FROM Sales.Store s
JOIN [Sales].[StoreContact] c ON s.[CustomerID] =
c.[CustomerID]
UNION ALL
SELECT 2 AS Tag,
1 AS Parent,
s.Name AS StoreName,
c.ContactID,
c.ContactTypeID
FROM Sales.Store s
JOIN [Sales].[StoreContact] c ON s.[CustomerID] =
c.[CustomerID]
ORDER BY [Store!1!StoreName],
[Contact!2!ContactID]
FOR XML EXPLICIT
The actual execution plan for this query is somewhat more complex and
is shown in Figure 3:
Figure 3
Chapter 7: XML in Execution Plans 197
The estimated cost of the plan is much higher at 0.29. The XML
output, in this case, looks as follows:
If you remove the FOR XML EXPLICIT clause and recapture the
plan then you'll see that, apart from seeing the Select instead of XML
Select operator, the plans are the same in every way, up to and including
the cost of each of the operations. The difference isn't in the execution
plan, but rather in the results. With FOR XML EXPLICIT you get
XML, without it, you get an oddly-formatted result set.
Even with this relatively simple example, you can see how, because of
the multiple queries unioned together, while you get more control over
the XML output, it comes at the cost of increased maintenance, due to
all the UNION operators and the explicit naming standards, and
decreased performance due to the increased number of queries required
to put the data together.
An extension of the XML AUTO mode allows you to specify the
TYPE directive in order to better control the output the results of the
query as the XML datatype. The following query is essentially the same
as the previous one, but is expressed using this simpler syntax that is
now available in SQL Server 2005:
The ELEMENTS directive specifies that the columns within the sub-
select appear as sub-elements within the outer select statement, as part
of the structure of the XML:
Figure 4
The estimated cost of the plan is 0.23. Two UDX operators have been
introduced. The UDX operator is an extended operator used by
XPATH and XQUERY operations. XPATH and XQUERY are two
different ways to querying XML data directly. In our case, by examining
the properties window, we can see that the UDX operator on the lower
right of the plan is creating the XML data:
Chapter 7: XML in Execution Plans 199
Figure 5
The output is Expr1004, which consists of the two columns from the
StoreContact table: ContactID and ContactTypeID. This data is
joined with the sorted data from the clustered index scan on the Stores
table. The next UDX operator takes this data which as been joined
through a nested loop with the outer query against the Store data and
then given a Scalar, probably some of the XML definitions or a
checksum (calculation value), for the final output as full fledged XML.
Finally, the XML PATH mode simply outputs the XML data type and
makes it much easier to output mixed elements and attributes. Using this
mode, the query we've already walked through twice now looks like this:
200
However, since more often than not, the XML created in the AUTO
doesn't meet with the application design, you'll probably end up using
XML PATH most often.
XML EXPLICIT ended up fairly poorly with more scans and reads than
the previous two options:
Table 'Worktable'. Scan count 0, logical reads 0, …
Table 'StoreContact'. Scan count 2, logical reads 8, …
Table 'Store'. Scan count 2, logical reads 206, …
XML AUTO with TYPE was truly horrendous due to the inclusion of
the UDX operations, causing a large number of reads and scans:
Chapter 7: XML in Execution Plans 201
OPENXML
To read XML within SQL Server, you can use OPENXML or XQuery.
OPENXML takes in-memory XML data and converts it into a format
that, for viewing purposes, can be treated as if it were a normal table.
This allows you to use it within regular T-SQL operations. It's most
often used when you need to take data from the XML format and
change it into structured storage within a normalized database. In order
to test this, we need an XML document.
<ROOT>
<Currency CurrencyCode="UTE" CurrencyName="Universal
Transactional Exchange">
<CurrencyRate FromCurrencyCode="USD" ToCurrencyCode="UTE"
CurrencyRateDate="1/1/2007" AverageRate=".553"
EndOfDateRate= ".558" />
<CurrencyRate FromCurrencyCode="USD" ToCurrencyCode="UTE"
CurrencyRateDate="6/1/2007" AverageRate=".928"
EndOfDateRate= "1.057" />
</Currency>
</ROOT>
BEGIN TRAN
DECLARE @iDoc AS INTEGER
DECLARE @Xml AS NVARCHAR(MAX)
[CurrencyCode],
[Name],
[ModifiedDate]
)
SELECT CurrencyCode,
CurrencyName,
GETDATE()
FROM OPENXML (@iDoc, 'ROOT/Currency',1) WITH (
CurrencyCode NCHAR(3), CurrencyName NVARCHAR(50) )
From this query, we get two actual execution plans, one for each
INSERT. The first INSERT is against the Currency table, as shown
in Figure 6:
Figure 6
A quick scan of the plan reveals no new XML icons. All the
OPENXML statement processing is handled within the Remote Scan
icon. This operator represents the opening of a DLL within SQL
Server, which will take the XML and convert it into a format within
Chapter 7: XML in Execution Plans 203
memory that looks like a table of data to the query engine. Since the
Remote Scan is not actually part of the query engine itself, the call
outside the query engine is represented by the single icon.
Examining the estimated plan reveals none of the extensive XML
statements that are present in this query: even the XML stored
procedures sp_xml_preparedocument and sp_xml_remove
document are referenced by simple logical T-SQL icons, as you can see
in Figure 7.
Figure 7
The only place where we can really see the evidence of the XML is in
the Output List for the Remote Scan. Here, in Figure 8, we can see the
OPENXML statement referred to as a table, and the properties selected
from the XML data listed as columns.
Figure 8
From there, it's a fairly straight-forward query with the data being sorted
first for insertion into the clustered index and then a second time for
addition to the other index on the table.
The second execution plan describes the INSERT against the
CurrencyRate table:
Figure 9
This query is the more complicated of the pair, because of the extra
steps required for the maintenance of referential integrity between the
Currency and CurrencyRate tables. Yet still, we see no XML icons
204
because the Remote Scan operation again takes the task of gathering the
new rows for the table. In this case, two comparisons against the parent
table are made through the Merge Join operations. The data is sorted,
first by FromCurrencyCode and then by ToCurrencyCode in order
for the data to be used in a Merge Join, the operation picked by the
Optimizer in this instance.
It's really that easy to bring XML data into the database for use within
your queries, or for inclusion within your database. As discussed
previously, OPENXML is a useful tool for importing the semi-
structured data within the XML documents into the well-maintained,
relational database structure. It can also allow you to pass in data for
other uses. For example, you can pass in a list of variables to be used as
a join in a SELECT statement. The main point to take away is that
once the OPENXML has been formatted, you get to use it as if it were
just another table within your queries..
One caveat worth mentioning: parsing XML uses a lot of memory. You
should plan on opening the XML, getting the data out, and then closing
and deallocating the XML parser as soon as possible. This will reduce
the amount of time that the memory is allocated within your system.
XQuery
Along with the introduction of the XML data type in SQL Server 2005,
came the introduction of XQuery as a method for querying XML data.
Effectively, the inclusion of XQuery gives you a whole new query
language to learn in addition to T-SQL. The XML data type is the
mechanism used to provide the XQuery functionality through the SQL
Server system. When you want to query from the XML data type, there
are five basic methods, each of which is reflected in execution plans in
different ways:
• .query(): used to query the xml data type and return the xml
data type
• .value(): used to query the xml data type and return a non-xml
scalar value
• .nodes(): a method for pivoting xml data into rows
• .exist(): queries the xml data type and returns a Bool to
indicate whether or not the result set is empty , just like the
EXISTS keyword in TSQL
• .modify(): a method for inserting, updating and deleting XML
snippets within the XML data set.
Chapter 7: XML in Execution Plans 205
The various options for running a query against XML, including the use
of FLWOR (for, let, where, order by and return) statements within the
queries, all affect the execution plans. I'm going to cover just two
examples to acquaint you with the concepts and introduce you to the
sort of execution plans you can expect to see. It's outside the scope of
this book to cover this topic in the depth that would be required to
cover all aspects of this new language.
SELECT c.[LastName],
c.[FirstName],
e.[HireDate],
e.[Title]
FROM [Person].[Contact] c
INNER JOIN [HumanResources].[Employee] e
ON c.[ContactID] = e.[ContactID]
INNER JOIN [HumanResources].[JobCandidate] jc
ON e.[EmployeeID] = jc.[EmployeeID]
AND jc.[Resume].exist(' declare namespace
res="http://schemas.microsoft.com/sqlserver/2004/07/
adventure-works/Resume";
/res:Resume/res:Employment/res:Emp.JobTitle[contains
(.,"Sales Manager")]') = 1
The query, in this case, finds a single employee who was formerly a sales
manager, and results in the execution plan in Figure 10:
Figure 10
Starting at the usual location, top and right, we see a normal execution
plan. A Clustered Index Scan operation against the JobCandidate
table is followed by a Filter operation that ensures that the Resume field
206
is not null. A Nested Loop join is used to combine this data from the
filtered JobCandidate table with data returned from the Employee
table, filtering us down to two rows.
Then, another Nested Loop operator is used combine data from a new
source, a Table Valued Function. This Table Valued Function is
subtitled "XML Reader with XPath filter". This operation represents as
relational data the output from the XQuery. The role it plays is not
dissimilar to that of the Remote Scan operation from the OPENXML
query. However, the TVF, unlike the Remote Scan in the example above,
is actually a part of the query engine and represented by a distinct icon.
The property sheet for the Table Valued Function shows that four rows
were found:
Figure 131
These rows are then passed to a Filter operator that determines if the
XPath query we defined equals one. This results in a single row for
output to the Nested Loop operator. From there it's a typical execution
plan, retrieving data from the Contact table and combining it with the
rest of the data already put together.
Chapter 7: XML in Execution Plans 207
SELECT s.Demographics.query('
declare namespace
ss="http://schemas.microsoft.com/sqlserver/2004/07/
adventureworks/StoreSurvey";
for $s in /ss:StoreSurvey
where ss:StoreSurvey/ss:SquareFeet > 20000
return $s
') AS Demographics
FROM [Sales].[Store] s
WHERE s.[SalesPersonID] = 279
208
Figure 12
The query actually consisted of two simple queries
• A regular T-SQL query against the Store table to return the
rows where the SalesPersonId = 279,
• A query that uses the .query method return the data where the
Store's square footage was over 20000
Stated that way, it sounds simple, but a lot more work was necessary
around those two queries to arrive at a result set..
As always, start at the top and right of Figure 12. The first operator is a
Clustered Index Scan against the Sales table, filtered by the
SalesPersonId. The data returned is fed into the top half of a Nested
Loop, left outer join. Going over to the right to find the second stream
of data for the join, we find a familiar operation: a Clustered Index
Seek. This time though, it's going against an XML clustered index.
Chapter 7: XML in Execution Plans 209
Figure 13
You can see in Figure 13 that the index seek is occurring on
PXML_Store_Demographics, returning the 80 rows from the index
that match on the CustomerId field from the store. Below this, another
Clustered Index Seek gathers data matching the CustomerId, but
adds the SquareFeet as part of the output. This data is filtered and
then the outputs are combined through a Left Join.
From there, it feeds on out joining against all the rest of the XML data
before going through a UDX operator that outputs the formatted XML
data. This is all then combined with the original rows returned from the
Store table. Of note is the fact that the XQuery information is being
treated almost as if it were T-SQL. The data above is being retrieved
210
from an XML index which stores all the data with multiple rows for
each node, sacrificing disk space for speed of recovery.
Summary
These examples don't begin to cover the depth of what's available
within XQuery. Functions for aggregating XML data are available. You
can pass variables from T-SQL into the XQuery commands. It really is a
whole new language and syntax that you'll have to learn in order to take
complete advantage of what it has to offer. For an even more thorough
introduction, read this white paper offered from Microsoft :
http://msdn2.microsoft.com/en-us/library/ms345122.aspx
It can take the place of FOR XML, but you might see some
performance degredation.
You can also use XQuery in place of OPENXML. The functionality
provided by XQuery goes way beyond what's possible within
OPENXML. Combining that with TSQL will make for a powerful
combination when you have to manipulate XML data within SQL
Server. As with everything else, please test the solution with all possible
tools to ensure that you're using the optimal one for your situation.
Chapter 8: Advanced Topics 211
optimizer, why, and how to change them, becomes that much more
important.
Let's take a look at what I'd consider a reasonably large-scale execution
plan (although I've seen much larger). The following stored procedure
returns the appropriate data set, based on whether or not any special
offers were used by a particular individual. In addition, if a particular
special offer is being requested, then procedure executes a different
query and returns a second, different result set.
ON soh.[SalesOrderID] = sod.[SalesOrderID]
INNER JOIN [Sales].[SpecialOffer] spo
ON sod.[SpecialOfferID] =
spo.[SpecialOfferID]
INNER JOIN [Production].[Product] p
ON sod.[ProductID] = p.[ProductID]
WHERE c.ContactID = @ContactId
AND sod.[SpecialOfferID] =
@SpecialOfferId;
END
-- use different query to return other data set
ELSE
BEGIN
SELECT c.[LastName] + ', ' + c.[FirstName]
,c.[EmailAddress]
,i.[Demographics]
,soh.SalesOrderNumber
,sod.[LineTotal]
,p.[Name]
,p.[ListPrice]
,sod.[UnitPrice]
,st.[Name] AS StoreName
,ec.[LastName] + ', ' + ec.[FirstName] AS
SalesPersonName
FROM [Person].[Contact] c
INNER JOIN [Sales].[Individual] i
ON c.[ContactID] = i.[ContactID]
INNER JOIN [Sales].[SalesOrderHeader] soh
ON i.[CustomerID] = soh.[CustomerID]
INNER JOIN [Sales].[SalesOrderDetail] sod
ON soh.[SalesOrderID] =
sod.[SalesOrderID]
INNER JOIN [Production].[Product] p
ON sod.[ProductID] = p.[ProductID]
LEFT JOIN [Sales].[SalesPerson] sp
ON soh.SalesPersonID = sp.SalesPersonID
LEFT JOIN [Sales].[Store] st
ON sp.SalesPersonID = st.SalesPersonID
LEFT JOIN [HumanResources].[Employee] e
ON sp.SalesPersonID = e.[EmployeeID]
LEFT JOIN Person.[Contacct] ec
ON e.[ContactID] = ec.[ContactID]
WHERE i.[ContactID] = @ContactId;
END
END TRY
BEGIN CATCH
214
EXEC [Sales].[uspGetDiscountRates]
@ContactId = 12298, -- int
@SpecialOfferId = 16 -- int
Figure 14
This image of the data set does not include all the columns, but you can
see that two result sets were returned, the first being the results that had
no discounts and the second being the query that runs if the special
offer is passed to the query. The estimated execution plan is shown in
Figure 2:
Chapter 8: Advanced Topics 215
Figure 2
Obviously, this plan is unreadable without drilling down. However, even
from this macro view, you can still see the logical steps of the query.
The first grouping of icons describes the first query that checks for the
existence of the special offer. The second, larger group of icons
describes the query that runs when there are no special offers. Finally,
the third group of icons describes the last query, which runs when the
script receives the SpecialOfferID = 16.
While this execution plan may look intimidating, it is not doing anything
that we haven't seen elsewhere. It's just doing a lot more of it. The key
to investigating plans of this type is to not be daunted by their size and
remember the basic methods for walking the plan. Start at the top and
on the right and work your way through.
You have at least one tool that can help you when working with a large
graphical plan. In the lower right of the results pane in the query
window, when you're looking at an execution plan, you'll see a little plus
sign, as shown in Figure 3:
216
Figure 3
Click on the plus sign to open a little window, showing a representation
of the entire execution plan. Keep your mouse button depressed, and
drag the cursor across the window. You'll see that this moves a small
"viewing rectangle" around the plan, as shown in Figure 4:
Figure 4
As you drag the viewable area, you'll notice that the main display in the
results pane tracks your mouse movements. In this way, you can
navigate around a large execution plan and keep track of where you are
within the larger context of the plan, as well as view the individual
operators that you're investigating.
As to the procedure itself, the only point worth noting here is that the
majority of the cost of the query (62%), as currently written, is a
Clustered Index Scan operator against the Sales.Individual table.
None of the existing indexes on that table include the ContactID
column, at least not in a way that can be used by the optimizer for these
queries. Adding an index to that column radically enhances the
performance of the query.
When dealing with large scale plans, you may opt to capture the XML
plan and then use the search capabilities inherent in XML to track down
issues such as Clustered Index Scans. Be warned, though, that as
difficult as navigating a large scale execution plan in the graphical
format becomes, that problem is multiplied within XML with all it's
extra data on display. If you're just getting started with execution plans
this large, it might be better to stay away from the XML, but be aware
that it is available as an added tool.
Chapter 8: Advanced Topics 217
In summary, the operations for a large scale execution plan are not
different from any other you have seen in this book; there are just more
of them. Don't be intimidated by them. Just start at the top right, in the
normal fashion and work your way through in stages, using the scrolling
window to navigate around, if necessary.
13In addition to these system settings, you can also affect the number of
processors used by a query by supplying the MAXDOP query hint, as
described in Chapter 5.
218
once for a plan that does. When a plan is reused, it is examined for the
number of threads it used the last time. The query engine, at execution
time, then determines whether that same number will be used, based on
the current system load and the number of threads available.
For more detail, read the article. However, I'll repeat the warning from
the article: never do this on a production system.
We're starting with an aggregation query, or the sort that you might see
in a data mart. If the data set that this query operated against was very
large, it might benefit from parallelism. Here's the query:
SELECT [so].[ProductID]
,COUNT(*) AS Order_Count
FROM [Sales].[SalesOrderDetail] so
WHERE [so].[ModifiedDate] >= '2003/02/01'
AND [so].[ModifiedDate] < DATEADD(mm, 3,
'2003/02/01')
GROUP BY [so].[ProductID]
ORDER BY [so].[ProductID]
If we take a look at the estimated execution plan, we'll see the fairly
straight forward plan shown in Figure 5:
Figure 5
There is nothing in this plan that we haven't seen before, so we'll move
on to see what would happen to this plan if it were executed with the
use of multiple processors. In order to force the optimizer to use a
parallel plan, change the Parallelism Threshold to 1 from whatever value
it is now (5 by default). Then, we can run this query and obtain a parallel
execution plan:
Select "Include Actual Execution Plan" so that you generate both the
graphical and XML versions of the plan. The graphical execution plan is
shown in Figure 6:
Figure 6
The first thing that will probably jump out at you, in addition to the new
operators that support parallelism, is the small yellow icon with two
arrows, which is attached to the otherwise familiar operators. This icon
designates that these operators as being used within a parallel processing
stream. If we examine the XML plan, we begin to see how parallelism is
implemented:
This Gather Streams operation finds all the various threads from the
different processors and puts it back together into a single stream of
data. In addition to Gather Streams and Repartition Streams,
described above, the Parallelism operator can also Distribute Streams,
which takes a single stream of data and splits it into multiple streams for
parallel processing in a logical plan.
As you can see in the RunTimeInformation element, after gathering
the other streams together for output to the Select operator, we're now
only dealing with a single thread.
Once you're done experimenting with parallelism, be sure to reset the
Parallelism Threshold to where it was on your system (the default is 5).
Figure 7
This action is performed by the optimizer in an effort to create plans
that are more likely to be reused. The optimizer is only able to perform
this function on relatively simple queries. The parameters created are as
close to the correct data type as the optimizer can get but, since it's just
an estimation, it could be wrong.
The optimizer arbitrarily provides the names for these parameters as
part of the process. It may or may not generate the same parameter
names, in the same order, from one generation of the execution plan of
the query in question to the next. As the queries get more complex it
may be unable to determine whether or not a hard-coded value should
be parameterized.
This is where Forced Parameterization comes into play. Instead of the
occasional parameter replacing a literal value, based on the simple rules,
Chapter 8: Advanced Topics 225
SQL Server attempts to replace all literal values with a parameter, with
the following important exceptions:
• Literals in the select list of any SELECT statement are not
replaced
• Parameterization does not occur within individual T-SQL
statements inside stored procedures, triggers and UDFs, which
get execution plans of their own.
• XQuery literals are not replaced with parameters
A very long list of other explicit exceptions is detailed in the Books
Online.
The goal of using forced parameterization is to reduce recompiles as
much as possible. Even when taking this more direct control over how
the optimizer behaves, you have no control over the parameter name,
nor can you count on the same name being used every time the
execution plan is generated. The order in which parameters are created
is also arbitrary. Crucially, you also can't control the data types picked
for parameterization. This means that if the optimizer picks a particular
data type that requires a CAST for comparisons to a given column, then
you may not see applicable indexes being used. Therefore, using forced
parameterization can result in sub-optimal execution plans being
selected.
So why would you want to use it? A system developed using stored
procedures with good parameters of appropriate data types is very
unlikely to benefit from forced parameterization. However a system that
has been developed with most of the T-SQL being ad-hoc, or client
generated, may contain nothing but hard-coded values. This type of
system could benefit greatly from forced parameterization. As with any
other attempts to force control out of the hands of the optimizer and
the query engine, testing is necessary.
Normally, forced parameterization is set at the database level. You have
the option of choosing to set it on for a single query using the query
hint PARAMETERIZATION FORCED, but this hint is only
available as a Plan Guide, which will be covered in the next section.
In this example, we have several literals used as part of the query, which
is a search to find email addresses that start with the literal, 'david':
SELECT 42 AS TheAnswer
,c.[EmailAddress]
,e.[BirthDate]
,a.[City]
226
FROM [Person].[Contact] c
JOIN [HumanResources].[Employee] e
ON c.[ContactID] = e.[ContactID]
JOIN [HumanResources].[EmployeeAddress] ea
ON e.[EmployeeID] = ea.[EmployeeID]
JOIN [Person].[Address] a
ON ea.[AddressID] = a.[AddressID]
JOIN [Person].[StateProvince] sp
ON a.[StateProvinceID] = sp.[StateProvinceID]
WHERE c.[EmailAddress] LIKE 'david%'
AND sp.[StateProvinceCode] = 'WA' ;
Run the query, and then let's take a look at the cached plans (see
Chapter 1 for more details):
SELECT [cp].[refcounts]
,[cp].[usecounts]
,[cp].[objtype]
,[st].[dbid]
,[st].[objectid]
,[st].[text]
,[qp].[query_plan]
FROM sys.dm_exec_cached_plans cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st
CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle)
qp ;
The query stored is identical with the query we wrote. In other words,
no parameterization occurred. The graphical execution plan looks as
shown in Figure 8:
Figure 8
Let's now enable forced parameterization and clean out the buffer cache
so that we're sure to see a new execution plan:
DBCC freeproccache
GO
If you run the query again, you'll see that the execution plan is the same
as that shown in Figure 8. However, the query stored in cache is not the
same. It now looks like this (formatted for readability):
(@0 varchar(8000))
select 42 as TheAnswer
,c.[EmailAddress]
,e.[BirthDate]
,a.[City]
from [Person].[Contact] c
join [HumanResources].[Employee] e
on c.[ContactID] = e.[ContactID]
join [HumanResources].[EmployeeAddress] ea
on e.[EmployeeID] = ea.[EmployeeID]
join [Person].[Address] a
on ea.[AddressID] = a.[AddressID]
join [Person].[StateProvince] sp
on a.[StateProvinceID] = sp.[StateProvinceID]
where c.[EmailAddress] like 'david%'
and sp.[StateProvinceCode] = @0
AS (
SELECT e.[EmployeeID], e.[ManagerID], c.[FirstName],
c.[LastName], 0
-- Get the initial list of Employees for Manager n
FROM [HumanResources].[Employee] e
INNER JOIN [Person].[Contact] c
ON e.[ContactID] = c.[ContactID]
WHERE [ManagerID] = @ManagerID
UNION ALL
SELECT e.[EmployeeID], e.[ManagerID], c.[FirstName],
c.[LastName], [RecursionLevel] + 1
-- Join recursive member to anchor
FROM [HumanResources].[Employee] e
INNER JOIN [EMP_cte]
ON e.[ManagerID] = [EMP_cte].[EmployeeID]
INNER JOIN [Person].[Contact] c
ON e.[ContactID] = c.[ContactID]
)
-- Join back to Employee to return the manager name
SELECT [EMP_cte].[RecursionLevel],
[EMP_cte].[ManagerID], c.[FirstName] AS
''ManagerFirstName'', c.[LastName] AS
''ManagerLastName'',
[EMP_cte].[EmployeeID], [EMP_cte].[FirstName],
[EMP_cte].[LastName] -- Outer select from the CTE
FROM [EMP_cte]
INNER JOIN [HumanResources].[Employee] e
ON [EMP_cte].[ManagerID] = e.[EmployeeID]
INNER JOIN [Person].[Contact] c
ON e.[ContactID] = c.[ContactID]
ORDER BY [RecursionLevel], [ManagerID], [EmployeeID]
OPTION (MAXRECURSION 25) ', @type = N'OBJECT',
@module_or_batch = N'dbo.uspGetManagerEmployees',
@params = NULL,
@hints = N'OPTION(RECOMPILE,MAXRECURSION 25)'
First, we use the @name parameter to give our plan guide a name, in
this case MyFirstPlanGuide. Note that plan guide names operate
within the context of the database, not the server.
The @stmt parameter has to be an exact match to the query that the
query optimizer will be called on to match. White space and carriage
returns don't matter, but in order to create the above, I had to include
the CTE. Without it I was getting errors. When the optimizer finds code
that matches, it will look up and apply the correct plan guide.
The @type parameter is going to be a database object, so this would be
referred to as an object plan guide.
In the @module_or_batch parameter, we specify the name of the
target object, if we're creating an object plan guide, as in this case. We
supply null otherwise.
230
We use @params only if we're using a template plan guide and forced
parameterization. Since we're not, it's null in this case. If we were
creating a template this would be a comma separated list of parameter
names and data types.
Finally, the @hints parameter specifies any hints that need to be
applied. We apply the RECOMPILE hint, but notice that this query
already had a hint, MAX RECURSION. That hint had also to be part
of my @stmt in order to match what was inside the stored procedure.
The plan guide replaces the existing OPTION; so if, like in this case,
we need the existing OPTION to be carried forward, we need to add it
to the plan guide.
From this point forward, without making a single change to the actual
definition of the stored procedure, each execution of this procedure will
be followed by a recompile.
select @my_templatetext
SELECT @my_parameters
and
@0 varchar(8000)
Figure 9
Aside from the procedure to create plan guides, a second one exists,
sp_control_plan_guide, which allows you to drop, disable or enable a
specific plan guide, or drop, disable or enable all plan guides in the
database.
Simply run execute the sp_control_plan_guide procedure, changing
the @operation parameter appropriately.
Summary
With these three simple examples we've created all three types of plan
guides. Don't forget, these are meant to be tools of last resort. It's
almost always better to directly edit the stored procedures if you are
able to. These tools are primarily for work with third party products
where you can never directly modify the objects within the database but
you still need a tuning and control mechanism.
,soh.[OnlineOrderFlag]
FROM [Sales].[SalesOrderHeader] soh
WHERE soh.[SalesPersonID] = @SalesPersonID
When the procedure is run using the value for @SalesPersonID = 277,
a clustered index scan results, and the plan is quite costly.
Figure 10
If the value is changed to 288, an index seek with a bookmark lookup
occurs.
Figure 11
This is much faster than the clustered index scan. If the execution plan
for the procedure takes the first value for its plan, then the later values
still get the clustered index scan. While we could simply add a plan guide
that uses the OPTIMIZE FOR hint, we're going to try USE PLAN
instead.
First, we need to create an XML plan that behaves in the way we want.
We can do this by taking the SELECT criteria out of the stored
procedure and modifying it to behave in the correct way. This results in
the correct plan. In order to capture this plan, we'll wrap it with
STATISTICS XML, which will generate an actual execution plan in
XML:
,soh.[OnlineOrderFlag]
FROM [Sales].[SalesOrderHeader] soh
WHERE soh.[SalesPersonID] = 288;
GO
SET STATISTICS XML OFF
GO
This simple little query generates a 107 line XML plan, which I won't
replicate here. With the XML plan in hand, we'll create a plan guide to
apply it to the stored procedure (due to the size of the XML, I've left it
off the following statement):
EXEC sp_create_plan_guide
@name = N'UsePlanPlanGuide',
@stmt = N'SELECT soh.[AccountNumber]
,soh.[CreditCardApprovalCode]
,soh.[CreditCardID]
,soh.[OnlineOrderFlag]
FROM [Sales].[SalesOrderHeader] soh
WHERE soh.[SalesPersonID] = @SalesPersonID --288 --277',
@type = N'OBJECT',
@module_or_batch = N'Sales.uspGetCreditInfo',
@params = NULL,
@hints = N'OPTION(USE PLAN N''<ShowPlanXML…
Now, when the query is executed using the values that generate a bad
plan:
Figure 12
As a final reminder: using a plan guide, especially one that involves USE
PLAN, should be a final attempt at solving an otherwise unsolvable
problem. As the data and statistics change or new indexes are
236
implemented, plan guides can become outdated and the exact thing that
saved you so much processing time yesterday will be costing you more
and more tomorrow.
Summary
With many of the options discussed in this chapter, especially USE
PLAN, you're taking as much direct control over the optimizer and the
query engine as you can. Because of that, you're taking as much of a
risk with your queries as you can as well. It really is possible to mess up
your system badly using these last few tools. That said, need may arise
when you have that third party tool that is recompiling like crazy or a
procedure that you can't edit because it would require financial testing
and then having a tool like forced parameterization or plan guides can
save your system. Just remember to test thoroughly before you attempt
to apply any of these options to your production systems.
Index 237
INDEX
Actual Number of Rows, 146 Concatenation, 118, 119, 136,
137, 138
Adding a WHERE Clause, 73
Constant Scan, 48, 84, 183
algebrizer, 18, 19, 25
cost-based, 19
An Actual XML Plan, 104
CROSS APPLY, 112, 113, 226
An Estimated XML Plan, 100
Cursor Catchall, 175
Assert, 83, 85, 88
DBCC SHOW_STATISTICS,
Automating plans with SQL
127, 129
Server profiler, 17, 41, 42, 145
dbcreator, 27
Batch, 38, 102, 148
Defined Values, 34, 40, 94, 95
BatchSequence, 38, 102
Degree Of Parallelism, 105
Bookmark Lookup, 123
Delete Statements, 86
CachedPlanSize, 39, 102, 105, 221
Derived Tables, 108, 109, 112
Clustered Index
using APPLY, 112
Scan, 48, 49, 50, 71, 73, 76, 77,
108, 109, 115, 121, 128, 134, Distribute Streams, 48, 223
136, 146, 147, 150, 153, 155,
Eager Spool, 48, 86
158, 164, 165, 171, 205, 208,
216, 221 Estimated Cost, 34
Seek To Delete, 87 Estimated plan
Seeks, 48, 51, 52, 64, 67, 85, 87, invalid, 24
88, 94, 97, 103, 111, 114,
EstimateExecutions, 94, 95, 103
115, 165, 181, 208, 209
EstimateRows, 36, 39, 94, 95,
Update, 86, 183
103, 221, 222, 223
Clustered Index Insert, 85, 180
Events Extraction Settings, 44
ColumnReference, 39, 40, 104
Execution Plans
Common Table Expressions, 116
Actual Execution Plan, 21, 28,
COMPUTE, 134 126, 187, 188, 221
Compute Scalar, 48, 69, 84, 86, Estimated Execution Plan, 23,
147, 172, 179, 183, 187 28, 170, 184, 187
238
Gather Streams, 48, 223 Key LookUp, 54, 57, 95, 125
You can buy our acclaimed SQL Server tools individually or bundled.
Our most popular deal is the SQL Toolbelt: all twelve SQL Server tools in a single
installer, with a combined value of $5,240 but an actual price of $1,795, a saving
of more than 65%.
Ê Produce simple, legible and fast HTML Ê Visually track database object dependencies
reports for multiple databases Ê Discover all cross-database and cross-
Ê Documentation is stored as part of server object relationships
the database Ê Analyze potential impact of database
schema changes
Ê Output completed documentation to
Ê Rapidly document database
a range of different formats.
dependencies for reports, version
control, and database change planning
$295 $195
SQL Packager SQL Multi Script
Compress and package your databases Single-click script execution on multiple
for easy installations and upgrades SQL Servers
Ê Full API access to Red Gate Twelve tools to help update and maintain
comparison tools databases quickly and reliably, including:
Ê Incorporate comparison and Ê Rename object and update all references
synchronization functionality into Ê Expand column wildcards, qualify object
your applications names, and uppercase keywords
Ê Summarize script
Ê Schedule any of the tasks you require
from the SQL Comparison Bundle Ê Encapsulate code as stored procedure
$595 $295
How to Become an
Exceptional DBA
Brad McGehee
A career guide that will show you, step-
by-step, exactly what you can do to
differentiate yourself from the crowd so
that you can be an Exceptional DBA.
While Brad focuses on how to become an
Exceptional SQL Server DBA, the advice
in this book applies to any DBA, no matter
what database software they use. If you
are considering becoming a DBA, or are a
DBA and want to be more than an average
DBA, this is the book to get you started.
ISBN: 978-1-906434-05-2
Published: July 2008
ISBN: 978-1-906434-06-9
Published: September 2008
Mastering SQL Server Profiler
Brad McGehee
ISBN: 978-1-906434-16-8
Published: March 2009
ISBN: 978-1-906434-13-7
Planned for March 2009