SQL Server 索引優化——sp_helpindex 改寫腳本

SQL Server 索引優化——sp_helpindex 改寫腳本

 

在索引優化中,我們常常用到sp_helpindex 幫我們查看一個表的索引情況,如下所示

但這些信息很明顯不足夠我們整體深入的剖析一個表的所有索引,因爲索引中有包含列,還有索引篩選,索引頁的存儲等,爲了方便索引分析, 改寫Kimberly Tripp創建的過程,具體操作如下:

第一步:創建sp_ExposeColsInIndexLevels過程,該過程給出了樹和葉的定義,這個過程適合SQL Server 2005/2008/2012/2016/2017 所有版本

/*============================================================================
  文件名:     sp_ExposeColsInIndexLevels
  摘要:  該存儲過程列出了非聚集索引鍵中包含的列,及索引葉包含的列。這依賴於非聚集索引是否唯一,表是否有聚集索引,同時也隨着聚集索引是否唯一而改變
  日期:     2019.1
  版本:    SQL Server 2005/2008/2012/2016/2017
------------------------------------------------------------------------------
  由 Jack zhou 重寫
  參考:https://www.sqlskills.com/blogs/kimberly/use-this-new-sql-server-2012-rewrite-for-sp_helpindex/ 
 
  重寫過程中去掉了大量的循環,同時消除索引鍵列不按順序輸出的問題
============================================================================*/
USE master;
GO
IF OBJECTPROPERTY(OBJECT_ID('sp_ExposeColsInIndexLevels'), 'IsProcedure') = 1
       DROP PROCEDURE sp_ExposeColsInIndexLevels;
