Showing posts with label Tuning. Show all posts
Showing posts with label Tuning. Show all posts

Thursday, 14 March 2013

Index Tuning

This week I've been carrying out some performance tuning of some of the more intensive procedures in one of our environments.  Using SQL Sentry Performance Advisor I highlighted four or five procedures to start with.  I was able to get a couple of quick wins which was a result, then analysing one of the execution plans I noticed a rather expensive index update.  A bit of digging using the help index scripts created by Kimberley Tripp of SQL Skills revealed the table in question had 24 indexes, one clustered index and 23 non clustered indexes, a little excessive considering the table has 29 columns don’t you think?  My first thought was that there must be some duplicate indexes, so I turned to yet more index scripts created by Kimberly Tripp, but to my surprise there were no duplicate indexes.  This got me thinking, indexes are great for reads but inherently bad for writes, there are no two ways about it, every index you create affects every insert, update and delete operation on the table.  There was no doubt in my mind that this number of indexes was impacting performance, it was crystal clear in the execution plan in the form of the red highlighted operator courtesy of SQL Sentry Plan Explorer (which is built in to Performance Advisor).


So what now?  I have identified the cause of the poorly performing procedure as being an index update, which in turn led to me finding 24 indexes on the table in question.  I can’t just blindly drop some of the indexes and hope the performance improves, what do I do?  I turn to my trusty DMV’s, when I say mine I mean mine, sure you have your DMV’s but the metadata they expose are for your environment not mine.  Right then too the DMV’s we turn, I have the below query which gets the index usage stats for all indexes on a particular table;

-- Set Database Context
USE master;
GO
-- Create table
IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   [object_id] = OBJECT_ID('dbo.IndexUsageStats')
                        AND [type] = 'U' )
    CREATE TABLE master.dbo.IndexUsageStats
        (
          IndexName VARCHAR(128) ,
          UserReads BIGINT ,
          UserWrites BIGINT
        );
ELSE
      -- If table exists truncate it before population
    TRUNCATE TABLE  master.dbo.IndexUsageStats;     
GO

-- Set database context
USE DatabaseName;
GO
-- Set isolation level
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
DECLARE @TableName AS NVARCHAR(128) = 'TableName';

-- Insert index usage data
INSERT  INTO master.dbo.IndexUsageStats
        SELECT  i.[name] AS [index_name] ,
                ddius.[user_seeks] + ddius.[user_scans] + ddius.[user_lookups] AS [user_reads] ,
                ddius.[user_updates] AS [user_writes]
        FROM    sys.dm_db_index_usage_stats ddius
                INNER JOIN sys.indexes i ON ddius.[object_id] = i.[object_id]
                                            AND i.[index_id] = ddius.[index_id]
                INNER JOIN sys.partitions SP ON ddius.[object_id] = SP.[object_id]
                                                AND SP.[index_id] = ddius.[index_id]
                INNER JOIN sys.objects o ON ddius.[object_id] = o.[object_id]
                INNER JOIN sys.sysusers su ON o.[schema_id] = su.[UID]
        WHERE   ddius.[database_id] = DB_ID() -- current database only
                AND ddius.[object_id] = OBJECT_ID(@TableName)
        GROUP BY su.[name] ,
                o.[name] ,
                i.[name] ,
                ddius.[user_seeks] + ddius.[user_scans] + ddius.[user_lookups] ,
                ddius.[user_updates]
        ORDER BY ddius.[user_seeks] + ddius.[user_scans]
                + ddius.[user_lookups] DESC;
GO

-- Return index Usage Stats
SELECT  'Index Usage Stats' AS ResultType,
            IUS.IndexName,
            IUS.UserReads,
            IUS.UserWrites
FROM    master.dbo.IndexUsageStats AS IUS;
GO

From this I was able to identify that 21 of the 24 indexes have been used to satisfy a query signified by the UserReads column, great that’s three indexes that are being updated for every INSERT, UPDATE and DELETE statement and are being maintained by expensive index reorganise and index rebuild operations but are never read from.  I script the drop and create of these three indexes and save the script file.  But we are not done yet, now this is the really cool part.  It is thanks to another SQL Skills member that I was able to achieve what I wanted to do next, interrogate the plan cache.  I came across a post from Jonathan Kehayias which shows how to find what queries in the plan cache used a specific index.  I tweaked this slightly as I wanted to find what queries in the plan cache accessed the table and which indexes they used to satisfy the query.  Below is the modified query;

