SQL Server索引管理——索引創建建議和經驗(四)

SQL Server索引管理——索引創建建議和經驗(四)

在前文的基礎上,本文將闡述一些高級的索引技術。

高級索引技術

  • 覆蓋索引

  • 索引交叉:使用多個非聚集索引滿足單個查詢需要的所有列

  • 索引連接: 使用索引交叉和覆蓋索引技術避免使用基礎表

  • 過濾索引:爲在奇異分佈或稀疏字段上創建索引,可以在索引上應用過濾,使得僅僅爲某些數據創建索引

  • 索引視圖:這實現磁盤數據的視圖輸出

覆蓋索引

創建測試表和測試數據

CREATE TABLE Member
(
       MemberNo INT IDENTITY,
       FirstName VARCHAR(30)  NULL,
       LastName VARCHAR(30)  NULL,
       EmailPromotion INT
);
INSERT INTO Member(FirstName,LastName,EmailPromotion)
SELECT FirstName,LastName,EmailPromotion
FROM Person.Person;
CREATE CLUSTERED INDEX clx_Member_No ON Member(MemberNo);
CREATE NONCLUSTERED INDEX ix_Member_FirstName ON Member(FirstName);

執行如下腳本,並查看執行計劃和邏輯讀情況

SET STATISTICS IO ON;
SELECT
       LastName,EmailPromotion
FROM Member
WHERE FirstName='Lolan';

由於非聚集索引ix_Member_FirstName僅僅包含FirstName列,沒有包含在索引中查詢需要的數據需要從表中(聚集索引的葉)中獲取,索引查詢計劃使用了鍵查找獲取數據。修改上面的非聚集索引,使其包含所有列,如下:

CREATE NONCLUSTERED INDEX ix_Member_FirstName ON Member(FirstName) INCLUDE(LastName,EmailPromotion)
WITH(DROP_EXISTING=ON);

再執行查詢,並查看結果:

可以看到,查詢計劃變爲簡單索引查找,邏輯讀次數由原來的6次變爲2次

 

INCLUDE最好的使用情景:

  • 你不想增加索引鍵的大小,但是你想要索引變爲覆蓋索引

  • 你想在一個不能創建索引的列上創建索引(text, ntext 和 images)

  • 你已經超出了索引鍵列的最大列數(這種情況最好避免)

覆蓋索引可以看做僞聚集索引

覆蓋索引以序列順序物理的組織所有索引數據。從磁盤I/O方面看,一個不使用包含列的覆蓋索引,對於索引的列能完全滿足查詢情景來說,其變爲一個聚集索引。如果查詢結果集需要排序輸出,那麼覆蓋索引可以按照結果集的需求,維持列的物理順序和其一致。

建議

爲利用覆蓋索引的優點,注意SELECT後面的結果列。儘可能保證覆蓋索引鍵列儘可能的小。使用INCLUDE增加列變得很有意義。因爲覆蓋索引包含查詢中的所有列,它可能非常寬,增加維護覆蓋索引的成本。你必須平衡維護成本和覆蓋索引帶來的性能優化。如果索引中所有列的字節數少於表中一行數據的字節,並且你確定採用覆蓋索引的查詢頻繁的被執行,那麼使用覆蓋索引將非常有意義。

在創建大量的覆蓋索引之前,考慮SQL Server 怎樣有效的、自動的使用交叉索引創建覆蓋索引使得查詢飛起來。

交叉索引

如果表有多個索引,那麼SQL Server 可能使用多個索引執行查詢。SQl Server 可以利用多個索引的優點,選擇基於每個索引較小的數據集,然後執行兩個子集的交叉(即返回滿足所有準則的行)。SQL Server可以利用一個表上的多個索引,然後使用join算法獲得兩個表的交集。

在如下的查詢語句中,WHERE條件列 SalesPersonID上沒有非聚集索引,OrderDate列上沒有索引:

SELECT *
FROM Sales.SalesOrderHeader
WHERE  OrderDate BETWEEN '2011-06-04' AND '2011-07-07'
       AND SalesPersonID=276;

在OrderDate字段上創建非聚集索引,腳本如下:

CREATE NONCLUSTERED INDEX IX_test ON Sales.SalesOrderHeader(OrderDate);

再執行上述查詢腳本,並檢查執行計劃和I/O情況:

正如你所看到的,SQL Server 使用了兩個非聚集索引查找(而不是掃描),並且使用了交叉算法獲得兩個子集的交集。然後在從前兩子集的交集中,進行鍵查找,檢索其餘不包含在索引中的數據。從邏輯的次數來看,交叉查找顯著提升了性能(聚集索引掃描邏輯查找698次,交叉索引僅用了34次)。

       爲改進查詢性能,SQL Server 可以使用表中的多個索引。因此,可以考慮創建多個窄索引替代創建的寬索引。當需要的時候,SQL Server可以一起使用它們。當不需要的時候,使用窄索引的查詢也會受益。當創建覆蓋索引時,確定索引的寬度是否被接受,是否使用包含列就可以完成任務。如果不行,找出已經存儲的包含需要的覆蓋索引的大部分列的非聚集索引。如果可能,重新組織已經存在的非聚集索引列的適當的順序,運行優化器考慮使用兩個非聚集索引的交叉。

       有時,可能因爲如下原因,你必須創建獨立的非聚集索引:

  • 不允許調整已經存在索引中列的順序

  • 某些覆蓋索引所需要的列不存在於已經存在的非聚集索引中

  • 兩個已經存在的非聚集索引列的總和超出覆蓋聚集索引所需要的列

