Click here to monitor SSC
  • Av rating:
  • Total votes: 421
  • Total comments: 11
Joe Webb

Using Covering Indexes to Improve Query Performance

29 September 2008

Designers of database systems will often assume that the use of a clustered index is always the best approach. However the nonclustered Covering index will usually provide the optimum performance of a query.


There are many ways to measure a database’s performance. We can look at its overall availability; that is: are our users able to get the information they need when they need it? We can consider its concurrency; how many users can query the database at one time? We can even watch the number of transactions that occur per second to gauge its level of activity.

Often for our users however, it comes down to one very simple, but somewhat nebulous, measurement – perceived responsiveness. Or put another way, when a user issues a query does he receive his results without having to wait too long?

Of course, there are many factors that influence this. There is the hardware itself and how its configured. There is the physical implementation of the database; the logical design of the database; and even how the queries themselves are written.

In this article however, I will focus on one specific technique that can be used to improve performance – creating covering indexes.

An indexing refresher

Before taking on the challenge of covering indexes, let’s take a moment for a brief but very relevant review of index types and terminology.

Indexes allow SQL Server to quickly find rows in a table based on key values, much like the index of a book allows us to easily find the pages that contain the information we want. There are two types of indexes in SQL Server, clustered and nonclustered indexes.

Clustered Indexes    