-- Set Database Context
USE master;
GO
-- Create table
IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   [object_id] = OBJECT_ID('dbo.PlanCacheIndexes')
                        AND [type] = 'U' )
    CREATE TABLE master.dbo.PlanCacheIndexes
        (
          StatementText VARCHAR(4000) ,
          DatabaseName VARCHAR(128) ,
          SchemaName VARCHAR(128) ,
          TableName VARCHAR(128) ,
          IndexName VARCHAR(128) ,
          Indextype VARCHAR(128) ,
          QueryPlan XML ,
          UseCounts BIGINT
        );
ELSE
      -- If table exists truncate it before population
    TRUNCATE TABLE  master.dbo.PlanCacheIndexes;     
GO   

-- Set isolation level
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

-- Declare and set variable
DECLARE @TableName AS NVARCHAR(128) = 'TableName';

-- Make sure the name passed is appropriately quoted
IF ( LEFT(@TableName, 1) <> '['
     AND RIGHT(@TableName, 1) <> ']'
   )
    SET @TableName = QUOTENAME(@TableName);
-- Handle the case where the left or right was quoted manually but not the opposite side
IF LEFT(@TableName, 1) <> '['
    SET @TableName = '[' + @TableName;
IF RIGHT(@TableName, 1) <> ']'
    SET @TableName = @TableName + ']';

