SQL Server索引優化——重複索引

SQL Server索引優化——重複索引

在寫完《SQL Server 索引優化——無用索引和索引缺失》系列後,就想着寫點關於發現重複索引的內容,剛好在Kimberly的博文中發現了這篇,就偷懶了,直接將其翻譯過來。

一直以來,對重複索引都有許多困惑,我想的最多是如何使用sp_helpindex(或者SSMS)展示索引所包含的內容。索引到底包含什麼?索引的架構是怎樣的?這些通常都不是我們所看到的那樣。這是我最初重寫sp_helpindex的動力所在,但即使這樣,我仍然感到很多困惑。在今天的博文中,我將首先準確的解釋哪些索引是相同的,哪些不同,並且說明一些工具中的錯誤。

因此,從索引的架構開始……(一切從內部開始)

聚集索引是數據。聚集索引的鍵(我通常稱其爲聚集鍵),定義了數據的存儲順序的方式(不一定準確,硬盤上的物理順序,而非邏輯順序)。而且,不,我並不打算在此展開來說索引內部的所有內容……僅僅幾點體現。

非聚集索引是重複的數據(類似於書後面的索引)。這些重複的數據可以被用於幫助檢索真實數據(就像書後面的索引一樣),或者被用於響應請求(例如,如果你單單是爲了查詢某姓的人有多少,那麼一個包含姓的索引可以用來計算,而不是去查詢實際的數據)。因此,索引有一些非常強大的用途。但是,唉,這不是一篇關於索引使用或者索引策略的文章——這裏都是關於內部的(理解索引的架構)。所以我們要開門見山了。

一個非聚集索引總是這樣的:

  • 鍵(這是定義索引順序的)

  • 葉級實體(這是索引中真正數據存儲的地方+查找值+所有包含列)——然而所有這些列僅存儲一次(且它們總是被存儲在這裏一次,即使你引用的一列是查找值的一部分,SQL Server也不會重複存儲它)。

*那麼什麼是查找值(lookup value)?

查找值是SQL Server 用來指向真實數據的行。如果一個表有聚集索引,那麼查找值就是聚集鍵(包括所有定義聚集鍵的列)。如果一個表沒有聚集索引(它是一個堆heap),那麼SQL Server 使用被稱爲RID作爲查詢值。一個RID是一個8字節的結構,組成比例爲2:4:2字節,其爲文件編號分2字節,4字節分給頁編號,2字節分給槽(slot)編號。雖然RID(和其歷史)很有趣——但它和這裏沒有一點關係(它們是如何工作的,或者它們的架構是怎樣的),但如果當它們存在於一個索引中,我仍然稱其爲RID。

現在,讓我們將這些和一個(或兩個)相對簡單的例子結合起來。

USE WideWorldImporters;
GO
CREATE TABLE Test(
       TestID INT IDENTITY,
       [Name] CHAR(16)
);
GO
CREATE UNIQUE CLUSTERED INDEX TestCL ON Test(TestID);
GO
CREATE INDEX TestName ON Test([Name]);
GO
sp_helpindex Test;
GO

輸出結果如下:

嗯,這看起來是正確的,但是卻有很大的誤導性。索引TestName同時也包含TestID。不是在頁級別,而是在樹幹中(排序目的)。因此真實的應該顯示Name,TestID。但是,如果你加上下面的,會讓你更困惑:

CREATE UNIQUE INDEX TestNameUnique ON Test([Name]);
EXECUTE sp_helpindex Test;
GO

此時,第二、第三索引看起來沒有什麼不同(當然,除了第三索引需要值是唯一的——如描述中所寫)。但是,對於“index_keys”,他們看起來一樣。然而,實際上,它們是不同的(從整個樹的來看)。所以,這就是我傾向於區分“葉”級和非葉級別索引的原因(當我描述它們的時候)。當你將包含列扔進去的時候,會變得更復雜(2005+)。

 

