Do we need to include a Clustered Index Column too in a Non-Clustered Index to make it as a Covering Index for a Query? Tip 7: Sql Server 101 Performance Tuning Tips and Tricks

Answer to this question is NO, because by default when we create a Non-Clustered Index in Sql Server it adds the Clustered Index Key Column values too in the Non-Clustered Index.

Let us understand this with an example. Let us create a Customer table with Clustered Index on the CustomerId column as shown in the below image with sample 35K records by executing the following script.

Performance Issue Function on Index Column Customers Demo Table

CREATE DATABASE SqlHints101PerfTips7
GO
USE SqlHints101PerfTips7
GO
--Create Demo Table Customers
CREATE TABLE dbo.Customers (
	CustomerId INT IDENTITY(1,1) NOT NULL,
        FirstName VARCHAR(50), 
	LastName  VARCHAR(50),
	PhoneNumber VARCHAR(10),
	EmailAddress VARCHAR(50),
	CreationDate DATETIME
)
GO
--Populate 35K dummy customer records
INSERT INTO dbo.Customers (FirstName, LastName, PhoneNumber, EmailAddress, CreationDate)
SELECT TOP 35000 REPLACE(NEWID(),'-',''), REPLACE(NEWID(),'-',''), 
	CAST( CAST(ROUND(RAND(CHECKSUM(NEWID()))*1000000000+4000000000,0) AS BIGINT) AS VARCHAR(10)),
	REPLACE(NEWID(),'-','') + '@gmail.com',		
	DATEADD(HOUR,CAST(RAND(CHECKSUM(NEWID())) * 19999 as INT) + 1 ,'2006-01-01')
FROM sys.all_columns c1
		CROSS JOIN sys.all_columns c2
GO
--Update one customer record with some known values
UPDATE dbo.Customers 
SET FirstName = 'Basavaraj', LastName = 'Biradar', 
    PhoneNumber = '4545454545', EmailAddress = 'basav@gmail.com'
WHERE CustomerId = 10000
GO
--Create a PK and a Clustered Index on CustomerId column
ALTER TABLE dbo.Customers 
ADD CONSTRAINT PK_Customers_CustomerId 
PRIMARY KEY CLUSTERED (CustomerId)
GO

Assume that we have a requirement to get the CustomerId for the given customer FirstName and LastName. We can write a query like below to get the CustomerId by Customers FirstName and LastName. Let us now enable the execution plan in the Sql Server Management Studio by pressing the key stroke CTRL + M and then execute the following query.

SELECT CustomerId
FROM dbo.Customers WITH(NOLOCK)
WHERE FirstName = 'Basavaraj' AND LastName = 'Biradar'

RESULT:
TIP 7 Result of a Clustered Index Scan Query
EXECUTION PLAN:
TIP 7 Execution Plan Clustered Index Scan

From the above execution plan we can see that this query is doing Clustered Index Scan (i.e. Table Scan). Now let us create a covering index (i.e. an index containing all queried columns) for this query so that it gives result from this Covering Index itself without requiring it to be looked-up into the Clustered Index. Now from the query we can see that it is referring to three columns i.e. FirstName, LastName and CustomerId. Out of these three columns CustomerId column is a Clustered Index column and other two columns (i.e. FirstName and LastName) are non-indexed column. Now comes doubt in the mind, to make a new index a Covering Index for the above query do I need to add the CustomerId column too in the list of columns in the new index apart from FirstName and LastName columns. The answer to this question is, we don’t need to add CustomerId column to the new index as it is a Clustered Index column. Let us understand this with an example. Now create a Composite non-clustered index on the FirstName and LastName column without CustomerId column by executing the following script.

--Create a non-clustered index on FirstName and LastName column
CREATE NONCLUSTERED INDEX IX_Customers_FirstName_LastName
    ON dbo.Customers (FirstName, LastName)
GO

Now execute the above select statement once again and verify whether this new index is a Covering index for this query

SELECT CustomerId
FROM dbo.Customers WITH(NOLOCK)
WHERE FirstName = 'Basavaraj' AND LastName = 'Biradar'

