{"id":7304,"date":"2026-07-20T10:00:49","date_gmt":"2026-07-20T17:00:49","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/azure-sql\/?p=7304"},"modified":"2026-07-17T17:00:27","modified_gmt":"2026-07-18T00:00:27","slug":"tsql-covering-index","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/azure-sql\/tsql-covering-index\/","title":{"rendered":"T-SQL Hygiene: Introducing the Covering Index"},"content":{"rendered":"<p>Often in applications, we write database queries. And often, the same query is executed again and again. To make queries against large tables faster, we can add an index. This is a general improvement for anyone accessing the table.\u00a0However, there is a specially designed index called a covering index that can make queries against large tables particularly faster for one specific query. We call it covering because it covers all the projections and predicates in that query.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/07\/covering-index.webp\"><img decoding=\"async\" class=\"alignnone size-full wp-image-7307\" src=\"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/07\/covering-index.webp\" alt=\"covering index\" width=\"1672\" height=\"941\" srcset=\"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/07\/covering-index.webp 1672w, https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/07\/covering-index-300x169.webp 300w, https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/07\/covering-index-1024x576.webp 1024w, https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/07\/covering-index-768x432.webp 768w, https:\/\/devblogs.microsoft.com\/azure-sql\/wp-content\/uploads\/sites\/56\/2026\/07\/covering-index-1536x864.webp 1536w\" sizes=\"(max-width: 1672px) 100vw, 1672px\" \/><\/a><\/p>\n<p><strong>Which query?<\/strong> That is up to you, the developer, to identify. Look for queries that are either 1) very important to run fast or 2) extremely frequent in your application code.\u00a0Just remember, indexes are not free, so we do not create one for every query. But for the right query, we can justify a covering index, and it can make all the difference.<\/p>\n<h2>A little deeper<\/h2>\n<p>Imagine an application that shows a list of customer orders. It would frequently run this query:<\/p>\n<pre><code class=\"language-sql\">SELECT\r\n    OrderDate,\r\n    TotalAmount,\r\n    Status\r\nFROM dbo.Orders\r\nWHERE CustomerId = 42;\r\n<\/code><\/pre>\n<p>Let\u2019s look at how SQL Server handles a query like this. Once the engine identifies the correct table, it applies the predicates in the <code>WHERE<\/code> clause to filter the rows. In this case, it keeps only rows where <code>CustomerId<\/code> is 42. This can reduce millions of rows, and tons of unnecessary data, to just the required few.<\/p>\n<p>Finally, SQL Server applies the projection in the <code>SELECT<\/code> clause to return only <code>OrderDate<\/code>, <code>TotalAmount<\/code>, and <code>Status<\/code>. Notice that <code>CustomerId<\/code> is needed to find the rows but is not part of the final column list. This can reduce hundreds of columns, and tons of unnecessary data, to just the required few.<\/p>\n<h2>The mechanics of filtering rows<\/h2>\n<p>To make this fast, SQL Server looks for a useful index. Without one, the engine may have to step through every row in the table. With one, SQL Server can seek directly to the matching values instead of scanning the entire table.\u00a0Think about finding a specific book in a large library without knowing where it is versus having its index location. It is night and day, even for a fast engine like SQL Server.<\/p>\n<h3>The clustered index<\/h3>\n<pre class=\"prettyprint language-sql\"><code class=\"language-sql\">CREATE CLUSTERED INDEX IX_Orders_OrderId\r\nON dbo.Orders(OrderId);<\/code><\/pre>\n<p>Tables with a primary key usually have an index supporting that key. By default, SQL Server creates it as a clustered index unless the table already has one or you specify otherwise.<\/p>\n<p>Imagine that same library with all its books mixed up. A clustered index is like arranging them in order, making it faster to locate a specific section.\u00a0However, this is the <code>Orders<\/code> table, so its primary key is likely <code>OrderId<\/code>, not <code>CustomerId<\/code>. The primary-key index therefore does not directly help SQL Server find every order for customer 42.<\/p>\n<h3>The nonclustered index<\/h3>\n<pre class=\"prettyprint language-sql\"><code class=\"language-sql\">CREATE NONCLUSTERED INDEX IX_Orders_CustomerId\r\nON dbo.Orders(CustomerId);<\/code><\/pre>\n<p>We could create a nonclustered index on <code>CustomerId<\/code>. Think of Battleship. When someone says E-4, you know exactly where to look. A nonclustered index works similarly: it identifies where matching rows can be found without reorganizing the entire table.\u00a0It is also worth pointing out that <code>CustomerId<\/code> being a foreign key does not mean it automatically has an index. A foreign key references a key in another table, in this case <code>Customers<\/code>, but SQL Server does not automatically create an index for it.<\/p>\n<h3>The worst case<\/h3>\n<p>Without an index on <code>CustomerId<\/code>, SQL Server does what it has to do: it scans the table, tests each row, and keeps the matching ones. With a few thousand rows, this may still be fast. With a few million, it can become noticeably slow.\u00a0All of this may happen in a second or so. A covering index can turn that into only a few milliseconds.<\/p>\n<h2>The covering index<\/h2>\n<p>Clustered or nonclustered is not the important part of a covering index. The query is. More specifically, the predicates in the query and the columns projected by the query are what matter.\u00a0A query may benefit generically from an existing index, but a covering index is intentionally designed to match that query\u2019s predicates and projected columns.<\/p>\n<h3>Including columns<\/h3>\n<pre><code class=\"language-sql\">CREATE NONCLUSTERED INDEX IX_Orders_CustomerId\r\nON dbo.Orders(CustomerId)\r\nINCLUDE (OrderDate, TotalAmount, Status);\r\n<\/code><\/pre>\n<p>Now is an important time to mention that indexes can contain more than predicate columns. They can also include data.<\/p>\n<p>The <code>INCLUDE<\/code> keyword adds columns to the index without making them part of the index key. If SQL Server finds the predicate value in the index and the query requests only columns included in that index, it may not need to return to the source table at all.<\/p>\n<p>This combination of matching predicates and available projected columns is the heart of a covering index. Nothing could be faster than finding matching predicates in an index and, while already there, returning the included columns. This is one of the quickest ways to return query results.<\/p>\n<h2>Designing a covering index<\/h2>\n<p>To design a covering index, start with the query. Identify the columns used in the predicates, then identify the columns returned by the projection. Let\u2019s go back to our original query:<\/p>\n<pre><code class=\"language-sql\">SELECT \r\n    OrderDate, \r\n    TotalAmount, \r\n    Status \r\nFROM dbo.Orders \r\nWHERE CustomerId = 42;\r\n<\/code><\/pre>\n<p>Once you determine the hot spots in your application, the query or queries that get the most exercise, it is worth testing their performance against production-sized tables. A frequent query is not always an immediate candidate for a covering index. However, if you determine that one of these hot-path queries is impacting the user experience, it is time to think about a covering index.<\/p>\n<h3>Step 1: Identify the predicates<\/h3>\n<p>A lot of custom code dynamically adds or removes <code>WHERE<\/code> predicates through a filter interface or based on user behavior. Identify either the superset of columns used as predicates or the columns that appear in your core use cases.<\/p>\n<p>In our sample, this is only <code>CustomerId<\/code>. Here is the syntax for adding that predicate to the index:<\/p>\n<pre><code class=\"language-sql\">CREATE NONCLUSTERED INDEX IX_Orders_CustomerId\r\nON dbo.Orders(CustomerId);\r\n<\/code><\/pre>\n<p>The columns inside the parentheses are the index key columns. They are ordered and used by SQL Server to find matching rows.<\/p>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Column order matters too. <\/strong><\/p>Put equality predicates first, followed by range predicates, because SQL Server uses the leading index columns to narrow the search. Included columns do not affect the sort order, so their order is usually less important. Projection columns order usually does not matter.<\/div><\/p>\n<h3>Step 2: Identify the data<\/h3>\n<p>Pay attention when duplicating table data into an index, especially when the column data types are large. Columns containing JSON, vectors, or binary data should cause you to reconsider this step.<\/p>\n<p>But perhaps the projected columns are only <code>OrderDate<\/code>, <code>TotalAmount<\/code>, and <code>Status<\/code>, as in our sample query. If you are comfortable with the additional storage, or the user experience is paramount in this equation, include all the projected columns.<\/p>\n<p><div class=\"alert alert-info\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Two caveats are important. <\/strong><\/p>If you include all but one projected column in the index, SQL Server may still be required to return to the source table for that missing value. This eliminates much of the core value of a covering index.<\/div><\/p>\n<p>On the flip side, if you include every column in the table, you have nearly duplicated the source table and introduced unnecessary storage and maintenance costs.<\/p>\n<p>Here is the syntax for including the projected columns:<\/p>\n<pre><code class=\"language-sql\">CREATE NONCLUSTERED INDEX IX_Orders_CustomerId\r\nON dbo.Orders(CustomerId)\r\nINCLUDE (OrderDate, TotalAmount, Status);\r\n<\/code><\/pre>\n<p>Indexes maintain themselves. Once an index is initially built, changes to the underlying table automatically update that index and every other affected index on the table.\u00a0It is worth remembering that an insert, update, or delete may not complete until the affected indexes are also updated. This means large or excessive indexes can impact the OLTP performance of your database.<\/p>\n<h2>Conclusion<\/h2>\n<p>A covering index is a kind of secret sauce when it comes to SQL performance. That is, for some queries. An army of covering indexes should be a code smell, for sure.<\/p>\n<p>For the rest of your database and other queries, it often makes more sense to create general indexes that serve several queries. In fact, the automatic tuning feature in Azure SQL reviews query activity to identify queries that might benefit from additional indexes or changes to existing indexes. You can do the same thing.<\/p>\n<p><div class=\"alert alert-success\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Lightbulb\"><\/i><strong>Do not just add a covering index. <\/strong><\/p>Test it to ensure that unwanted side effects do not creep into your workload.<\/div><\/p>\n<p>In either case, the key to database performance is a smart schema, smart queries, and appropriate indexes. Think through your application and the queries you run. The next time you find a hot spot, open <a href=\"https:\/\/learn.microsoft.com\/en-us\/ssms\/github-copilot\/get-started\">Copilot in SQL Server Management Studio<\/a>, paste your query, and prompt:<\/p>\n<pre><code class=\"language-text\">Help me design a covering index for this query.\r\n<\/code><\/pre>\n<p>Then see what you can do.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Often in applications, we write database queries. And often, the same query is executed again and again. To make queries against large tables faster, we can add an index. This is a general improvement for anyone accessing the table.\u00a0However, there is a specially designed index called a covering index that can make queries against large [&hellip;]<\/p>\n","protected":false},"author":96788,"featured_media":7311,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,619],"tags":[670,487],"class_list":["post-7304","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure-sql","category-t-sql","tag-github-copilot","tag-query-processing"],"acf":[],"blog_post_summary":"<p>Often in applications, we write database queries. And often, the same query is executed again and again. To make queries against large tables faster, we can add an index. This is a general improvement for anyone accessing the table.\u00a0However, there is a specially designed index called a covering index that can make queries against large [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts\/7304","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/users\/96788"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/comments?post=7304"}],"version-history":[{"count":2,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts\/7304\/revisions"}],"predecessor-version":[{"id":7314,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts\/7304\/revisions\/7314"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/media\/7311"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/media?parent=7304"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/categories?post=7304"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/tags?post=7304"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}