這種情形下,你可以在其餘的列上創建一個非聚集索引。如果新索引的組合列順序和存在的非聚集索引滿足覆蓋索引需求,優化器將能夠適應交叉索引。辨識新索引的列和它們的順序,關注其他查詢,儘量最大化索引的效用。

Index Joins

索引連接是索引交叉的一個變化,是覆蓋索引在交叉索引上的應用。如果沒有單一的索引覆蓋查詢所有的列,但多個索引結合起來可以覆蓋查詢的所有列,SQL Server可以使用索引連接的方式完全滿足查詢,而不去檢索基礎表

讓我們在實踐中檢驗這個索引技術。將“索引交叉”的查詢像這樣修改:

USE AdventureWorks2016CTP3
GO
SET STATISTICS IO ON;
SELECT SalesPersonID,OrderDate
FROM Sales.SalesOrderHeader
WHERE  OrderDate BETWEEN '2011-06-04' AND '2011-07-07'
       AND SalesPersonID=276;

其查詢計劃和讀如下:

從執行計劃來看,優化器沒有使用列SalesPersonId 上存在的非聚集索引。因爲查詢還需要OrderDate列值,優化器選擇聚集索引檢索查詢所需要的所有列值。如果我們在OrderDate列上創建像下面的索引:

CREATE NONCLUSTERED INDEX IX_test ON Sales.SalesOrderHeader(OrderDate);

再執行上述查詢腳本,查看執行計劃和讀如下:

兩個索引聯合,就像一個覆蓋索引,使用兩個索引查找操作連接一起,而不再使用聚集索引,將表的讀從698降低到4

索引過濾

過濾索引是使用過濾器的非聚集索引,基於WHERE語句,在列或列集上創建高選擇性索引,不進行篩選,可能不具有高選擇性。例如,一個擁有大量NULL值的列,可能以稀疏列的形式存儲,以減少NULL值的消耗。在這樣的列上增加過濾索引,允許你擁有一個非NULL數據的可用索引。理解這個的最好方式是看其在實踐中應用。

Sales.SalesOrderHeader 表中有3萬多條記錄,其中PurchaseOrderNumber 和SalesPersonId列均有27000多個NULL值,如下面的查詢結果:

USE AdventureWorks2016CTP3
GO
SELECT COUNT(1),COUNT(PurchaseOrderNumber),COUNT(SalesPersonID)
FROM Sales.SalesOrderHeader;

如果你想要獲得簡單購買訂單數量列表,可能使用如下的腳本:

SELECT
       PurchaseOrderNumber
       ,OrderDate
       ,ShipDate
       ,SalesPersonID
FROM Sales.SalesOrderHeader
WHERE PurchaseOrderNumber LIKE 'PO5%'
       AND SalesPersonID IS NOT NULL;

爲解決這個問題,可能創建一個索引,包含查詢結果的某些列,如下:

CREATE INDEX IX_Test ON Sales.SalesOrderHeader(PurchaseOrderNumber,SalesPersonID)
INCLUDE(OrderDate,ShipDate)
WITH(DROP_EXISTING=ON);

當你返回執行查詢的時候,發現性能改進非常明顯,如下所示:

正如你所看到的,邏輯讀從696次降低到5次,通常,這已經足夠了。假設這個查詢被頻繁調用,非常頻繁。現在你能從中擠出來的每個位速度提升,都將給你帶來紅利。我們知道,索引列中的很多數據都是NULL,你可以調整索引,過濾掉NULL值,這些NULL值不再在索引中,減少索引樹的大小,因此減少了搜索:

CREATE INDEX IX_Test ON Sales.SalesOrderHeader(PurchaseOrderNumber,SalesPersonID)
INCLUDE(OrderDate,ShipDate)
WHERE PurchaseOrderNumber IS NOT NULL
       AND SalesPersonID IS NOT NULL
WITH(DROP_EXISTING=ON);

儘管從絕對數上來看,讀從5降到4不是很多,但從比例上看,其減少了查詢I/O消耗的20%,如果這個查詢每分鐘運行成百上千次,20%的降低將是很大的提升。

過濾索引在如下多種情景下應用會有較好的效果:

  • 通過減少索引的大小改進性能

  • 創建較小的索引,減少存儲成本

  • 因爲減少索引大小而降低索引維護的消耗