-- Dig into the plan cache and find all plans using Indexes
WITH XMLNAMESPACES
   (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')   
INSERT INTO master.dbo.PlanCacheIndexes
SELECT
stmt.value('(@StatementText)[1]', 'varchar(max)') AS SQL_Text,
obj.value('(@Database)[1]', 'varchar(128)') AS DatabaseName,
obj.value('(@Schema)[1]', 'varchar(128)') AS SchemaName,
obj.value('(@Table)[1]', 'varchar(128)') AS TableName,
obj.value('(@Index)[1]', 'varchar(128)') AS IndexName,
obj.value('(@IndexKind)[1]', 'varchar(128)') AS IndexKind,
qp.query_plan,
cp.usecounts
FROM sys.dm_exec_cached_plans AS cp
CROSS APPLY sys.dm_exec_query_plan(plan_handle) AS qp
CROSS APPLY query_plan.nodes('/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS batch(stmt)
CROSS APPLY stmt.nodes('.//IndexScan/Object[@Table=sql:variable("@TableName")]') AS TableName(obj)
OPTION(MAXDOP 1, RECOMPILE);
GO

-- Return plan information
SELECT  'Plan Cache Indexes' AS ResultType,
            REPLACE(REPLACE(IndexName, '[', ''), ']', '') AS IndexName ,
        IndexType ,
        COUNT(IndexName) AS IndexUsageInstances ,
        SUM(UseCounts) AS TimesIndexUsed
FROM    master.dbo.PlanCacheIndexes
GROUP BY IndexName ,
        IndexType
ORDER BY SUM(UseCounts) DESC;
GO

NOTE – As Jonathan states in his post the OPTION(MAXDOP 1, RECOMPILE) is advisable as this query can be particularly intensive.

This showed me that in the plan cache there are 18 indexes that have been used to satisfy a query against the table so that gives us another 3 indexes we can potentially get rid of.  I ran the below query to show me the index usage stats for the indexes that are not in the plan cache.

-- Set Database Context
USE master;
GO
-- Return Index usage stats for indexes that are not used in the plan cache
SELECT  'Index Usage Not In Plan Cache' AS ResultType,
            IUS.IndexName,
            IUS.UserReads,
            IUS.UserWrites
FROM    master.dbo.IndexUsageStats AS IUS
WHERE   IUS.IndexName NOT IN (
        SELECT  REPLACE(REPLACE(IndexName, '[', ''), ']', '')
        FROM    master.dbo.PlanCacheIndexes );
GO

I script the drop and create of these 3 indexes again saving the script file.  What is important to point out here is that all three of these indexes have still been used to satisfy a query since the index usage stats where last reset.  You can use the below query to find out that last time the index stats where reset;

-- Set Database Context
USE master;
GO
-- How old are the index usage stats?
SELECT  'Index Stats Age' AS ResultType,
            DATEDIFF(DAY, sd.crdate, GETDATE()) AS days_history
FROM    sys.sysdatabases sd
WHERE   sd.[name] = 'tempdb';
GO

Take a note of the percentage of reads to writes, if this is low, which I expect it will be (in my case less than 0.02%) then chances are there was a one off query maybe for a report or extract.

So with this approach I identified 6 indexes, 3 that haven’t been used and 3 that have been used but do not have a query in the plan cache.  What you do next will depend on your environment, for me it meant running the drop index statements in a test environment and running a workload to prove that dropping the indexes improves the procedure and does not cause any unexpected performance issues.  Then I had to raise a change request to schedule in remove the indexes from production which I am pleased to say was approved and will be being run tonight outside of core business hours.

I am still not comfortable that the remaining 18 indexes are the best fit for our workload so have created a task for myself to review the indexes in place on this table and make improvements where possible.  I will post my results in a future post.

Enjoy!

Chris

Tuesday, 26 February 2013

Cost Threshold for Parallelism


Parallelism, where to start?  Well let’s start right at the beginning shall we with the question, what is parallelism?  Simply put it is a feature of SQL Server which allows queries to utilize more threads in order to complete quicker.  There are several ways to influence SQL Servers decision whether to use parallelism or not and if so how “much” which is referred to as the degree of parallelism.

Cost Threshold For Parallelism
This is a server wide setting which indicates what the minimum query cost has to be before Parallelism is considered by the optimizer (yes UK readers I spelt optimizer with a Z not an S).

Maximum Degree Of Parallelism
This is a server wide setting which indicates the maximum number of processors a query run in parallel can use.

OPTION (MAXDOP X)
This is a query hint which contrary to some beliefs will override the above two settings by reducing the degree of parallelism for queries greater than the cost threshold, but will not make a too small query run in parallel (Thanks to Brent Ozar for pointing this out).

So what settings should you be using, well as always “it depends”, sorry but this really isn’t a one size fits all solution.  Servers will have different workloads, number of cores, some will have hard NUMA some will have Soft NUMA some older servers won’t have NUMA at all.  What you really need to do is understand the impact parallelism has on your environment, today I will look specifically at the sp_configure option ‘cost threshold for parallelism‘.

SQL Server exposes an increasing wealth of information via DMV’s that allow us to make more informed decisions; one idea that I loved was using these DMV’s specifically sys.dm_exec_cached_plans and sys.dm_exec_query_plan to return information from the Plan Cache for queries run in parallel. This idea I took from Jonathan Kehayias and adapted to group the exposed information by StatementSubTreeCost.  This will give you the spread of query costs which you can use to make an educated decision as to if the default cost threshold for parallelism value of 5 is in fact the best for your environment.

/*
      -----------------------------------------------------------------
      Cost Threshold For Parallelism
      -----------------------------------------------------------------
   
      For more SQL resources, check out SQLServer365.blogspot.com

      -----------------------------------------------------------------

      You may alter this code for your own purposes.
      You may republish altered code as long as you give due credit.
      You must obtain prior permission before blogging this code.

      THIS CODE AND INFORMATION ARE PROVIDED "AS IS"
    
      -----------------------------------------------------------------
*/
-- Set Database Context
USE master;
GO
-- Create table
IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   [object_id] = OBJECT_ID('dbo.PlanCacheForMaxDop')
                        AND [type] = 'U' )
    CREATE TABLE master.dbo.PlanCacheForMaxDop
        (
          CompleteQueryPlan XML ,
          StatementText VARCHAR(4000) ,
          StatementOptimizationLevel VARCHAR(25) ,
          StatementSubTreeCost FLOAT ,
          ParallelSubTreeXML XML ,
          UseCounts INT ,
          PlanSizeInBytes INT
        );
ELSE
      -- If table exists truncate it before population
    TRUNCATE TABLE  master.dbo.PlanCacheForMaxDop;     
GO   

-- Collect parallel plan information
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
WITH XMLNAMESPACES  
(DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') 
INSERT INTO master.dbo.PlanCacheForMaxDop
SELECT 
     query_plan AS CompleteQueryPlan,
     n.value('(@StatementText)[1]', 'VARCHAR(4000)') AS StatementText,
     n.value('(@StatementOptmLevel)[1]', 'VARCHAR(25)') AS StatementOptimizationLevel,
     n.value('(@StatementSubTreeCost)[1]', 'VARCHAR(128)') AS StatementSubTreeCost,
     n.query('.') AS ParallelSubTreeXML, 
     ecp.usecounts,
     ecp.size_in_bytes 
FROM sys.dm_exec_cached_plans AS ecp
CROSS APPLY sys.dm_exec_query_plan(plan_handle) AS eqp
CROSS APPLY query_plan.nodes('/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS qn(n)
WHERE  n.query('.').exist('//RelOp[@PhysicalOp="Parallelism"]') = 1;
GO

-- Return parallel plan information
SELECT  CompleteQueryPlan ,
        StatementText ,
        StatementOptimizationLevel ,
        StatementSubTreeCost ,
        ParallelSubTreeXML ,
        UseCounts ,
        PlanSizeInBytes
FROM    master.dbo.PlanCacheForMaxDop;
GO

-- Return grouped parallel plan information
SELECT  MAX(CASE WHEN StatementSubTreeCost BETWEEN 1 AND 5 THEN '1-5'
                 WHEN StatementSubTreeCost BETWEEN 5 AND 6 THEN '5-6'
                 WHEN StatementSubTreeCost BETWEEN 6 AND 7 THEN '6-7'
                 WHEN StatementSubTreeCost BETWEEN 7 AND 8 THEN '7-8'
                 WHEN StatementSubTreeCost BETWEEN 8 AND 9 THEN '8-9'
                 WHEN StatementSubTreeCost BETWEEN 9 AND 10 THEN '9-10'
                 WHEN StatementSubTreeCost BETWEEN 10 AND 11 THEN '10-11'
                 WHEN StatementSubTreeCost BETWEEN 11 AND 12 THEN '11-12'
                 WHEN StatementSubTreeCost BETWEEN 12 AND 13 THEN '12-13'
                 WHEN StatementSubTreeCost BETWEEN 13 AND 14 THEN '13-14'
                 WHEN StatementSubTreeCost BETWEEN 14 AND 15 THEN '14-15'
                 WHEN StatementSubTreeCost BETWEEN 15 AND 16 THEN '15-16'
                 WHEN StatementSubTreeCost BETWEEN 16 AND 17 THEN '16-17'
                 WHEN StatementSubTreeCost BETWEEN 17 AND 18 THEN '17-18'
                 WHEN StatementSubTreeCost BETWEEN 18 AND 19 THEN '18-19'
                 WHEN StatementSubTreeCost BETWEEN 19 AND 20 THEN '19-20'
                 WHEN StatementSubTreeCost BETWEEN 20 AND 25 THEN '20-25'
                 WHEN StatementSubTreeCost BETWEEN 25 AND 30 THEN '25-30'
                 WHEN StatementSubTreeCost BETWEEN 30 AND 35 THEN '30-35'
                 WHEN StatementSubTreeCost BETWEEN 35 AND 40 THEN '35-40'
                 WHEN StatementSubTreeCost BETWEEN 40 AND 45 THEN '40-45'
                 WHEN StatementSubTreeCost BETWEEN 45 AND 50 THEN '45-50'
                 WHEN StatementSubTreeCost > 50 THEN '>50'
                 ELSE CAST(StatementSubTreeCost AS VARCHAR(100))
            END) AS StatementSubTreeCost ,
        COUNT(*) AS countInstance
FROM    master.dbo.PlanCacheForMaxDop
GROUP BY CASE WHEN StatementSubTreeCost BETWEEN 1 AND 5 THEN 2.5
              WHEN StatementSubTreeCost BETWEEN 5 AND 6 THEN 5.5
              WHEN StatementSubTreeCost BETWEEN 6 AND 7 THEN 6.5
              WHEN StatementSubTreeCost BETWEEN 7 AND 8 THEN 7.5
              WHEN StatementSubTreeCost BETWEEN 8 AND 9 THEN 8.5
              WHEN StatementSubTreeCost BETWEEN 9 AND 10 THEN 9.5
              WHEN StatementSubTreeCost BETWEEN 10 AND 11 THEN 10.5
              WHEN StatementSubTreeCost BETWEEN 11 AND 12 THEN 11.5
              WHEN StatementSubTreeCost BETWEEN 12 AND 13 THEN 12.5
              WHEN StatementSubTreeCost BETWEEN 13 AND 14 THEN 13.5
              WHEN StatementSubTreeCost BETWEEN 14 AND 15 THEN 14.5
              WHEN StatementSubTreeCost BETWEEN 15 AND 16 THEN 15.5
              WHEN StatementSubTreeCost BETWEEN 16 AND 17 THEN 16.5
              WHEN StatementSubTreeCost BETWEEN 17 AND 18 THEN 17.5
              WHEN StatementSubTreeCost BETWEEN 18 AND 19 THEN 18.5
              WHEN StatementSubTreeCost BETWEEN 19 AND 20 THEN 19.5
              WHEN StatementSubTreeCost BETWEEN 10 AND 15 THEN 12.5
              WHEN StatementSubTreeCost BETWEEN 15 AND 20 THEN 17.5
              WHEN StatementSubTreeCost BETWEEN 20 AND 25 THEN 22.5
              WHEN StatementSubTreeCost BETWEEN 25 AND 30 THEN 27.5
              WHEN StatementSubTreeCost BETWEEN 30 AND 35 THEN 32.5
              WHEN StatementSubTreeCost BETWEEN 35 AND 40 THEN 37.5
              WHEN StatementSubTreeCost BETWEEN 40 AND 45 THEN 42.5
              WHEN StatementSubTreeCost BETWEEN 45 AND 50 THEN 47.5
              WHEN StatementSubTreeCost > 50 THEN 100
              ELSE StatementSubTreeCost
         END;
GO

Enjoy!

Chris