GO
CREATE PROCEDURE sp_ExposeColsInIndexLevels
(
       @object_id INT,
       @index_id INT,
       @ColsInTree NVARCHAR(2126) OUTPUT,--非唯一索引樹中的列=非聚集索引的鍵列+不包含在非聚集索引中的聚集索引的鍵列;唯一索引樹=非聚集索引的鍵列+聚集索引的鍵列
       @ColsInLeaf NVARCHAR(MAX) OUTPUT  --非唯一索引葉中的列=非聚集索引的鍵列+聚集索引的鍵列+包含列;唯一索引葉=非聚集索引的鍵列+包含列+不在非聚集索引和包含列種的聚集索引的鍵列
)
AS
BEGIN
       DECLARE @nonclus_uniq INT
                     , @column_id INT
                     , @column_name NVARCHAR(260)
                     , @col_descending BIT
                     , @colstr     NVARCHAR (MAX);
       --declare @max_clKey_Ordinal int
       --            ,@max_nonclKey_Ordinal int
       -- 獲取聚集索引鍵的 (id and name)
       SELECT sic.key_ordinal,sic.column_id
              , QUOTENAME(sc.name, '[') AS column_name 
              , is_descending_key
       INTO #clus_keys
       FROM sys.index_columns AS sic
              JOIN sys.columns AS sc
                     ON sic.column_id = sc.column_id AND sc.OBJECT_ID = sic.OBJECT_ID
       WHERE sic.[object_id] = @object_id
       AND [index_id] = 1;

       -- 獲取非聚集索引鍵
       SELECT sic.key_ordinal,sic.column_id, sic.is_included_column
              , QUOTENAME(sc.name, '[') AS column_name 
              , is_descending_key
       INTO #nonclus_keys
       FROM sys.index_columns AS sic
              JOIN sys.columns AS sc
                     ON sic.column_id = sc.column_id
                           AND sc.OBJECT_ID = sic.OBJECT_ID
       WHERE sic.[object_id] = @object_id
              AND sic.[index_id] = @index_id;
      
       -- Is the nonclustered unique?
       SELECT @nonclus_uniq = is_unique
       FROM sys.indexes
       WHERE [object_id] = @object_id
              AND [index_id] = @index_id;
       IF (@nonclus_uniq = 0)
       BEGIN
              -- Case 1: nonunique nonclustered index
              SELECT @colstr=STUFF((
                     SELECT column_name+CASE WHEN is_descending_key=1 THEN '(-)' ELSE N'' END +N','
                     FROM #nonclus_keys
                     WHERE is_included_column=0
                     ORDER BY key_ordinal
                     FOR XML PATH(''),TYPE).value('.','varchar(max)'),1,0,'');
              
              -- 如果有聚集索引,則包含聚集鍵
              SELECT @colstr=ISNULL(@colstr,N'')+
                     ISNULL(STUFF((
                           SELECT column_name+CASE WHEN is_descending_key=1 THEN '(-)' ELSE N'' END +N','
                           FROM #clus_keys
                           WHERE column_id NOT IN (SELECT column_id FROM #nonclus_keys
                                                                     WHERE is_included_column = 0)
                           ORDER BY key_ordinal
                           FOR XML PATH(''),TYPE).value('.','varchar(max)'),1,0,''),'');
              
              SELECT @ColsInTree = SUBSTRING(@colstr, 1, LEN(@colstr) -1);
                     
              -- find columns not in the nc and not in cl - that are still left to be included.
              SELECT @colstr=ISNULL(@colstr,N'')+
                     ISNULL(STUFF((
                           SELECT  column_name FROM #nonclus_keys
                           WHERE column_id NOT IN (SELECT column_id FROM #clus_keys UNION SELECT column_id FROM #nonclus_keys WHERE is_included_column = 0)
                           FOR XML PATH(''),TYPE).value('.','varchar(max)'),1,0,''),'');
              SELECT @ColsInLeaf = @colstr;
              
       END;
       -- Case 2: unique nonclustered
       ELSE
       BEGIN
              -- cursor over nonclus_keys that are not includes
              SELECT @colstr = '';
              SELECT @colstr=STUFF((
                     SELECT column_name+CASE WHEN is_descending_key=1 THEN '(-)' ELSE N'' END +N','
                     FROM #nonclus_keys
                     WHERE is_included_column=0
                     ORDER BY key_ordinal
                     FOR XML PATH(''),TYPE).value('.','varchar(max)'),1,0,'');
              
              SELECT @ColsInTree = SUBSTRING(@colstr, 1, LEN(@colstr) -1);
       
              -- start with the @ColsInTree and add remaining columns not present...
              SELECT @colstr=ISNULL(@colstr,N'')+
                     ISNULL(STUFF((
                           SELECT column_name+CASE WHEN is_descending_key=1 THEN '(-)' ELSE N'' END +N','
                           FROM #nonclus_keys
                           WHERE is_included_column = 1
                           FOR XML PATH(''),TYPE).value('.','varchar(max)'),1,0,''),'');
              -- get remaining clustered column as long as they're not already in the nonclustered
              SELECT @colstr=ISNULL(@colstr,N'')+
                     ISNULL(STUFF((
                           SELECT column_name+CASE WHEN is_descending_key=1 THEN '(-)' ELSE N'' END +N','
                           FROM #clus_keys
                           WHERE column_id NOT IN (SELECT column_id FROM #nonclus_keys)
                           FOR XML PATH(''),TYPE).value('.','varchar(max)'),1,0,''),'');
              SELECT @ColsInLeaf = SUBSTRING(@colstr, 1, LEN(@colstr) -1);
              SELECT @colstr = '';
       
       END;
       -- Cleanup
       DROP TABLE #clus_keys;
       DROP TABLE #nonclus_keys;
       
END;
GO
EXECUTE sys.sp_MS_marksystemobject 'sp_ExposeColsInIndexLevels';
GO

第二步,創建sp_helpindex_SQL2016.sql

    Kimberly Tripp給出了2005~2012的創建腳本,這裏對其進行簡單調整給出2016的腳本。爲方便使用,將其命名改爲sp_helpindex_SQL2016

/*============================================================================
  文件:     sp_helpindex_SQL2016.sql

  摘要:  這是對sp_helpindex的修改,修改後的sp_helpindex_SQL2016展示列包含列,過濾列信息,同時給出列索引樹和葉所包含的列。
         展示索引是否被廢棄

            2012 Version: Added columnstore indexes to the output
                        
  日期:     2019.1

  版本:SQL Server 2016
------------------------------------------------------------------------------
由Jack zhou 改寫

參考:https://www.sqlskills.com/blogs/kimberly/use-this-new-sql-server-2012-rewrite-for-sp_helpindex/ 的 sp_SQLskills_SQL2012_helpindex.sql
============================================================================*/

USE [master];
GO

IF OBJECTPROPERTY(OBJECT_ID(N'sp_helpindex_SQL2016')
        , N'IsProcedure') = 1
    DROP PROCEDURE sp_helpindex_SQL2016;
GO

SET ANSI_NULLS ON;
SET QUOTED_IDENTIFIER ON;
GO

CREATE PROCEDURE [dbo].[sp_helpindex_SQL2016]
    @objname nvarchar(776)        -- the table to check for indexes
as


    set nocount on

    declare @objid int,                -- the object id of the table
            @indid smallint,        -- the index id of an index
            @type tinyint,            -- the index type
            @groupid int,              -- the filegroup id of an index
            @indname sysname,
            @groupname sysname,
            @status int,
            @keys nvarchar(2126),    --Length (16*max_identifierLength)+(15*2)+(16*3)
            @inc_columns nvarchar(max),
            @inc_Count smallint,
            @loop_inc_Count smallint,
            @dbname    sysname,
            @ignore_dup_key    bit,
            @is_unique bit,
            @is_hypothetical bit,
            @is_primary_key    bit,
            @is_unique_key bit,
            @is_disabled bit,
            @auto_created bit,
            @no_recompute bit,
            @filter_definition nvarchar(max),
            @ColsInTree nvarchar(2126),
            @ColsInLeaf nvarchar(max)

    -- Check to see that the object names are local to the current database.
    select @dbname = parsename(@objname,3)
    if @dbname is null
        select @dbname = db_name()
    else if @dbname <> db_name()
        begin
            raiserror(15250,-1,-1)
            return (1)
        end

    -- Check to see the the table exists and initialize @objid.
    select @objid = object_id(@objname)
    if @objid is NULL
    begin
        raiserror(15009,-1,-1,@objname,@dbname)
        return (1)
    end

    -- OPEN CURSOR OVER INDEXES (skip stats: bug shiloh_51196)
    declare ms_crs_ind cursor local static for
        select i.index_id, i.[type], i.data_space_id, QUOTENAME(i.name, N']') AS name,
            i.ignore_dup_key, i.is_unique, i.is_hypothetical, i.is_primary_key, i.is_unique_constraint,
            s.auto_created, s.no_recompute, i.filter_definition, i.is_disabled
        from sys.indexes as i
            join sys.stats as s
                on i.object_id = s.object_id
                    and i.index_id = s.stats_id
        where i.object_id = @objid
    open ms_crs_ind
    fetch ms_crs_ind into @indid, @type, @groupid, @indname, @ignore_dup_key, @is_unique, @is_hypothetical,
            @is_primary_key, @is_unique_key, @auto_created, @no_recompute, @filter_definition, @is_disabled

    -- IF NO INDEX, QUIT
    if @@fetch_status < 0
    begin
        deallocate ms_crs_ind
        raiserror(15472,-1,-1,@objname) -- Object does not have any indexes.
        return (0)
    end

    -- create temp tables
    CREATE TABLE #spindtab
    (
        index_name            sysname    collate database_default NOT NULL,
        index_id            int,
        [type]                tinyint,
        ignore_dup_key        bit,
        is_unique            bit,
        is_hypothetical        bit,
        is_primary_key        bit,
        is_unique_key        bit,
        is_disabled         bit,
        auto_created        bit,
        no_recompute        bit,
        groupname            sysname collate database_default NULL,
        index_keys            nvarchar(2126)    collate database_default NULL, -- see @keys above for length descr
        filter_definition    nvarchar(max),
        inc_Count            smallint,
        inc_columns            nvarchar(max),
        cols_in_tree        nvarchar(2126),
        cols_in_leaf        nvarchar(max)
    )

    CREATE TABLE #IncludedColumns
    (    RowNumber    smallint,
        [Name]    nvarchar(128)
    )

    -- Now check out each index, figure out its type and keys and
    --    save the info in a temporary table that we'll print out at the end.
    while @@fetch_status >= 0
    begin
        -- First we'll figure out what the keys are.
        declare @i int, @thiskey nvarchar(131) -- 128+3

        select @keys = QUOTENAME(index_col(@objname, @indid, 1), N']'), @i = 2
        if (indexkey_property(@objid, @indid, 1, 'isdescending') = 1)
            select @keys = @keys  + '(-)'

        select @thiskey = QUOTENAME(index_col(@objname, @indid, @i), N']')
        if ((@thiskey is not null) and (indexkey_property(@objid, @indid, @i, 'isdescending') = 1))
            select @thiskey = @thiskey + '(-)'

        while (@thiskey is not null )
        begin
            select @keys = @keys + ', ' + @thiskey, @i = @i + 1
            select @thiskey = QUOTENAME(index_col(@objname, @indid, @i), N']')
            if ((@thiskey is not null) and (indexkey_property(@objid, @indid, @i, 'isdescending') = 1))
                select @thiskey = @thiskey + '(-)'
        end

        -- Second, we'll figure out what the included columns are.
        select @inc_columns = NULL
        
        SELECT @inc_Count = count(*)
        FROM sys.tables AS tbl
        INNER JOIN sys.indexes AS si
            ON (si.index_id > 0
                and si.is_hypothetical = 0)
                AND (si.object_id=tbl.object_id)
        INNER JOIN sys.index_columns AS ic
            ON (ic.column_id > 0
                and (ic.key_ordinal > 0 or ic.partition_ordinal = 0 or ic.is_included_column != 0))
                AND (ic.index_id=CAST(si.index_id AS int) AND ic.object_id=si.object_id)
        INNER JOIN sys.columns AS clmns
            ON clmns.object_id = ic.object_id
            and clmns.column_id = ic.column_id
        WHERE ic.is_included_column = 1 and
            (si.index_id = @indid) and
            (tbl.object_id= @objid)

        IF @inc_Count > 0
        BEGIN
            DELETE FROM #IncludedColumns
            INSERT #IncludedColumns
                SELECT ROW_NUMBER() OVER (ORDER BY clmns.column_id)
                , clmns.name
                FROM
                sys.tables AS tbl
                INNER JOIN sys.indexes AS si
                    ON (si.index_id > 0
                        and si.is_hypothetical = 0)
                        AND (si.object_id=tbl.object_id)
                INNER JOIN sys.index_columns AS ic
                    ON (ic.column_id > 0
                        and (ic.key_ordinal > 0 or ic.partition_ordinal = 0 or ic.is_included_column != 0))
                        AND (ic.index_id=CAST(si.index_id AS int) AND ic.object_id=si.object_id)
                INNER JOIN sys.columns AS clmns
                    ON clmns.object_id = ic.object_id
                    and clmns.column_id = ic.column_id
                WHERE ic.is_included_column = 1 and
                    (si.index_id = @indid) and
                    (tbl.object_id= @objid)
            
            SELECT @inc_columns = QUOTENAME([Name], N']') FROM #IncludedColumns WHERE RowNumber = 1

            SET @loop_inc_Count = 1

            WHILE @loop_inc_Count < @inc_Count
            BEGIN
                SELECT @inc_columns = @inc_columns + ', ' + QUOTENAME([Name], N']')
                    FROM #IncludedColumns WHERE RowNumber = @loop_inc_Count + 1
                SET @loop_inc_Count = @loop_inc_Count + 1
            END
        END
    
        select @groupname = null
        select @groupname = name from sys.data_spaces where data_space_id = @groupid

        -- Get the column list for the tree and leaf level, for all nonclustered indexes IF the table has a clustered index
        IF @indid = 1 AND (SELECT is_unique FROM sys.indexes WHERE index_id = 1 AND object_id = @objid) = 0
            SELECT @ColsInTree = @keys + N', UNIQUIFIER', @ColsInLeaf = N'All columns "included" - the leaf level IS the data row, plus the UNIQUIFIER'
            
        IF @indid = 1 AND (SELECT is_unique FROM sys.indexes WHERE index_id = 1 AND object_id = @objid) = 1
            SELECT @ColsInTree = @keys, @ColsInLeaf = N'All columns "included" - the leaf level IS the data row.'
        
        IF @indid > 1 AND (SELECT COUNT(*) FROM sys.indexes WHERE index_id = 1 AND object_id = @objid) = 1
        exec sp_ExposeColsInIndexLevels @objid, @indid, @ColsInTree OUTPUT, @ColsInLeaf OUTPUT
        
        IF @indid > 1 AND @is_unique = 0 AND (SELECT is_unique FROM sys.indexes WHERE index_id = 1 AND object_id = @objid) = 0
            SELECT @ColsInTree = @ColsInTree + N', UNIQUIFIER', @ColsInLeaf = @ColsInLeaf + N', UNIQUIFIER'
        
        IF @indid > 1 AND @is_unique = 1 AND (SELECT is_unique FROM sys.indexes WHERE index_id = 1 AND object_id = @objid) = 0
            SELECT @ColsInLeaf = @ColsInLeaf + N', UNIQUIFIER'
        
        IF @indid > 1 AND (SELECT COUNT(*) FROM sys.indexes WHERE index_id = 1 AND object_id = @objid) = 0 -- table is a HEAP
        BEGIN
            IF (@is_unique_key = 0)
                SELECT @ColsInTree = @keys + N', RID'
                    , @ColsInLeaf = @keys + N', RID' + CASE WHEN @inc_columns IS NOT NULL THEN N', ' + @inc_columns ELSE N'' END
        
            IF (@is_unique_key = 1)
                SELECT @ColsInTree = @keys
                    , @ColsInLeaf = @keys + N', RID' + CASE WHEN @inc_columns IS NOT NULL THEN N', ' + @inc_columns ELSE N'' END
        END
            
        -- INSERT ROW FOR INDEX
        
        insert into #spindtab values (@indname, @indid, @type, @ignore_dup_key, @is_unique, @is_hypothetical,
            @is_primary_key, @is_unique_key, @is_disabled, @auto_created, @no_recompute, @groupname, @keys, @filter_definition, @inc_Count, @inc_columns, @ColsInTree, @ColsInLeaf)

        -- Next index
        fetch ms_crs_ind into @indid, @type, @groupid, @indname, @ignore_dup_key, @is_unique, @is_hypothetical,
            @is_primary_key, @is_unique_key, @auto_created, @no_recompute, @filter_definition, @is_disabled
    end
    deallocate ms_crs_ind

    -- DISPLAY THE RESULTS
    
    select
        'index_id' = index_id,
        'is_disabled' = is_disabled,
        'index_name' = index_name,
        'index_description' = convert(varchar(210), --bits 16 off, 1, 2, 16777216 on, located on group
                case when index_id = 1 and type = 1 then 'clustered'
                    when index_id > 1 and type = 6 then 'nonclustered columnstore'
                    when index_id > 1 and type = 2 then 'nonclustered'
                    when index_id > 1 and type = 3 then 'XML'
                    when index_id > 1 and type = 4 then 'Spatial'
                    when index_id > 1 and type = 5 then 'Clustered columnstore index'
                    when index_id > 1 and type = 7 then 'Nonclustered hash index'
                    else 'new index type' end
                + case when ignore_dup_key <>0 then ', ignore duplicate keys' else '' end
                + case when is_unique=1 then ', unique' else '' end
                + case when is_hypothetical <>0 then ', hypothetical' else '' end
                + case when is_primary_key <>0 then ', primary key' else '' end
                + case when is_unique_key <>0 then ', unique key' else '' end
                + case when auto_created <>0 then ', auto create' else '' end
                + case when no_recompute <>0 then ', stats no recompute' else '' end
                + ' located on ' + groupname),
        'index_keys' =
            case when type in(5,6,7) then 'n/a, see columns_in_leaf for details'
            else index_keys end,
        'included_columns' =
            case when type in(5,6,7) then 'n/a, columnstore index'
            else inc_columns end,
        'filter_definition' =
            case when type in(5,6,7) then 'n/a, columnstore index'
            else filter_definition end,
        'columns_in_tree' =
            case when type in(5,6,7) then 'n/a, columnstore index'
            else cols_in_tree end,
        'columns_in_leaf' =
            case when type in(5,6,7) then 'Columns with column-based index: ' + cols_in_leaf
            else cols_in_leaf end

    from #spindtab
    order by index_id

    return (0) -- sp_helpindex_SQL2016
go

exec sys.sp_MS_marksystemobject 'sp_helpindex_SQL2016'
go

第三步,設置快捷鍵

通過第二步更改名稱後,這個過程就更容易記住,加上智能輸入提示,基本不用快捷鍵,也能很快輸入。這裏仍然將快捷鍵的設置方法說明一下:

在SSMS的工具→選項→環境→鍵盤,在Ctrl+F1的存儲過程中輸入sp_helpindex_SQL2016

點擊確定,重啓SSMS,即可以使用Ctrl+F1快捷鍵查看索引信息了,下面使用快捷鍵Ctrl+F1查看數據庫WideWorldImporters 中Sales架構下的Invoices表中的所有索引信息,輸入表名,可以包括數據庫名和架構名,選中表名,同時按先Ctrl和F1鍵,結果如下:

使用如下腳本,可以獲得和上面一致的結果:

exec sp_helpindex_SQL2016 'WideWorldImporters.[Sales].[Invoices]'

這樣,修改後的sp_helpindex已經完全覆蓋一個所有索引的基本信息,這對於後續的索引優化將很有幫助。

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

                                                                 

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