Monitor all Indexes in SQL Server with PowerShell

Problem

Having a full overview of clustered and non-clustered indexes across all SQL Server instances is very important as it can give you a quick entry point when you are experiencing performance issues or just want to look for index issues from one central location.

Solution

This module will gather information about indexes from all specified SQL Server instances.  It will connect with each server in inventory.MasterServerList and capture the data for each of these instances.  The connection to each SQL Server instance will be based on the settings in the inventory.MasterServerList table.

By having this information, at a glance we can be constantly monitor the current status of indexes deployed in all databases across all SQL Server instances under our support. This can be a starting point in a decision-making process around performance caused by indexes or general proactive maintenance (i.e. addressing unused indexes in our environment).

Prerequisites

In order to use this module, you need to create the core objects found here.

Database Objects

For this particular module, only one table will be created (in case it doesn’t exist), and it is the table to store index information from each monitored instance.

If you want to add more fields to this table, make sure to adjust the structure within the PowerShell script and adapt the respective logic that will handle the additional columns.

  • inventory.Indexes
    • serverId – serverid ties back to inventory.MasterServerList
    • database – the name of the database in the instance
    • schema – the name of the schema housing the database in the instance
    • table – the name of the table that has the index structures
    • index – the name of the index
    • type – the type of index (i.e. Clustered, Non-Clustered)
    • allocation_unit_type – (i.e. IN_ROW_DATA, ROW_OVERFLOW_DATA, LOB_DATA)
    • fragmentation – percentage of fragmentation for the particular index
    • pages – the number of pages (8KB) that conform the index
    • writes – the number of writes that have taken place for the index
    • reads – the number of reads that have taken place for the index
    • disabled – if the index is currently disabled or not
    • stats_timestamp – the last time the statistics were updated

PowerShell Script

The PowerShell script that creates the above object and inserts data into the inventory.Indexes table is called:

  • Get-MSSQL-Instance-Indexes.ps1

The script has some validations that will help you check if some key elements are missing for the script to run successfully. For instance, it will confirm that the inventory.MasterServerList table exists and that it has at least 1 active instance registered to be able to have something to work with.

If you have followed along the other modules, you will notice that we have been storing all objects in "C:\temp", but you can use any folder you want.  If you make a change to the central folder location, you will need to edit the first line in the following PowerShell script to specify the new folder location.

How to Use

Navigate to the folder where you created the files and you can run the PowerShell script as follows:

Option 1

  • Right click on Get-MSSQL-Instance-Indexes.ps1 and select Run with PowerShell

Option 2

  • Open a command window and navigate to the folder where you saved the above files and run
powershell "C:\temp\Get-MSSQL-Instance-Indexes.ps1"

Option 3

  • Schedule this as a SQL Server Agent Job to run on a regular basis.

Option 4

  • Schedule this as a Windows Task Scheduler job to run on a regular basis.

Check Creation of Database and Objects

After running the PowerShell script, we can see the objects that are created.

database objects

If we query the inventory.Indexes table, we can see the data that has been collected.

Important note: the PowerShell script will store only the information from the very last execution. If you’d like to keep information from previous executions, you would have to modify the script and adapt it to your particular use case.

query results

Checking for Errors

To check for errors query the monitoring.ErrorLog table using the following query: 

SELECT *
FROM monitoring.ErrorLog
WHERE script = 'Get-MSSQL-Instance-Indexes'

If you’d like to know the SQL Server instance that got the errors, you would have to issue the query like this:

SELECT 
   CASE WHEN msl.instance = 'MSSQLSERVER' THEN msl.server_name ELSE 
   CONCAT(msl.server_name,'\',msl.instance) 
   END AS instance,
   e.script,
   e.message,
   e.error_timestamp
FROM monitoring.ErrorLog e
JOIN inventory.MasterServerList msl ON msl.serverId = e.serverId
WHERE e.script = 'Get-MSSQL-Instance-Indexes'

Useful Queries

By collecting all the data related to the execution of jobs across all your instances, you might answer things like:

Which indexes are very fragmented?

SELECT *
FROM inventory.Indexes
WHERE fragmentation >= 85;

See all indexes listed by page count (from largest to smallest)

SELECT *
FROM inventory.Indexes
ORDER BY pages DESC;

Which indexes are currently disabled?

SELECT *
FROM inventory.Indexes
WHERE disabled = 1;

Which indexes are not being used that frequently?

By knowing this, you could have a sense of those index structures that are not contributing in any way to the performance of the queries, and could be good candidates to be deleted.

SELECT *
FROM inventory.Indexes
WHERE writes > reads AND type <> 'CLUSTERED';

Which indexes have never had their statistics updated or last time was some time ago?

Replace X with the value you consider appropriate.

SELECT *
FROM inventory.Indexes
WHERE statistics_timestamp IS NULL OR DATEDIFF(DAY, statistics_timestamp, GETDATE()) > X ;

Download Scripts

Next Steps

4 Comments

  1. Two more things to consider. I ran into an issue of duplicate entries for some indexes. They all had an allocation_unit_type of either ‘LOB_DATA’ or ‘ROW_OVERFLOW_DATA’ I filtered those out of the results to avoid the errors. I also found that sp_MSForEachDB doesn’t exist in a couple of instances so you may want to switch to one of the alternatives, like maybe the one on Brent Ozar’s site:
    https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/blob/dev/sp_ineachdb.sql

  2. I found a couple of things I had to change to get the index script to work for me. 1) in the settings.ini I had to make the server_name use the same format as for my instances (i.e. server_name, port). That fixed the serverId issue. 2) In the Execute-Query fucntion I changed the -ErrorAction in the catch portion to Continue. Otherwise the script would stop at the first error and not process the other indexes. 3) In the Perform insert in the inventory.Indexes section, I changed the if($i -eq 500){ down to 1. Otherwise if the ran into an error executing the query, none of the records in that insert statement would get into the table. This way only the duplicates in my case were reported as errors while all the rest of the indexes were inserted as expected

  3. Hi Dennis!
    So for some reason the $serverId value is not set in your particular case. I find it a bit weird because the $instanceLookupQuery string includes it and if it exists in your own MasterServerList table, then when it is called like $instance.serverId, it should return a value and not NULL. If you’d like I can help you to troubleshoot this in a live call/session when we both can find a time that best suits our schedule… let me know.

  4. In order to get a connection to default instances, I’ve had to add the port number to the server_name column of the MasterServerList table. All other script work except these 2: Get-mssql-Instance-Indexes-v2.ps1 and Get-MSSQL-Instance-Top5CPU.ps1. They both give nearly identical errors:
    Fetching Top 5 CPU Queries information from instance MyServerName,1535\MyInstanceName
    Invoke-Sqlcmd : Cannot insert the value NULL into column ‘serverId’, table ‘DBA.monitoring.ErrorLog’; column does not allow nulls. INSERT fails.
    The statement has been terminated.
    Msg 515, Level 16, State 2, Procedure , Line 1.
    At C:\temp\Get-MSSQL-Instance-Top5CPU.ps1:48 char:13
    + Invoke-Sqlcmd -Query $errorQuery -Database $inventoryDB – …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [Invoke-Sqlcmd], SqlPowerShellSqlExecutionException
    + FullyQualifiedErrorId : SqlError,Microsoft.SqlServer.Management.PowerShell.GetScriptCommand

    It’s as if the $serverId variable isn’t set correctly. Any ideas as to how to fix this?

Leave a Reply

Your email address will not be published. Required fields are marked *