因此,你將如何去區分它們的不同呢?不幸的是,在SQL Server中,沒有工具(或者甚至據我所知的第三方工具),可以通過UI展示這個。但是,你可以使用替換的sp_helpindex開始。《SQL Server 索引優化——sp_helpindex 改寫腳本》一文給出了改寫腳本,更爲詳盡的可以參閱Kimberly各版本sp_helpindex改寫腳本。使用這個,你可以看到輸出的內容更詳細。

輸出展示如下(尤其注意最後兩列)

現在,我們獲得了一些眉目。我們可以明顯區分出兩個索引的不同。一個非唯一的非聚集索引需要將查找值放入樹中。唯一的非聚集索引,則不需要這樣做。

下一步,我們進行更具有挑戰性的例子:

USE WideWorldImporters;
GO
CREATE TABLE Member
(
       MemberNo INT IDENTITY,
       FirstName VARCHAR(30) NOT NULL,
       LastName VARCHAR(30) NOT NULL,
       RegionNo INT
);
GO
CREATE UNIQUE CLUSTERED INDEX MemberCL ON Member(MemberNo);
GO
CREATE INDEX MemberIndex1
ON Member(FirstName,RegionNo,MemberNo)
INCLUDE(LastName);
GO
CREATE INDEX MemberIndex2
ON Member(FirstName,RegionNo)
INCLUDE(LastName);
GO
CREATE INDEX MemberIndex3
ON Member(FirstName,RegionNo)
INCLUDE(MemberNo,LastName);
GO
CREATE UNIQUE INDEX MemberIndex4
ON Member(FirstName,RegionNo)
INCLUDE(MemberNo,LastName);
GO

首先,我們使用sp_helpindex來查看索引情況

EXECUTE sp_helpindex Member;

輸出如下:

單獨看sp_helpindex的結果,看起來第一個非聚集索引和第2、3、4個索引不同,而第2、3、4個索引是相同。實際上並非如此。接下來我們使用改進的sp_helpindex來查看:

EXECUTE sp_helpindex_SQL2016 Member;

從這裏,你可以看到4個非聚集索引的葉節點列是相同的,第4個非聚集索引和其他三個非聚集索引的樹結構有一點不同。最終,非聚集索引1,2,3是相同的,4和其他三個不同。它們的不同之處在哪裏(除非聚集索引4保證唯一性的事實外),超出了該文的範圍。但是,是的,有(在這個案例中相對較小的)不同。因爲我只尋找相同的索引所以只有1 2 3滿足這個要求。

並且,當你的聚集索引有多列,和\或有更復雜的包含列,事情將變得更爲複雜。

話雖如此,如何找到重複的索引呢?

很好……開始時,我用一種簡單的方法讓您使用我改進的sp_helpindex版本的檢查重複索引,但是後來我發現了包含列的一個問題。我已經展示了它們的定義(存儲)架構。但是,從使用上來說,包含列的順序無關緊要。結果兩個擁有不同順序的包含列將變爲兩個不同的索引(技術上,或者存儲上,它們確實不同)。然而確實沒有什麼不同(從使用上來看)。因此我需要寫代碼來調整它(發現真正的重複索引)。

現在,這裏有一些快速的代碼可以讓您更接近它。當我做客於London Immersion Event時,我也寫了一些。然而,在考慮了一些有趣的異常之後,我在這裏對它進行了進一步的調整。這段代碼將發現絕對的重複(結構的順序完全相同)。爲了使用它,你首先需要根據《SQL Server 索引優化——sp_helpindex 改寫腳本》一文創建sp_helpindex_SQL2016過程,然後需要架構名和表名,腳本如下:

IF(OBJECT_ID('tempdb..#FindDupes',N'U')) IS NOT NULL
       DROP TABLE #FindDupes;
