SQL Server Index Tuning Tip: Identify Overlaps
If you’ve got performance troubles with an application that stores data in SQL Server, and especially if it’s a home-grown application (not a store-bought app), you can get dramatic performance improvements simply by focusing on some basic indexing techniques. These tips and tricks pay off more than pouring money into hardware that might look good sitting in the datacenter, but doesn’t really make the application significantly faster.
When I go into a shop to speed up an application I’ve never seen before, two of my favorite quick-hits are from the index performance tuning queries from SQLServerPedia:
- Find unused indexes – these are indexes the SQL Server engine says it’s not using. Unused indexes incur a speed penalty because SQL Server still has to add/update the indexes as records change, so they make writes slower.
- Find missing indexes – these are indexes SQL Server wishes it had available.
I’m not going to cover those in detail this week because I’ve already recorded tutorial videos over at SQLServerPedia for those, but I do want to focus on something these queries won’t pick up. Sometimes a table has two nearly-identical indexes, and they’re both being used for reads. Take these two:
CREATE NONCLUSTERED INDEX [IX_RunID_SiteID_DataSource_OutputType_PeriodType] ON [dbo].[MyTable] ([RunID] ASC, [SiteID] ASC, [DataSource] ASC, [OutputType] ASC, [PeriodType] ASC ) CREATE NONCLUSTERED INDEX [IX_RunID_SiteID_DataSource_OutputType_PeriodType_QuotaItemDriverID] ON [dbo].[MyTable] ([RunID] ASC, [SiteID] ASC, [DataSource] ASC, [OutputType] ASC, [PeriodType] ASC, [QuotaItemDriverID] ASC, )
They’re two different indexes, and they’re both getting used – but does that mean we need them both?
They’re very nearly identical – but the second index has one extra field. When the SQL Server engine gets a query that needs RunID, SiteID, DataSource, OutputType, and PeriodType – but not QuotaItemDriverID – then it will use the first index. When it gets a query that needs all six fields, then it’ll use the second index.
In cases like this, I prefer to drop that first index and let the slightly bigger index pick up the slack. Reading a slightly larger index will take slightly more time: if a query didn’t need that QuotaItemDriverID field, it still has to pull it off the disk in order to perform the query. However, dropping the index pays off during inserts/updates/deletes, because it’s one less index SQL Server has to manage. It also makes the database smaller, thereby making database maintenance tasks smaller/faster.
If:
- I have two indexes with the exact same fields in the same order, but
- One has 1-2 extra fields, and
- There aren’t include fields, or the include fields are the same
Then I’ll drop the shorter index with extreme prejudice.
When Indexes Have Include Fields
If they have “include” fields, then I’ll merge the include fields between the two indexes to make one index to serve both needs. Say we have these two indexes:
CREATE NONCLUSTERED INDEX [IX_RunID_SiteID_DataSource_OutputType_PeriodType_Includes] ON [dbo].[MyTable] ([RunID] ASC, [SiteID] ASC, [DataSource] ASC, [OutputType] ASC, [PeriodType] ASC ) INCLUDE ( [YTDRevenue], [MTDRevenue] ) CREATE NONCLUSTERED INDEX [IX_RunID_SiteID_DataSource_OutputType_PeriodType_QuotaItemDriverID_Includes] ON [dbo].[MyTable]([RunID] ASC, [SiteID] ASC, [DataSource] ASC, [OutputType] ASC, [PeriodType] ASC, [QuotaItemDriverID] ASC, ) INCLUDE ( [SalespersonID], [MTDRevenue] )
The first index includes the YTDRevenue field, but the second index doesn’t. If I just drop the first index, then queries that needed that field won’t get the full speed benefits from the second index. To merge the two indexes, I need to drop both indexes and recreate the second one with the YTDRevenue field included, like this:
CREATE NONCLUSTERED INDEX [IX_RunID_SiteID_DataSource_OutputType_PeriodType_QuotaItemDriverID_Includes] ON [dbo].[MyTable]([RunID] ASC, [SiteID] ASC, [DataSource] ASC, [OutputType] ASC, [PeriodType] ASC, [QuotaItemDriverID] ASC, ) INCLUDE ( [SalespersonID], [MTDRevenue], [YTDRevenue] )
In this example, I tacked the YTDRevenue field on to the end of the include field list. The order of the included fields doesn’t matter, since SQL Server doesn’t sort by those.
Things to Watch Out For
In my examples, I kept things simple by omitting all of the extra indexing options like partitioning and sorting in TempDB. When doing index tuning in real life, though, you’ll want to check those options to make sure they’re consistent from index to index.
Field order matters in indexes; if two indexes have the same fields but in different order, that doesn’t mean you can drop one of them.
Ideally, after making index changes, we would restart the SQL Server instance to reset the DMV counters that monitor index use. In reality, though, that’s not so easy to pull off, so we need to log our changes to understand what the changes have been. After making index changes, log the changes somewhere. I keep the output of the index performance tuning DMV queries in Excel spreadsheets because it’s easier to email those back and forth from machine to machine, especially when I’m consulting. The next time you do index performance tuning on the same database, you can use the historical spreadsheets to determine whether or not your changes worked the way you’d planned.