A clustered index is an index whose leaf nodes, that is the lowest level of the index, contain the actual data pages of the underlying table. Hence the index and the table itself are, for all practical purposes, one and the same.  Each table can have only one clustered index. For more information on clustered indexes, see the Books Online topic "Clustered Index Structures" (

When a clustered index is used to resolve a query, SQL Server begins at the root node for the index and traverses the intermediate nodes until it locates the data page that contains the row it's seeking.

Many database designs make prolific use of clustered indexes. In fact, it is generally considered a best practice to include a clustered index on each table; of course that's painting with a very broad brush and there will most assuredly be exceptions. For more information about the benefits of clustered indexes, see the SQL Server Best Practices Article entitled "Comparing Tables Organized with Clustered Indexes versus Heaps" on TechNet .

Lets’ consider an example. In the Figure 1, the Customers table has a clustered index defined on the Customer_ID column. When a query is executed that searches by the Customer_ID column, SQL Server navigates through the clustered index to locate the row in question and returns the data. This can be seen in the Clustered Index Seek operation in the query’s Execution Plan.

Figure 1. Clustered index execution plan

The following Transact-SQL statement was used to create the clustered index on the Customer_ID column.

CREATE CLUSTERED INDEX [ix_Customer_ID] ON [dbo].[Customers]
[Customer_ID] ASC

Nonclustered indexes

Nonclustered indexes use a similar methodology to store indexed data for tables within SQL Server. However in a nonclustered index, the lowest level of the index does not contain the data page of the table. Instead, it contains the information that allows SQL Server to navigate to the data pages it needs. For tables that have a clustered index, the leaf node of the nonclustered index contains the clustered index keys. In the previous example, the leaf node of a nonclustered index on the Customers table would contain the Customer_ID key.

If the underlying table does not have a clustered index (this data structure is known as a heap), the leaf node of the nonclustered index contains a row locator to the heap data pages.

In the following example, a nonclustered composite index has been created on the Customers table as described in the following Transact-SQL code.

CREATE NONCLUSTERED INDEX [ix_Customer_Name] ON [dbo].[Customers]
[Last_Name] ASC,
[First_Name] ASC

In this case when a query that searched by customer last name was executed, the SQL Server query optimizer chose to use the ix_Customer_Name index to resolve the query. This can be seen in the Execution Plan in the following figure.

Figure 2. Nonclustered index execution plan

For more information on clustered indexes, see the Books Online topic "Nonclustered Index Structures" .

Using Nonclustered indexes

As illustrated in the preceding example, nonclustered indexes may be employed to provide SQL Server with an efficient way to retrieve data rows. However, under some circumstances, the overhead associated with nonclustered indexes may be deemed too great by the query optimizer and SQL Server will resort to a table scan to resolve the query. To understand how this may happen, let's examine the preceding example in more detail.

Key Lookups

Looking again at the graphical query execution plan depicted in Figure 2, notice that the plan not only includes an Index Seek operation that uses the ix_Customer_Name nonclustered index, it also includes a Key Lookup operation.

SQL Server uses a Key Lookup to retrieve non-key data from the data page when a nonclustered index is used to resolve the query. That is, once SQL Server has used the nonclustered index to identify each row that matches the query criteria, it must then retrieve the column information for those rows from the data pages of the table.

Since the leaf node of the nonclustered index contains the key value information for the row, SQL Server must navigate through the clustered index to retrieve the columnar information for each row of the result set. In this example, SQL Server choose to do this using a nested loop join type.

This query produced a result of 1,000 rows and nearly 100% of the expense of the query was directly attributed to the Key Lookup operation. Digging a little deeper into the Key Lookup operation, we can see why.

Figure 3. Key lookup operation properties

This Key Lookup operation was executed 1000 times, once for each row of the result set. 

Resorting to Table Scans

As the number of rows in the result set increases, so does the number of Key lookups. At some point, the cost associated with the Key Lookup will outweigh any benefit provided by the nonclustered index.

To illustrate this point, let's modify the query so that it retrieves more rows. Figure 4. depicts the new query along with the actual execution plan used to resolve the query.

Figure 4. Resorting to a table scan

The new query searches for a range of customers whose last name is between "Roland" and "Smith". There are 69,000 of them in our database. From the actual execution plan, we can see that the query optimizer determined that the overhead cost of performing a Key Lookup for each of the 69,000 rows was more than simply traversing the entire table via a table scan. Hence, our ix_Customer_Name index was not used at all during the query.

Figure 5  shows some additional properties of the table scan.

Figure 5. Properties for the table scan operation

One may be tempted to force SQL Server to resolve the query using the nonclustered index by supplying a table hint as shown in the following illustration.

Figure 6. Using a table hint to resolve the query

This is almost always a bad idea since the optimizer generally does a good job in choosing an appropriate execution plan. Additionally, the optimizer bases its decisions on column statistics; those are likely to change over time. A table hint that works well today, may not work well in the future when the selectivity of the key columns change.

Figure 7 shows the properties for the Key Lookup when we forced SQL Server to use the nonclustered ix_Customer_Name index. The Estimated Operator Cost for the Key Lookup is 57.02 compared to 12.17 for the Clustered Index Scan shown in Figure 5. Forcing SQL Server to use the index significantly affected performance, and not for the better!

Figure 7. Properties of the Key Lookup for the table hint

Covering Indexes

So, if Key Lookups can be detrimental to performance during query resolution for large result sets, the natural question is: how can we avoid them? To answer that question, let's consider a query that does not require a Key Lookup.

Let's begin by modifying our query so that it no longer selects the Email_Address column. Figure 8 illustrates this updated query along with its actual execution plan.

Figure 8. Reduced query to eliminate the Key Lookup

The new execution plan has been streamlined and only uses the ix_Customer_Name nonclustered index. Looking at the properties of the operation providers further evidence of the improvement. The properties are shown in Figure 9.

Figure 9. Reduced query to eliminate the Key Lookup properties

The Estimated Operator Cost went down dramatically, from 12.17 in Figure 5 to 0.22 in Figure 9.  We could also look at the Logical and Physical Read characteristics by setting STATISTICS IO on, however for this demonstration it’s sufficient to view the Operator Costs for each operation.

The observed improvement is due to the fact that the nonclustered index contained all of the required information to resolve the query. No Key Lookups were required. An index that contains all information required to resolve the query is known as a "Covering Index"; it completely covers the query.

Using the Clustered Key Column

Recall that if a table has a clustered index, those key columns are automatically part of the nonclustered index. So, the following query is a covering query by default.

Figure 10. Covering index using the clustered index keys

However unless your clustered index contains the required columns, which is not the case in our example, it will be insufficient for covering our query.

Adding Key Columns to the Index

To increase the likelihood that a nonclustered index is a covering index, it is tempting to begin adding additional columns to the index key. For example, if we regularly query the customer's middle name and telephone number, we could add those columns to the ix_Customer_Name index. Or, to continue with our previous example, we could the Email_Address column to the index as shown in the following Transact-SQL code.

CREATE NONCLUSTERED INDEX [ix_Customer_Email] ON [dbo].[Customers]
[Last_Name] ASC,
[First_Name] ASC,
[Email_Address] ASC

Before doing so, it is important to remember that indexes must be maintained by SQL Server during data manipulation operations. Too many index hurts performance during write operations. Additionally, the wider the index, that is to say the more bytes that make up the index keys, the more data pages it will take to store the index.

Furthermore, there are some built in limitations for indexes. Specifically, indexes are limited to 16 key columns or 900 bytes, whichever comes first, in both SQL Server 2005 and SQL Server 2008. And some datatypes cannot be used as index keys, varchar(max) for instance.

Including Non-Key columns

SQL Server 2005 provided a new feature for nonclustered indexes, the ability to include additional, non-key columns in the leaf level of the nonclustered indexes. These columns are technically not part of the index, however they are included in the leaf node of the index. SQL Server 2005 and SQL Server 2008 allow up to 1023 columns to be included in the leaf node.

To create a nonclustered index with included columns, use the following Transact-SQL syntax.

CREATE NONCLUSTERED INDEX [ix_Customer_Email] ON [dbo].[Customers]
[Last_Name] ASC,
[First_Name] ASC

Rerunning our query yields an execution plan that make use of our new index to rapidly return the result set. The execution plan may be found in the Figure 11.

Figure 11. Covering query with included columns

Notice that even though our query selects columns that are not part of the nonclustered index’s key, SQL Server is still able to resolve the query without having to use a Key Lookup for each row. Since the ix_CustomerEmail index includes the Email_Address column as part of its definition, the index “covers” the query. The properties of the Nonclustered Index Seek operator confirm our findings as depicted in the Figure 12.

Figure 12. Execution properties for the covering query with included columns

From this execution plan, we can see that the Estimate Operator Cost decreased from 12.17 in Figure 5 to 0.41. Including an additional non-key column has dramatically improved the performance of the query.

For more information about including non-key columns in a nonclustered index, See "Index with Included Columns" in Books Online .


By including frequently queried columns in nonclustered indexes, we can dramatically improve query performance by reducing I/O costs. Since the data pages for an nonclustered index are frequently readily available in memory, covering indexes are the usually the ultimate in query resolution.

Joe Webb

Author profile:

Joe Webb, a Microsoft SQL Server MVP, serves as Chief Operating Manager for WebbTech Solutions, a Nashville-based consulting company. He has over 15 years of industry experience and has consulted extensively with companies in the areas of software development, database design, and technical training. In addition to helping his consulting clients, Joe enjoys writing and speaking at technical conferences. He has spoken at conferences in Europe and the North America and has authored two books: “The Rational Guide To: SQL Server Notification Services” and “The Rational Guide To: IT Consulting”. He may be reached at

Search for other articles by Joe Webb

Rate this article:   Avg rating: from a total of 421 votes.





Must read
Have Your Say
Do you have an opinion on this article? Then add your comment below:
You must be logged in to post to this forum

Click here to log in.

Subject: Very useful
Posted by: Anonymous (not signed in)
Posted on: Wednesday, October 1, 2008 at 12:53 AM
Message: Excellent

Subject: Excellent Article
Posted by: dmoyer (view profile)
Posted on: Wednesday, October 1, 2008 at 8:29 AM
Message: This is an excellent explanation of covering indexes. I have been struggling with this subject recently and this article cleared up my questions. Thanks.

Subject: Well-Written Article
Posted by: EAl-Chokhachy (not signed in)
Posted on: Wednesday, October 1, 2008 at 11:39 AM
Message: Excellent presentation of what can be a very daunting issue in SQL Server. Concise, thorough and easily understood. Thanks.

Subject: Brilliant
Posted by: Anonymous (not signed in)
Posted on: Monday, October 6, 2008 at 8:35 AM
Message: A wonderfully concise and informative article.

Subject: Awesome
Posted by: Raj (view profile)
Posted on: Thursday, October 9, 2008 at 7:43 PM
Message: Thanks Joe, Excellent article & very clear about what is mean by covering index. My long time question is clear now.
Thanks once again

Subject: Excellent
Posted by: Shashank (not signed in)
Posted on: Friday, October 17, 2008 at 4:52 AM
Message: Joe,
Thanks for writing this extremely useful article.

Subject: Great overview - Pingback
Posted by: TPowell_3557 (view profile)
Posted on: Monday, February 21, 2011 at 6:32 PM
Message: Your article is very in-depth and a great refresher. I'm blogging about the relationship between tables and files and I would like to link to your article as a great primer if someone needs it. I'm not sure of the etiquette but I will assume the link is OK unless I hear otherwise. It will be posted here:

Subject: thanks for post
Posted by: rins (view profile)
Posted on: Monday, May 23, 2011 at 12:48 AM
Message: I have been struggling with this subject recently and this article cleared up my questions. Thanks. [url=]wordpress premium themes[/url]

Subject: Excellent article
Posted by: YaHozna (view profile)
Posted on: Wednesday, January 15, 2014 at 2:01 PM
Message: Written 5 years ago but still a valid and lucid article. Sometimes it just takes the right person to write about something and it becomes perfectly clear.

Subject: Using Covering Indexes to Improve Query Performance
Posted by: Ashish Kumar (view profile)
Posted on: Tuesday, February 17, 2015 at 11:43 PM
Message: Hi Joe,

This is a great article but I have a doubt, can you please help me out.

In figure 2 you were create a non clustered index on customer table.
when you execute a query to get the customer informations based on last name. The query execution plan show missing index information.
What was the reason behind that when you have a non clustered index already defined on it and you filter a query result set on that column where has a index.

Can you please share the reason why it is happen ?

Many Thanks
Ashish Kumar

Subject: Excellent article, one question
Posted by: justramesh2000 (view profile)
Posted on: Monday, August 17, 2015 at 7:53 AM
Message: Joe, this is a very good article with all required insight. I have an out of context question, which is generic about indexes. What is the significance of Allow_Row_Locks and Allow_Page_Locks and how then impact performance ?

Simple-Talk Database Delivery

Patterns & Practices Library

Visit our patterns and practices library to learn more about database lifecycle management.

Find out how to automate the process of building, testing and deploying your database changes to reduce risk and make rapid releases possible.

Get started

Phil Factor
How to Build and Deploy a Database from Object-Level Source in a VCS

It is easy for someone who is developing a database to shrug and say 'if only my budget would extend to buying fancy... Read more...

 View the blog

Top Rated

Clone, Sweet Clone: Database Provisioning Made Easy?
 One of the difficulties of designing a completely different type of development tool such as SQL Clone... Read more...

Database Lifecycle Management: Deployment and Release
 So often, the unexpected delays in delivering database code are more likely to happen after the... Read more...

The PoSh DBA: Assigning Data to Variables Via PowerShell Common Parameters
 Sometimes, it is the small improvements in a language that can make a real difference. PowerShell is... Read more...

Issue Tracking for Databases
 Any database development project will be hard to manage without a system for reporting bugs in the... Read more...

Releasing Databases in VSTS with Redgate SQL CI and Octopus Deploy
 You can still do Database Lifecycle Management (DLM) workflows in the hosted version of Team foundation... Read more...

Most Viewed

Beginning SQL Server 2005 Reporting Services Part 1
 Steve Joubert begins an in-depth tour of SQL Server 2005 Reporting Services with a step-by-step guide... Read more...

Ten Common Database Design Mistakes
 If database design is done right, then the development, deployment and subsequent performance in... Read more...

Temporary Tables in SQL Server
 Temporary tables are used by every DB developer, but they're not likely to be too adventurous with... Read more...

Concatenating Row Values in Transact-SQL
 It is an interesting problem in Transact SQL, for which there are a number of solutions and... Read more...

SQL Server Index Basics
 Given the fundamental importance of indexes in databases, it always comes as a surprise how often the... Read more...

Why Join

Over 400,000 Microsoft professionals subscribe to the Simple-Talk technical journal. Join today, it's fast, simple, free and secure.