The Estimated or Actual Execution Plan provided by SQL Server gives a nice graphical overview, but can be hard to read if you have too many objects and you will also need to point your cursor on each object for further information. There is also an option to extract the Execution Plan in XML, but that requires additonal steps to make it readable. In this tutorial I will show an alternative way with SQL Server Profiler that will provide detailed execution plan information in text form that can be easily reused for further analysis.
Once you've open SQL Server Profiler click on the File tab and then on New Trace.
SQL Server Profiler returns massive information. To narrow down the results and to focus on our main goal, some settings are essential. In the Trace properties click on the Show all events tab and expand the Performance section to capture the Showplan data.
Under Performacnce you will see all types of Showplan information. The one most relevant for our excercise here is Showplan All.
To narrow down the results even more, click on Column Filters in the Event Selections Tab. Depending on column and operator you can basically filter on any available column with an exact serch term or use wildcards. I will filter on my login to avoid getting system admin records returned.
Meanwhile I executed my query from SSMS and SQL Server Profiler immediately returned following records:
Showplan All returns the execution plan and in addition detailed information such as what type of join operator is used, CPU costs, nr of executions etc. Simply mark the relevant text and right click copy.
You can save this info as a txt file or simply paste it in Excel. Once you have it in the right format, the whole thing is very easy to read and you can sort descending by the most expensive objects to lead you straight to the root cause.
In this tutorial, I will describe the 3 Join Operators SQL Server uses and the reason why it comes to the individual selections by the Optimizer.
I will focus on the Inner Join and will stick to using 2 tables. We basically have 3 scenarios:
1) None of the tables have an index on the joining column
2) One of the two tables has an index on the joining column
3) Both columns have an index on the joining column
In the samples below I will use 2 tables from the AdventureWorks Database and modify indexes to demonstrate why the Optimizer decides to vary on the join operator. Following 2 tables will be used: Sales.SalesOrderHeader
Sales.SalesTerritory
Following query will be used throughout all samples:
SELECT so.SalesOrderID ,st.Name ,so.OrderDate ,so.DueDate ,so.ShipDate ,so.SalesOrderNumber ,so.PurchaseOrderNumber ,so.AccountNumber ,so.CurrencyRateID ,so.TotalDue FROM Sales.SalesOrderHeader so INNER JOIN Sales.SalesTerritory st ON so.TerritoryID = st.TerritoryID
NESTED LOOP JOIN
Tablename
Row Count
Indexed Column
Sales.SalesOrderHeader
31465
Clustered Index on
joining column
Sales.SalesTerritory
10
No index
Nested Loop Join will consist out of inner (lower object in Execution Plan) and outer (upper object in Execution Plan) inputs.
The outer input gets scanned row by row and an index seek is processed on the inner table (the larger indexed table).
This is exactly the advantage of Nested Loop Joins compared to Hash and Merge Joins. The outer, smaller input, is placed in memory and is compared with an indexed outer input.
Let's have a closer look at the Execution plan by placing the cursor above the object in question. The Clustered Index Scan shows us that there was 1 Execution returning 10 rows (all rows in that table).
The Clustered Index Seek provides information that 10 Executions took place returning 31465 rows. Therefor keep in mind that Nested Loop Joins will always work well with a small outer input and a indexed inner input.
MERGE JOIN
Tablename
Row Count
Indexed Column
Sales.SalesOrderHeader
31465
Clustered Index on
joining column
Sales.SalesTerritory
10
Clustered Index on
joining column
The requirements for a Merge Join is that both tables are sorted on the joining column. Assuming you have an index on both columns, then the sort is already covered here.
If you force a Merge Join by passing a hint and an index is missing on one or both of the columns, the Sort Operator will be used here, which is quite expensive due to high memory and I/O resources.
Merge Join can be very fast when columns are ordered by the index. In this case matching rows are created while the sorted columns are compared for equality.
The big advantage with Merge Joins is that both outputs will be only executed once.
Although I have an index on both tables, the Optimizer will probably still go for a Nested Loop Join here, since the outer input is quite small. Merge Joins are perfect for indexed and larger tables.
HASH JOIN
Tablename
Row Count
Indexed Column
Sales.SalesOrderHeader
31465
No index
Sales.SalesTerritory
10
No Index
Hash Joins are usually selected when there is no other option, meaning the data is not sorted and nonindexed. A Hash Join consists of 2 inputs.
1) Build Input: the upper object in the execution plan and usually the smaller table since it is saved on the system and to keep used memory low
2) Probe Input: the lower object in the execution plan
Hash joins are used by the Optimizer to process large and unsorted as well nonindexed values.
In most cases the Optimizer will make the right choice, but in cases where statistics are not up to date, it may be necessary to enforce the right option.
You can do this, by either adding a OPTION (x JOIN) at the end of the query
In this
blog entry, I would like to describe how to reorganize or rebuild indexes.
First of
all, SQL server stores row or index data on 8KB sized memory Pages. For each
table (no matter how big), a own page is created. If the table exceeds a
certain size due to manipulation of data (e.g. new row inserts), the page will
be extended („Page Split“) to a second page etc.
The new
page will also cause the creation of a new page in the upper level with the
index tree, which may also lead to a page split.
Page Spilts
are time consuming, cause fragmentation and index pages aren’t phyically
ordered as supposed. All this results to a decrease of performance.
Let's have a brief look on the architecture of indexes in SQL Server.
In a table
without a clustered index, the data will be stored within the page unsorted
(„Heap“).
Indexes
have a root page, which is the starting point. The tree is split between
non-leaf (root and intermediate leafs) and leaf-levels.
In a
clustered index, the leaf level is the actual data page. This means the data is
stored here in ordered form.
The most significant difference in the
index architecture between clustered and non-clustered, is that the leaf level in
non-clustered indexes contain key values and not the actual data.
In case of a
heap, there is a row
locator in the leaf node pointing to the correct data page.
A
non-clustered index in a table that has a clustered index, will refer to the
clustered index for pointing the row location.
Disadvantage here is that
non-clustered indexes will have to be rebuild if the
clustered index is rebuild or dropped. An advantage here is that a Reorganize
of a clustered index, will mean that the non-clustered index does not have to
be reorganized.
To solve the above listed issues concerning performance, indexes need to
be REORGANIZEd or REBUILD.
The level of fragmentation can be analyzed with the help of system function sys.dm_db_index_physical_stats
Here is a more precise query: SELECT sch.name + ' ' + tab.name AS 'Table', idx.name AS 'Index', stat.avg_fragmentation_in_percent, stat.page_count, idx.type_desc AS IndexType FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS stat INNER JOIN sys.tables AS tab ON tab.object_id = stat.object_id INNER JOIN sys.schemas AS sch ON tab.schema_id = sch.schema_id INNER JOIN sys.indexes AS idx ON idx.object_id = stat.object_id AND stat.index_id = idx.index_id ORDER BY stat.avg_fragmentation_in_percent DESC If the average fragmention is between 5 and 30 %, a REORGANIZE is fine, if it's above 30% REBUILD is advised.
Following T-SQL syntax is required for
REORGANIZE:
ALTER INDEX indexname
ON tablename REORGANIZE
In case of
a rebuild, there is an option for a fill factor. This means the page will be
filled with the percentage value defined. The rest is left open to avoid a fast
fragmentation.
ALTER INDEX indexname
REBUILD WITH (FILLFACTOR = 80)
The fillfactor by default will only relate
to the leaf level of the tree. If you want to also cover the other pages
PAD_INDEX = ON will do the job.
ALTER INDEX indexname REBUILD WITH (FILLFACTOR = 80, PAD_INDEX =
ON)