SQL Suggested Indexes

SQL Suggested Indexes

How SQL access data

Whenever SQL Server executes a query, it builds an execution plan to find the data and return it to the requesting process. I have another post that explains what the plans do. One major part of the process, is to examine the Indexes it has over the data within the tables or views the query is running over.

Performance Problems

In tables with small numbers of rows (100s or 1000s), modern computers are so fast that often you don’t see that there is a problem. However, as the size of those tables grows, suddenly, out of the blue, queries that have been running without a problem grind along.

Of course, those queries were inefficient all along; you were protected from their inefficiencies by SQL Server. We should be testing our applications with expected data volumes for the moment we start our projects, and we should monitor those applications and databases to pre-emptively identify and fix such errors before they become a problem.

We all try to do these things -right?

What indexes are needed?

However, when we do find that our applications suddenly grind to a halt, the SQL below is something I run to identify which Indexes are missing: –

DECLARE @KeyFieldMatches INT
SET @KeyFieldMatches = 3 --Number of key fields to match in order
IF object_id('tempdb..#IndexList') IS NOT NULL BEGIN
    DROP TABLE #IndexList
END
IF object_id('tempdb..#IndexListShort') IS NOT NULL BEGIN
    DROP TABLE #IndexListShort
END
CREATE TABLE #IndexList (
      object_id    bigint not null
    , table_name varchar(150) not null
    , index_id int not null
    , index_name varchar(150) not null
    , index_column int not null
    , included bit not null
    , column_name varchar(150) not null
    , index_type int not null
)
CREATE CLUSTERED INDEX IndexList_Clu ON #IndexList (object_id, index_id, index_column, included)
CREATE TABLE #IndexListShort (
      object_id bigint not null
    , table_name varchar(150) not null
    , index_id int not null
    , index_name varchar(150) not null
    , column_names_first_n varchar(450) not null
    , column_names_key varchar(4000) not null
    , column_names_included varchar(4000) not null
)
DECLARE @object_id bigint
DECLARE @index_id int
DECLARE @List varchar(4000)
DECLARE @ListIncl varchar(4000)
DECLARE @ListShort varchar(450)
INSERT INTO #IndexList
SELECT o.object_id
    , table_name = o.name
    , i.index_id
    , index_name = i.name
    , index_column = ic.index_column_id
    , included = ic.is_included_column
    , column_name = c.name
    , index_type = i.type
FROM sys.objects o
    INNER JOIN sys.indexes i ON o.object_id = i.object_id
    INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
    INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
SET @object_id = (SELECT TOP 1 object_id FROM #IndexList)
SET @index_id = (SELECT TOP 1 index_id FROM #IndexList WHERE object_id = @object_id)
WHILE @object_id IS NOT NULL BEGIN
    SET @List = ''
    SET @ListIncl = ''
    SET @ListShort = ''
SELECT @List = @List + COALESCE(i.column_name + ', ', '') FROM #IndexList i WHERE i.object_id = @object_id and i.index_id = @index_id and i.included = 0 ORDER BY i.index_column
    SELECT @List = substring(@List, 0, Len(@List))
SELECT @ListIncl = @ListIncl + COALESCE(i.column_name + ', ', '') FROM #IndexList i WHERE i.object_id = @object_id and i.index_id = @index_id and i.included = 1 ORDER BY i.index_column
    SELECT @ListIncl = substring(@ListIncl, 0, Len(@ListIncl))
SELECT @ListShort = @ListShort + COALESCE(i.column_name + ', ', '') FROM #IndexList i WHERE i.object_id = @object_id and i.index_id = @index_id and i.included = 0 and index_column <= @KeyFieldMatches ORDER BY i.index_column
    SELECT @ListShort = substring(@ListShort, 0, Len(@ListShort))
INSERT INTO #IndexListShort 
    SELECT TOP 1 i.object_id
        , i.table_name
        , i.index_id
        , i.index_name
        , @ListShort
        , @List
        , CASE i.index_type --Clustered indexes include everything
            WHEN 1 THEN '*'
            ELSE @ListIncl
            END
    FROM #IndexList i
    WHERE i.object_id = @object_id and i.index_id = @index_id
DELETE #IndexList WHERE object_id = @object_id and index_id = @index_id
SET @object_id = (SELECT TOP 1 object_id FROM #IndexList)
    SET @index_id = (SELECT TOP 1 index_id FROM #IndexList WHERE object_id = @object_id)
END
SELECT table_name, index_name, column_names_key, column_names_included
FROM #IndexListShort i
WHERE EXISTS (SELECT * FROM #IndexListShort i2 WHERE i.object_id = i2.object_id AND i.column_names_first_n = i2.column_names_first_n GROUP BY object_id, column_names_first_n HAVING Count(*) > 1)
ORDER BY table_name, column_names_key
DROP TABLE #IndexList
DROP TABLE #IndexListShort

This produces a list of databases and tables where SQL Server believes an index should exist. It even gives you the TSQL commands to generate those missing indexes.

Of course, you must be careful not to create too many indexes within databases as each index must be maintained by SQL Server. The more you add, the slower insert, delete and update commands become.

I will create another post that shows how to check for unnecessary indexes!

Summary

Indexes are vital to ensure SQL databases perform well. Testing our applications and databases with the volume of data expected to go through them is important to guarantee their performance is within acceptable limits. Monitoring databases and creating, maintaining, and deleting unnecessary indexes should be something we do regularly – weekly isn’t too often – to ensure our applications run smoothly.

Stephen

Hi, my name is Stephen Finchett. I have been a software engineer for over 30 years and worked on complex, business critical, multi-user systems for all of my career. For the last 15 years, I have been concentrating on web based solutions using the Microsoft Stack including ASP.Net, C#, TypeScript, SQL Server and running everything at scale within Kubernetes.