GO
CREATE TABLE #FindDupes
(
       index_id INT,
       is_disabled BIT,
       index_name sysname,
       index_description VARCHAR(210),
       index_keys VARCHAR(2126),
       included_columns NVARCHAR(MAX),
       filter_definition NVARCHAR(MAX),
       columns_in_tree NVARCHAR(2126),
       columns_in_leaf NVARCHAR(MAX)
);
GO
DECLARE @SchemaName sysname,
              @TableName sysname,
              @ExecStr NVARCHAR(MAX);
SELECT @SchemaName=N'dbo',@TableName=N'Member';
SELECT @ExecStr='EXECUTE sp_helpindex_SQL2016 '''
                           +QUOTENAME(@SchemaName)
                           +N'.'
                           +QUOTENAME(@TableName)
                           +N'''';
INSERT INTO #FindDupes
EXECUTE(@execStr);
SELECT t1.index_id, COUNT(*) AS 'Duplicate Indexes w/Lower Index_ID',
        N'DROP INDEX '
            + QUOTENAME(@SchemaName, N']')
            + N'.'
            + QUOTENAME(@TableName, N']')
            + N'.'
            + t1.index_name AS 'Drop Index Statement'
FROM #FindDupes AS t1
    INNER JOIN #FindDupes AS t2
        ON t1.columns_in_tree = t2.columns_in_tree
            AND t1.columns_in_leaf = t2.columns_in_leaf
            AND ISNULL(t1.filter_definition, 1) = ISNULL(t2.filter_definition, 1)
            AND PATINDEX('%unique%', t1.index_description) = PATINDEX('%unique%', t2.index_description)
            AND t1.index_id > t2.index_id
GROUP BY t1.index_id, N'DROP INDEX ' + QUOTENAME(@SchemaName, N']')
                    + N'.'
                    + QUOTENAME(@TableName, N']')
                    + N'.' + t1.index_name;
GO

接下來,我們給出發現重複索引的存儲過程,這個過程也是建立在《SQL Server 索引優化——sp_helpindex 改寫腳本》一文中各過程的基礎上,創建如下腳本,先創建sp_helpindex_SQL2016相關過程,腳本如下:

/*============================================================================
  File:     sp_FindDupesIndexes_SQL2016.sql
  摘要: 在一個數據庫中運行它,將獲得該數據庫下的所有重複索引和刪除重複索引的腳本
                     
  Date:     2019.2
  SQL Server 2016 Version
------------------------------------------------------------------------------
 
============================================================================*/
USE master;
go
IF OBJECTPROPERTY(OBJECT_ID('sp_FindDupesIndexes_SQL2016'), 'IsProcedure') = 1
       DROP PROCEDURE sp_FindDupesIndexes_SQL2016;
go
CREATE PROCEDURE [dbo].[sp_FindDupesIndexes_SQL2016]
(
    @ObjName NVARCHAR(776) = NULL        -- 輸入表名,檢驗該表的重複索引
                                        -- 輸入 NULL 檢驗該數據庫所有重複索引
)
AS
SET NOCOUNT ON;
DECLARE @ObjID INT,               -- 表對象編號
              @DBName       sysname,      --數據庫名稱
              @SchemaName sysname, --架構名稱
              @TableName sysname,        --表名
              @ExecStr NVARCHAR(4000);
-- 檢查輸入數據庫是否存在
SELECT @DBName = PARSENAME(@ObjName,3);
IF @DBName IS NULL
    SELECT @DBName = DB_NAME();
ELSE
IF @DBName <> DB_NAME()
    BEGIN
           RAISERROR(15250,-1,-1);
           -- select * from sys.messages where message_id = 15250
           RETURN (1);
    END;
IF @DBName = N'tempdb'
    BEGIN
           RAISERROR('WARNING: This procedure cannot be run against tempdb. Skipping tempdb.', 10, 0);
           RETURN (1);
    END;
-- 檢查架構是否存在,並初始化架構名
SELECT @SchemaName = PARSENAME(@ObjName, 2);
IF @SchemaName IS NULL
    SELECT @SchemaName = SCHEMA_NAME();
-- 檢查表是否存在,並初始化表編號
IF @ObjName IS NOT NULL
BEGIN
    SELECT @ObjID = OBJECT_ID(@ObjName);
       
    IF @ObjID IS NULL
    BEGIN
        RAISERROR(15009,-1,-1,@ObjName,@DBName);
        -- select * from sys.messages where message_id = 15009
        RETURN (1);
    END;
END;
CREATE TABLE #DropIndexes
(
    DatabaseName    sysname,
    SchemaName      sysname,
    TableName       sysname,
    IndexName       sysname,
    DropStatement   NVARCHAR(2000)
);
CREATE TABLE #FindDupes
(
    index_id INT,
       is_disabled BIT,
       index_name sysname,
       index_description VARCHAR(210),
       index_keys NVARCHAR(2126),
    included_columns NVARCHAR(MAX),
       filter_definition NVARCHAR(MAX),
       columns_in_tree NVARCHAR(2126),
       columns_in_leaf NVARCHAR(MAX)
);
-- OPEN CURSOR OVER TABLE(S)
IF @ObjName IS NOT NULL
    DECLARE TableCursor CURSOR LOCAL STATIC FOR
        SELECT @SchemaName, PARSENAME(@ObjName, 1);
ELSE
    DECLARE TableCursor CURSOR LOCAL STATIC FOR                  
        SELECT SCHEMA_NAME(uid), name
        FROM sysobjects
        WHERE TYPE = 'U' --AND name
        ORDER BY SCHEMA_NAME(uid), name;
          
OPEN TableCursor;
FETCH TableCursor
    INTO @SchemaName, @TableName;
-- For each table, list the add the duplicate indexes and save
-- the info in a temporary table that we'll print out at the end.
WHILE @@fetch_status >= 0
BEGIN
    TRUNCATE TABLE #FindDupes;
   
    SELECT @ExecStr = 'EXEC sp_helpindex_SQL2016 '''
                        + QUOTENAME(@SchemaName)
                        + N'.'
                        + QUOTENAME(@TableName)
                        + N'''';
    --SELECT @ExecStr
    INSERT #FindDupes
    EXECUTE (@ExecStr);    
   
    --SELECT * FROM #FindDupes
       
    INSERT #DropIndexes
    SELECT DISTINCT @DBName,
            @SchemaName,
            @TableName,
            t1.index_name,
            N'DROP INDEX '
                + QUOTENAME(@SchemaName, N']')
                + N'.'
                + QUOTENAME(@TableName, N']')
                + N'.'
                + t1.index_name
    FROM #FindDupes AS t1
        JOIN #FindDupes AS t2
            ON
            --t1.columns_in_tree = t2.columns_in_tree
            --    AND t1.columns_in_leaf = t2.columns_in_leaf
                           (PATINDEX('%'+t2.index_keys+'%',t1.index_keys)>0 OR PATINDEX('%'+t1.index_keys+'%',t2.index_keys)>0)
                AND ISNULL(t1.filter_definition, 1) = ISNULL(t2.filter_definition, 1)
                AND PATINDEX('%unique%', t1.index_description) = PATINDEX('%unique%', t2.index_description)
                AND t1.index_id > t2.index_id;
               
    FETCH TableCursor
        INTO @SchemaName, @TableName;
END;
       
DEALLOCATE TableCursor;
-- DISPLAY THE RESULTS
IF (SELECT COUNT(*) FROM #DropIndexes) = 0
           RAISERROR('Database: %s has NO duplicate indexes.', 10, 0, @DBName);
ELSE
    SELECT * FROM #DropIndexes
    ORDER BY SchemaName, TableName;
RETURN (0); -- sp_FindDupesIndexes_SQL2016
go
EXECUTE sys.sp_MS_marksystemobject 'sp_FindDupesIndexes_SQL2016';
go

文章來源,譯Kimberly的博客

如果喜歡,可以掃碼關注SQL Server 公衆號,將有更多精彩內容分享:

                                                                 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章