Next, you will take a look at the most
expensive statements within your cached stored procedures for average
I/O, using the query shown in Listing 44.
LISTING 44: Top statements by average I/O
-- Lists the top statements by average input/output
-- usage for the current database
SELECT TOP(50) OBJECT_NAME(qt.objectid) AS [SP Name],
(qs.total_logical_reads + qs.total_logical_writes) /qs.execution_count
AS [Avg IO],SUBSTRING(qt.[text],qs.statement_start_offset/2,
(CASE
WHEN qs.statement_end_offset = -1
THEN LEN(CONVERT(nvarchar(max), qt.[text])) * 2
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2) AS [Query Text]
FROM sys.dm_exec_query_stats AS qs WITH (NOLOCK)
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS qt
WHERE qt.[dbid] = DB_ID()
ORDER BY [Avg IO] DESC OPTION (RECOMPILE);
-- Helps you find the most expensive statements for I/O by SP
This query identifies the most expensive cached
statements for I/O, ordered by average I/O. If your system is showing
any signs of I/O pressure, you should definitely take a look at the
results of this query. Even if you are not seeing I/O pressure, it
never hurts to be aware of which statements within your stored
procedures are causing the most I/O pain.
Next, using the query shown in Listing 45, you will look for nonclustered indexes that have more writes than reads.
LISTING 45: Possible bad nonclustered indexes
-- Possible Bad NC Indexes (writes > reads)
SELECT OBJECT_NAME(s.[object_id]) AS [Table Name], i.name AS [Index Name],
i.index_id,user_updates AS [Total Writes],
user_seeks + user_scans + user_lookups AS [Total Reads],
user_updates - (user_seeks + user_scans + user_lookups) AS [Difference]
FROM sys.dm_db_index_usage_stats AS s WITH (NOLOCK)
INNER JOIN sys.indexes AS i WITH (NOLOCK)
ON s.[object_id] = i.[object_id]
AND i.index_id= s.index_id
WHERE OBJECTPROPERTY(s.[object_id],'IsUserTable') = 1
AND s.database_id= DB_ID()
AND user_updates > (user_seeks + user_scans + user_lookups)
AND i.index_id > 1
ORDER BY [Difference] DESC, [Total Writes] DESC, [Total Reads] ASC OPTION
(RECOMPILE);
-- Look for indexes with high numbers of writes
-- and zero or very low numbers of reads
-- Consider your complete workload
-- Investigate further before dropping an index!
This query returns all nonclustered indexes in
the current database, along with their total writes and total reads
ordered by the difference between the number of writes and the number
of reads. The idea here is to find indexes that have a lot of writes
and very few (or zero) reads. An index that is only written to, but
never used for reads, is not useful at all. You are paying the cost to
maintain the index, but you are receiving no benefit. Having many
“unused” indexes on a table hurts your insert/update/delete
performance, and it makes your table and database need more space in
the data file(s). It also makes backups and restores take longer to
complete.
Keep in mind that these read and write statistics
reflect only the period since this instance of SQL Server has been
running. Depending on how long your instance has been running, you may
not have seen your complete workload yet. For example, some indexes may
be used only for monthly reporting queries, meaning they might have a
lot more writes than reads during the rest of the month. If you dropped
an index like that based on the results of this query, you could cause
some serious performance issues when it comes time to run those
reporting queries.
In other words, use some caution and common sense
before you start dropping indexes solely based on the results of this
query. You should always do some further investigation and analysis
before you drop an index on an important database.
Next, using the query shown in Listing 46, you will look for indexes that SQL Server thinks you would benefit from adding to this database.
LISTING 46: Missing indexes by index advantage
-- Missing Indexes current database by Index Advantage
SELECT user_seeks * avg_total_user_cost * (avg_user_impact * 0.01)
AS [index_advantage],
migs.last_user_seek, mid.[statement] AS [Database.Schema.Table],
mid.equality_columns, mid.inequality_columns, mid.included_columns,
migs.unique_compiles, migs.user_seeks, migs.avg_total_user_cost,
migs.avg_user_impact
FROM sys.dm_db_missing_index_group_stats AS migs WITH (NOLOCK)
INNER JOIN sys.dm_db_missing_index_groups AS mig WITH (NOLOCK)
ON migs.group_handle = mig.index_group_handle
INNER JOIN sys.dm_db_missing_index_details AS mid WITH (NOLOCK)
ON mig.index_handle = mid.index_handle
WHERE mid.database_id= DB_ID() -- Remove this to see for entire instance
ORDER BY index_advantage DESC OPTION (RECOMPILE);
-- Look at last user seek time, number of user seeks
-- to help determine source and importance
-- SQL Server is overly eager to add included columns, so beware
-- Do not just blindly add indexes that show up from this query!!!
This query shows you what SQL Server considers to be “missing indexes” ordered by a calculated column called index_advantage.
The idea here is that anytime the SQL Server query optimizer determines
that a particular index not present in the database would help reduce
the cost of a query, it will note that fact. Over time, as your
workload runs on your database server, you will likely see a growing
number of proposed new indexes returned when you run this query. I
strongly caution you to not get overly enthusiastic about creating new
indexes based solely on the results of this query. Many people have
proudly told me that they wrote a script that automatically creates
every single index that SQL Server identifies in this query, which is a
huge mistake!
Instead, you should consider a number of factors
before you start adding new indexes to your tables. First, consider
what type of workload you have and how volatile your table is. If you
have an OLTP type of workload, with a lot of writes to your table, you
should be much more hesitant about adding new indexes, as more indexes
will slow down your insert/update/delete performance on that table.
Second, you should look at the last_user_seek column to get an idea of whether this “missing” index would really affect your normal workload. If your last_user_seek
is from a few seconds or a few minutes ago, it is more likely to be
part of your normal workload. If it is from a few days or a few weeks
ago, it is more likely to be from an ad hoc query or a reporting query,
and I would be much less inclined to add that index. You should also
look at the user_seeks column to get an idea of how many times SQL Server has determined it would need this index, along with the avg_user_impact and avg_total_user_cost columns to help assess how important the index really might be.
You should also consider your existing
indexes on the table in question. In many cases this query will
recommend a new index that is almost an exact duplicate of an existing
index. You need to apply some judgment and common sense and consider
your complete workload before you start adding new indexes based solely
on the results of this query. Finally, you should be aware that if you
make any kind of index change on a particular table, the missing index
statistics for that table will be cleared out, and it will take some
time (as your workload is running) for the missing index statistics to
show any information for that table. To understand how this could bite
you, suppose you had a table that needed three new indexes to help an
important part of your normal workload. After a thorough analysis, you
decide to add the first index. After that index is added, you run this
query again, and no results are returned for the table in question.
This might lead you to conclude that SQL Server does not need the other
two indexes, which would probably be incorrect. You just need to wait
for a period of time, depending on your workload, to see whether SQL
Server really needs any more indexes on the table. You can use the
query shown in Listing 45 along with this query to help zero in on which indexes are really needed on each table.