第一個過濾索引應用的地方像前的例子一樣,從索引中排除NULL值。你也可以將頻繁讀取的數據集獨立出來,這樣針對這部分的查詢將快得多。你可以使用WHERE語句以類似於創建索引視圖的方式過濾數據,創建覆蓋的過濾索引,而沒有維護索引視圖那些頭痛的事情,和上面的例子一樣。

       當讀取或創建過濾索引時,它們需要特定ANSI設定集:

  • ON:ANSI_NULLS(ON時NULL值只能使用IS NULL、IS NOT NULL 判斷,否則可以使用=、<>、!=判斷),ANSI_PADDING,ANSI_WARNINGS,ARITHABORT,CONCAT_NULL_YIELDS_NULL,QUOTED_IDENTIFIER(關鍵字使用雙引號括起來,字符使用單引號)

  • OFF:NUMERIC_ROUNDABOART

索引視圖

創建有聚集索引的視圖,在視圖上創建聚集索引後,還可以創建其他非聚集索引。普通視圖是虛擬的,不佔用存儲的;索引視圖會在創建聚集索引時物化視圖。

適合於數據倉庫、統計庫等OLAP數據庫中的彙總查詢,不適合OLTP系統,因爲原表的數據變化,將引起索引視圖同步更新;位於原表中索引視圖的聚集索引鍵列的更新,會引起索引視圖及所有索引的更新,這些在事務系統中都是重大的開銷。

USE AdventureWorks2016CTP3
GO
IF EXISTS(SELECT *
                FROM sys.VIEWS
                WHERE [object_id]=OBJECT_ID(N'[Purchasing].[IndexedView]'))
       DROP VIEW [Purchasing].[IndexedView];
GO
CREATE VIEW Purchasing.IndexdView
WITH schemabinding
AS
SELECT
       ProductID
       ,SUM(OrderQty) OrderQty
       ,SUM(ReceivedQty) ReceivedQty
       ,SUM(RejectedQty) RejectedQty
       ,COUNT_BIG(*) [Count]
FROM Purchasing.PurchaseOrderDetail
GROUP BY ProductID;
GO
CREATE UNIQUE CLUSTERED INDEX iv ON [Purchasing].[IndexdView](ProductId);

索引壓縮

數據和索引壓縮是在SQL Server 2008版本引入的(企業版、開發板可用)。壓縮索引意味着在一頁上有更多的鍵信息。這可能引起一系列的性能改進,因爲壓縮後只需要較少的索引頁、索引級別存儲索引。當索引中的鍵值被壓縮或者解壓時,會有CPU、內存消耗,因此這個方案可能不適合所有索引。

       默認情況下索引是不壓縮的。當需要壓縮索引時,需要在創建索引時顯示的指定索引的壓縮屬性。有兩個類型的壓縮:行級、頁級。在頁級別的壓縮中索引的非葉頁不能壓縮。爲了看其表現,考慮如下索引:

--測試1.默認無壓縮
CREATE NONCLUSTERED INDEX IX_Test
ON Person.Address([City],[PostalCode])
--測試2.行壓縮
CREATE NONCLUSTERED INDEX IX_Comp_Test
ON Person.Address([City],[PostalCode])
WITH(data_compression=ROW);
--測試3.頁壓縮
CREATE NONCLUSTERED INDEX IX_Comp_Page_Test
ON Person.Address([City],[PostalCode])
WITH(data_compression=Page);

使用如下腳本查看三個索引的頁數情況

SELECT
       i.name
       ,i.type_desc
       ,s.page_count
       ,s.record_count
       ,s.index_level
       ,compressed_page_count
FROM sys.indexes i
       JOIN sys.dm_db_index_physical_stats(DB_ID(N'AdventureWorks2016CTP3')
              ,OBJECT_ID(N'Person.Address'),NULL,NULL,'DETAILED') AS s
       ON i.index_id=s.index_id
WHERE i.[object_id]=OBJECT_ID(N'Person.Address');

結果如下:

可以看到,索引數據頁從106(無壓縮)→63(行壓縮)→25(頁壓縮)

爲查看壓縮索引對性能的影響,我們使用如下查詢

SELECT
       a.City,a.PostalCode
FROM Person.Address AS a
WHERE a.City='Newton'
       AND a.PostalCode='V2M1N7';

當三個索引都存在時,執行上述查詢,並查看執行計劃和讀的情況如下:

刪除頁壓縮的索引

DROP INDEX IX_Comp_Page_Test ON Person.Address;

再執行查詢,並查看執行計劃和讀的情況:

刪除行壓縮索引,

DROP INDEX IX_Comp_Test ON Person.Address;

再執行查詢,並查看邏輯讀如下:

比較無壓縮、行壓縮、頁壓縮三種索引的邏輯讀情形,可以看到,無壓縮時,邏輯讀最多;行壓縮邏輯讀次之;頁壓縮邏輯讀最少。

如果喜歡,可以搜索關注 MSSQLServer 公衆號,將有更多精彩內容分享:

                                                                 

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