RESULT:
TIP 7 Execution Plan Covering Index

From the above execution plan we can see that Sql Server is doing the Seek of the newly created composite index IX_Customers_FirstName_LastName and we don’t see any key look-up into the Clustered index. So, this index is a covering index for the above query even though CustomerId column refereed in the SELECT statement is not part of the non-clustered index.

Let us inspect the index pages of the non-clustered index to see how Sql Server is adding Clustered Index Key Column values too in the Non-Clustered Index.

To explain this I will be using the below function which I have Created and explained in the previous article How to get an Index’s Root Page, Intermediate Pages and Leaf Pages Information?. This function uses the un-documented (i.e. feature that may change or removed without any notice or may produce un-expected result. So avoid using it in the Production environment) Dynamic Management Function sys.dm_db_database_page_allocations which is available from Sql Server 2012 onwards.

CREATE FUNCTION dbo.GetPagesOfBPlusTreeLevel(
    @DBName VARCHAR(100), @TableName VARCHAR(100) = NULL, @IndexName VARCHAR(100) = NULL, 
    @PartionId INT = NULL, @MODE VARCHAR(20), @BPlusTreeLevel VARCHAR(20) 
)
RETURNS
@IndexPageInformation TABLE (
    [DataBase] VARCHAR(100), [Table] VARCHAR(100), [Index] VARCHAR(100), 
    [partition_id] INT, [file_id] INT, [page_id] INT, page_type_desc VARCHAR(100), 
    page_level INT, [previous_page_id] INT, [next_page_id] INT)
AS
BEGIN
 
    DECLARE @MinPageLevelId INT = 0 , @MaxPageLevelId INT = 0, @IndexId INT = NULL
         
    SELECT  @IndexId = index_id
    FROM sys.indexes
    WHERE OBJECT_NAME(object_id) = @TableName AND name = @IndexName
 
    IF @IndexId IS NULL
        RETURN
     
    IF @BPlusTreeLevel IN ('Root', 'Intermediate')  
    BEGIN  
        SELECT  @MaxPageLevelId = (CASE WHEN  @BPlusTreeLevel ='Intermediate' THEN MAX(page_level) - 1 ELSE MAX(page_level) END), 
                @MinPageLevelId = (CASE WHEN  @BPlusTreeLevel ='Intermediate' THEN 1 ELSE MAX(page_level) END)
        FROM sys.dm_db_database_page_allocations 
            (DB_ID(@DBName), OBJECT_ID(@TableName), @IndexId, @PartionId, 'DETAILED') PA
                    LEFT OUTER JOIN sys.indexes SI 
                ON SI.object_id = PA.object_id AND SI.index_id = PA.index_id
        WHERE is_allocated = 1 AND page_type in (1,2)  -- INDEX_PAGE and DATA_PAGE Only
 
        IF @MaxPageLevelId IS NULL OR @MaxPageLevelId = 0 
            RETURN
    END
  
    INSERT INTO @IndexPageInformation
    SELECT DB_NAME(PA.database_id) [DataBase], OBJECT_NAME(PA.object_id) [Table], SI.Name [Index], 
        [partition_id], allocated_page_file_id [file_id],  allocated_page_page_id [page_id], page_type_desc, 
        page_level, previous_page_page_id [previous_page_id], next_page_page_id [next_page_id]
    FROM sys.dm_db_database_page_allocations 
            (DB_ID(@DBName), OBJECT_ID(@TableName), @IndexId, @PartionId, 'DETAILED') PA
             LEFT OUTER JOIN sys.indexes SI 
            ON SI.object_id = PA.object_id AND SI.index_id = PA.index_id
    WHERE is_allocated = 1 AND page_type in (1,2) -- INDEX_PAGE and DATA_PAGE Only
            AND page_level between @MinPageLevelId AND @MaxPageLevelId
    ORDER BY page_level DESC, previous_page_page_id
 
    RETURN
END

Inspect the content of the Non-Culstered Index B+ tree’s ROOT Page

Let us use the above function GetPagesOfBPlusTreeLevel to get the Root page of a Non-Clustered Index. Execute the following statement to get the Root page of the Non-Clustered Index IX_Customers_FirstName_LastName on the Customers table.

SELECT *
FROM dbo.GetPagesOfBPlusTreeLevel ('SqlHints101PerfTips7', 
	'Customers', 'IX_Customers_FirstName_LastName',
	 NULL, 'DETAILED', 'Root')

RESULT:
TIP 7 Root Page of a Non-Clustered Index

Let us use the DBCC PAGE Command which I have explained in the previous article How to Inspect the content of a DATA PAGE or INDEX PAGE? to inspect the content of the Root Page with FileId:1 and PageId:2810 of the Non-Clustered Index. Note: DBCC PAGE is an un-documented (i.e. feature that may change or removed without any notice or may produce un-expected result. So avoid using it in the Production environment) command.

DBCC PAGE ('SqlHints101PerfTips7', 1, 2810, 3) WITH TABLERESULTS

RESULT:
TIP 7 Non-Clustered Index Root Page Content

From the above result we can see that Clustered Index Key Column CustomerId value corresponding to the the Non-Clustered Index Key Columns is added in the non-clustered Index Root Page.

Inspect the content of the Non-Culstered Index B+ tree’s INTERMEDIATE Page

Let us use execute the following statement to get the INTERMEDIATE pages of a Non-Clustered Index IX_Customers_FirstName_LastName on the Customers table.

SELECT *
FROM dbo.GetPagesOfBPlusTreeLevel ('SqlHints101PerfTips7', 
	'Customers', 'IX_Customers_FirstName_LastName',
	 NULL, 'DETAILED', 'Intermediate')

RESULT:
TIP 7 Intermediate Pages of a Non-Clustered Index

From the above result we can see that there are four Intermediate Level Pages for a Non-Clustered Index. Let us use the DBCC PAGE Command to inspect the content of the one of the Intermediate Level Page with FileId:1 and PageId:2811 of the Non-Clustered Index.

DBCC PAGE ('SqlHints101PerfTips7', 1, 2811, 3) WITH TABLERESULTS

RESULT:
TIP 7 Non-Clustered Index Intermediate Page Content

From the above content of the Non-Clustered Index Intermediate Page, we can see that Clustered Index Key Column CustomerId value corresponding to the the Non-Clustered Index Key Columns is added in the non-clustered Index Intermediate Level Page.

Inspect the content of the Non-Culstered Index B+ tree’s LEAF Page

Let us use execute the following statement to get all the LEAF Pages of a Non-Clustered Index IX_Customers_FirstName_LastName on the Customers table.

SELECT *
FROM dbo.GetPagesOfBPlusTreeLevel ('SqlHints101PerfTips7', 
	'Customers', 'IX_Customers_FirstName_LastName',
	 NULL, 'DETAILED', 'Leaf')

RESULT:
TIP 7 Leaf Pages of a Non-Clustered Index

In this case there are 347 leaf pages. Let us inspect the content of the one of the Leaf Level Page with FileId:1 and PageId:3000 of the Non-Clustered Index.

DBCC PAGE ('SqlHints101PerfTips7', 1, 3000, 3) WITH TABLERESULTS

RESULT:
TIP 7 Leaf Page of a Non-Clustered Index

From the above content of the Non-Clustered Index Leaf Page, we can see that Clustered Index Key Column CustomerId value corresponding to the the Non-Clustered Index Key Columns is added in the non-clustered Index Leaf Level Page.

Conclusion:

From the above examples we can see that when we create a Non-Clustered Index, Sql Server adds the Clustered Index Key Column values Corresponding to the Non-Clustered Index Key columns in the Index Root, Intermediate and Leaf Level Pages. So, we don’t explicitly need to include a Clustered Index Key Column in a Non-Clustered Index to make it as a Covering Index for a Query. In case of an unique Non-Clustered Index the Root and Intermediate level pages will not have the Clustered Index Key Column Values in it, but it’s Leaf Page will have the Clustered Index Key Column value. In this way again for Unique Non-Clustered Index also we don’t explicitly need to add Clustered Index Key Column.

Leave a Reply

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