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 公衆號,將有更多精彩內容分享: