SQL Server索引的維護 - 索引碎片、填充因子 <第三篇>

  實際上,索引的維護主要包括如下兩個方面:html

  •   頁拆分
  •   碎片

  這兩個問題都和頁密度有關,雖然二者的表現形式在本質上有所區別,可是故障排除工具是同樣的,由於處理是相同的。sql

  對於很是小的表(比64KB小得多),一個區中的頁面可能屬於多餘一個的索引或表---這被稱爲混合區。若是數據庫中有太多的小表,混合區幫助SQL Server節約磁盤空間。數據庫

  隨着表(或索引)增加而且請求超過8個頁面,SQL Server建立專用於該表(或索引)的區而且從該區中分配頁面。這樣一個區被稱爲統一區,它能夠爲多達8個相同表或索引的頁面請求服務。併發

1、碎片

  當數據庫增加,頁拆分,而後刪除數據時,就會產生碎片。從增加的方面看,平衡樹處理得很不錯。可是對於刪除方面,它並無太大的做用。最終可能會出現這種狀況,一個頁上有一條記錄,而另外一個頁上有幾個記錄。在這種狀況下,一個頁上保存的數據量只是它可以保存總數據量的一小部分。app

  一、碎片會形成空間的浪費,SQL Server每次會分配一個區段,若是一個頁上只有一條記錄,則仍然會分配整個區段。函數

  二、散佈在各處的數據會形成數據檢索時的額外系統開銷。爲了獲取須要的10行記錄,SQL Server不是隻加載一個頁,而是可能必須加載10個頁來獲取相同的信息。並不僅是讀取行致使了這一結果,在讀取行前,SQL Server必須先讀取頁。更多的頁意味着更多的工做量。工具

  可是碎片也不僅是有壞處,好比一個插入很是頻繁的表就很喜歡碎片,由於在插入數據時幾乎不用擔憂頁拆分的問題。因此大量的碎片意味着較差的讀取性能,但也意味着極好的插入性能。性能

  關於碎片的理解,找到了數據庫牛人CareySon的這篇文章 T-SQL查詢高級—SQL Server索引中的碎片和填充因子 ,在這裏消化一下
ui

  碎片分兩種,外部碎片和內部碎片this

  外部碎片:

  外部碎片指的是頁拆分而產生的碎片。如向表中插入一行,而這一行致使現有的頁空間沒法容納新插入的行,則致使頁拆分。

  

  新的頁不斷隨數據的增加而產生,而彙集索引要求行之間連續,因此若是彙集索引不是自增列,頁拆分後和原來的頁在磁盤上並不連續-這就是外部碎片。
  因爲頁拆分,致使數據在頁之間的移動,因此若是插入更新等操做常常須要分頁,則會大大消耗IO資源,形成性能降低。
  對於查找連說,在有特定搜索條件,如where子句有很細的限制或者返回無序結果集時,外部碎片並不會對性能產生影響。但若是要返回掃描彙集索引並且查找連續頁面時,外部碎片就會產生性能上的影響。因此當要讀取相同的數連續的數據時須要掃描更多的頁,更多的區。並且連續數據不能預讀,形成額外的物理讀,增長磁盤IO。一般,外部碎片過多會形成頻繁的區切換。

  若是頁面連續排序,預讀功能能夠提早讀取頁面而不須要太多的磁頭移動。

  內部碎片:  

  內部碎片是頁拆分後,致使索引頁的數據並不滿,有空行。一樣讀取一個索引頁,卻只能拿到x%的數據。

--新建一張表
CREATE TABLE Person
(
    Id    int,
    Name    char(999),
    Addr    varchar(10)
)
--彙集索引 CREATE CLUSTERED INDEX CIX ON Person(Id)
--插入8條數據 DECLARE @var INT SET @var=100 WHILE(@var < 900) BEGIN INSERT INTO Person(Id,Name,Addr) VALUES(@var,'xx','') SET @var = @var+100 END

  這個表每一個行由int(4字節),char(999字節)和varchar(10字節組成),因此每行爲1003個字節,則8行佔用空間1003*8=8024字節加上一些內部開銷,能夠容納在一個頁面中。(原來這個表和數據搞得還挺巧的)。

  執行查看語句:

  SELECT page_count,avg_page_space_used_in_percent,record_count,avg_record_size_in_bytes,avg_fragmentation_in_percent,fragment_count 
  FROM sys.dm_db_index_physical_stats
  (DB_ID('Nx'),object_id('dbo.Person'),NULL,NULL,'sampled')

  示例以下:

  

  其中page_count是查看佔用了多少個頁,而第二個參數表示該頁空間的使用率。所以從以上信息能夠得到,這8條數據是放在一個頁上,並且該頁的空間使用率已是百分之百了。

  如今將其中一行的Addr改長一點:

  UPDATE Person SET Addr = '廣東廣州' where Id = 100

  則再執行檢查索引語句:

  

  能夠看到,這個表已經有了兩頁,頁面平均使用爲50%左右。可是明顯也形成了碎片,在列avg_fragmentation_in_percent上能夠看到,碎片大約爲50%。

  頁拆分後的示意圖以下:

  

  這個時候,繼續插入數據,碎片會上升。在又插入了至達到48條記錄後,碎片程度以下:

  

  這個時候,執行一個查詢計劃,查看下IO性能:

  

  能夠看到I/O降低了很多。

2、元數據函數sys.dm_db_index_physical_stats分析碎片

  SQL Server提供了一種特殊的元數據函數sys.dm_db_index_physical_stats,它有助於肯定數據庫中的頁和區段有多滿。而後用該信息做出一些維護數據庫的決策。

  該函數語法以下:

sys.dm_db_index_physical_stats(
    {<database id> | NULL | 0 | DEFAULT},
    { <object id> | NULL | 0 | DEFAULT },
    { <index id> } | NULL | 0 | -1 | DEFAULT },
    { <partition no> | NULL | 0 | DEFAULT },
    { <mode> | NULL | DEFAULT }
)

  下面假設從SmartScan中獲取全部的索引信息:

DECLARE @db_id SMALLINT;
DECLARE @object_id INT;
SET @db_id = DB_ID(N'Nx');
SET @object_id = OBJECT_ID(N'Account')
SELECT database_id,object_id,index_id,index_depth,avg_fragmentation_in_percent,page_count
FROM sys.dm_db_index_physical_stats(@db_id,@object_id,NULL,NULL,NULL);

   下面看看統計信息的說明:

列名

數據類型

說明

database_id

smallint

表或視圖的數據庫 ID。

object_id

int

索引所在的表或視圖的對象 ID。

index_id

int

索引的索引 ID。

0 = 堆。

partition_number

int

所屬對象內從 1 開始的分區號;表、視圖或索引。

1 = 未分區的索引或堆。

index_type_desc

nvarchar(60)

索引類型的說明:

HEAP

CLUSTERED INDEX

NONCLUSTERED INDEX

PRIMARY XML INDEX

SPATIAL INDEX

XML INDEX

alloc_unit_type_desc

nvarchar(60)

對分配單元類型的說明:

IN_ROW_DATA

LOB_DATA

ROW_OVERFLOW_DATA

LOB_DATA 分配單元包含類型爲textntextimagevarchar(max)nvarchar(max)varbinary(max) 和 xml 的列中所存儲的數據。 

ROW_OVERFLOW_DATA 分配單元包含類型爲 varchar(n)nvarchar(n)varbinary(n) 和sql_variant 的列(已推送到行外)中所存儲的數據。

index_depth

tinyint

索引總級別數。

1 = 堆,或 LOB_DATA 或 ROW_OVERFLOW_DATA 分配單元。

index_level

tinyint

索引的當前位於B樹結構中的級別。

0 表示索引葉級別、堆以及 LOB_DATA 或 ROW_OVERFLOW_DATA 分配單元。

大於 0 的值表示非葉索引級別。 index_level 在索引的根級別中屬於最高級別。

僅當 mode = DETAILED 時才處理非葉級別的索引。

avg_fragmentation_in_percent

float

索引的邏輯碎片,或 IN_ROW_DATA 分配單元中堆的區碎片。

此值按百分比計算,並將考慮多個文件。

0 表示 LOB_DATA 和 ROW_OVERFLOW_DATA 分配單元。

若是是堆表且mode模式 爲 Sampled 時,爲 NULL。若是碎片小於10%~20%,碎片不太可能會成爲問題,若是索引碎片在20%~40%,碎片可能成爲問題,可是能夠經過索引重組來消除索引解決,大規模的碎片(當碎片大於40%),可能要求索引重建。

fragment_count

bigint

IN_ROW_DATA 分配單元的葉級別中的碎片數。 

對於索引的非葉級別,以及 LOB_DATA 或 ROW_OVERFLOW_DATA 分配單元,爲 NULL。

對於堆,當 mode 爲 SAMPLED 時,爲 NULL。

avg_fragment_size_in_pages

float

IN_ROW_DATA 分配單元的葉級別中的一個碎片的平均頁數。

對於索引的非葉級別,以及 LOB_DATA 或 ROW_OVERFLOW_DATA 分配單元,爲 NULL。

對於堆,當 mode 爲 SAMPLED 時,爲 NULL。

page_count

bigint

索引或數據頁的總數。

對於索引,表示 IN_ROW_DATA 分配單元中 b 樹的當前級別中的索引頁總數。

對於堆,表示 IN_ROW_DATA 分配單元中的數據頁總數。

對於 LOB_DATA 或 ROW_OVERFLOW_DATA 分配單元,表示該分配單元中的總頁數。

avg_page_space_used_in_percent

float

全部頁中使用的可用數據存儲空間的平均百分比。

對於索引,平均百分比應用於 IN_ROW_DATA 分配單元中 b 樹的當前級別。

對於堆,表示 IN_ROW_DATA 分配單元中全部數據頁的平均百分比。

對於 LOB_DATA 或 ROW_OVERFLOW DATA 分配單元,表示該分配單元中全部頁的平均百分比。

當 mode 爲 LIMITED 時,爲 NULL。

record_count

bigint

總記錄數。

對於索引,記錄的總數應用於 IN_ROW_DATA 分配單元中 b 樹(包括非葉子數據頁的數量)的當前級別。

對於堆,表示 IN_ROW_DATA 分配單元中的總記錄數。

注意 注意

對於堆,此函數返回的記錄數可能與經過對堆運行 SELECT COUNT(*) 返回的行數不匹配。 這是由於一行可能包含多個記錄。 例如,在某些更新狀況下,單個堆行可能因爲更新操做而包含一條前推記錄和一條被前推記錄。 此外,多數大型 LOB 行在 LOB_DATA 存儲中拆分爲多個記錄。

對於 LOB_DATA 或 ROW_OVERFLOW_DATA 分配單元,表示整個分配單元中總記錄數。

當 mode 爲 LIMITED 時,爲 NULL。

ghost_record_count

bigint

分配單元中將被虛影清除任務刪除的虛影記錄數。

對於 IN_ROW_DATA 分配單元中索引的非葉級別,爲 0。

當 mode 爲 LIMITED 時,爲 NULL。

version_ghost_record_count

bigint

由分配單元中未完成的快照隔離事務保留的虛影記錄數。

對於 IN_ROW_DATA 分配單元中索引的非葉級別,爲 0。

當 mode 爲 LIMITED 時,爲 NULL。

min_record_size_in_bytes

int

最小記錄大小(字節)。

對於索引,最小記錄大小應用於 IN_ROW_DATA 分配單元中 b 樹的當前級別。

對於堆,表示 IN_ROW_DATA 分配單元中的最小記錄大小。

對於 LOB_DATA 或 ROW_OVERFLOW_DATA 分配單元,表示整個分配單元中的最小記錄大小。

當 mode 爲 LIMITED 時,爲 NULL。

max_record_size_in_bytes

int

最大記錄大小(字節)。

對於索引,最大記錄的大小應用於 IN_ROW_DATA 分配單元中 b 樹的當前級別。

對於堆,表示 IN_ROW_DATA 分配單元中的最大記錄大小。

對於 LOB_DATA 或 ROW_OVERFLOW_DATA 分配單元,表示整個分配單元中的最大記錄大小。

當 mode 爲 LIMITED 時,爲 NULL。

avg_record_size_in_bytes

float

平均記錄大小(字節)。

對於索引,平均記錄大小應用於 IN_ROW_DATA 分配單元中 b 樹的當前級別。

對於堆,表示 IN_ROW_DATA 分配單元中的平均記錄大小。

對於 LOB_DATA 或 ROW_OVERFLOW_DATA 分配單元,表示整個分配單元中的平均記錄大小。

當 mode 爲 LIMITED 時,爲 NULL。

forwarded_record_count

bigint

堆中具備指向另外一個數據位置的轉向指針的記錄數。 (在更新過程當中,若是在原始位置存儲新行的空間不足,將會出現此狀態。)

除 IN_ROW_DATA 分配單元外,對於堆的其餘全部分配單元都爲 NULL。

當 mode = LIMITED 時,對於堆爲 NULL。

compressed_page_count

bigint

壓縮頁的數目。

  • 對於堆,新分配的頁未進行 PAGE 壓縮。 堆在如下兩種特殊狀況下進行 PAGE 壓縮:大量導入數據時和從新生成堆時。 致使頁分配的典型 DML 操做不會進行 PAGE 壓縮。 當 compressed_page_count 值增加到超過您所需的閾值時,將從新生成堆。

  • 對於具備彙集索引的表,compressed_page_count 值表示 PAGE 壓縮的效率。

   一般返回多行的時候,有個index_level列,這個列表示改行屬於B樹結構的第幾層。

  分析小表的碎片

  不要過度關注小表的sys.dm_db_index_physical_stats輸出。對於少於8個頁面的小表或者索引,SQL Server使用混合區。例如,若是一個表僅包含兩個頁面,SQL Server從一個混合區中分配兩個頁面,二不是分配一個區給該表。混合區也能夠包含其餘小表或索引的頁面。

  跨越多個混合區的頁面分佈可能致使你相信在表或索引中有大量的外部碎片,而實際上這是SQL Server的設計,於是是可接受的。

  先來建一張表以下,3個int字段,1個char(2000)字段。平均尺寸爲4+4+4+2000=2012字節,8KB的頁面最多包含4行。在添加了28行以後,建立一個彙集索引來從屋裏上排列行並將碎片減小到最低限度。

  

  咋一看,好像碎片很是厲害。實際上並非這麼回事。

  分析以下:

  •   avg_fragmentation_in_percent:儘管這個索引可能跨越多個區,這裏看到碎片的狀況並非外部碎片的跡象,由於該索引保存在混合區上。
  •   avg_page_space_used_in_percent:這說明全部或大部分縣市在page_count中的7個頁面中的數據存儲情況良好。幾乎滿了,99點幾。這消除了邏輯碎片的可能性。
  •   fragment_count:這說明數據有碎片而且保存在多於一個區上,可是由於它的長度小於8個頁面,SQL Server對存儲該數據的地點沒有不少選擇。

  儘管有上述引發誤導的數值,一個少於8個頁面的小表(或索引)不可能從去除碎片的工做中獲益,由於它保存在混合區上。

  索引說明:

  

3、關於碎片的解決方法

  1.刪除索引並重建

  這種方式有以下缺點:

  索引不可用:在刪除索引期間,索引不可用。

  阻塞:卸載並重建索引會阻塞表上全部的其餘請求,也可能被其餘請求所阻塞。

  對於刪除彙集索引,則會致使對應的非彙集索引重建兩次(刪除時重建,創建時再重建,由於非彙集索引中有指向彙集索引的指針)。

  惟一性約束:用於定義主鍵或者惟一性約束的索引不能使用DROP INDEX語句刪除。並且,惟一性約束和主鍵均可能被外鍵約束引用。在主鍵卸載以前,全部引用該主鍵的外鍵必須首先被刪除。儘管能夠這麼作,但這是一種冒險並且費時的碎片整理方法。

  基於以上緣由,不建議在生產數據庫,尤爲是非空閒時間不建議採用這種技術。

  2.使用DROP_EXISTING語句重建索引

  爲了不重建兩次索引,使用DROP_EXISTING語句重建索引,由於這個語句是原子性的,不會致使非彙集索引重建兩次,但一樣的,這種方式也會形成阻塞。

CREATE UNIQUE CLUSTERED INDEX IX_C1 ON t1(c1)
WITH (DROP_EXISTING = ON)

  缺點:

  阻塞:與卸載重建方法相似,這種技術也致使並面臨來自其餘訪問該表(或該表的索引)的查詢的阻塞問題。

  使用約束的索引:與卸載重建不一樣,具備DROP_EXISTING子句的CREATE INDEX語句能夠用於從新建立使用約束的索引。若是該約束是一個主鍵或與外鍵相關的惟一性約束,在CREATE語句中不能包含UNIQUE。

  具備多個碎片化的索引的表:隨着表數據產生碎片,索引經常也碎片化。若是使用這種碎片整理技術,表上全部索引都必須單獨確認和重建。

  3.使用ALTER INDEX REBUILD語句重建索引

  使用這個語句一樣也是重建索引,可是經過動態重建索引而不須要卸載並重建索引.是優於前兩種方法的,但依舊會形成阻塞。能夠經過ONLINE關鍵字減小鎖,但會形成重建時間加長。

  阻塞:這個依然有阻塞問題。

  事務回滾:ALTER INDEX REBUILD徹底是一個原子操做,若是它在結束前中止,全部到那時爲止進行的碎片整理操做都將丟失,能夠經過ONLINE關鍵字減小鎖,但會形成重建時間加長。

  4.使用ALTER INDEX REORGANIZE

  這種方式不會重建索引,也不會生成新的頁,僅僅是整理葉級數據,不涉及非葉級,當遇到加鎖的頁時跳過,因此不會形成阻塞。但同時,整理效果會差於前三種。

  4種索引整理技術比較:

特性/問題 卸載並重建索引 DROP_EXISTING ALTER INDEX REBUILD ALTER INDEX REORGANIZE
在彙集索引碎片整理時,重建非彙集索引 兩次
丟失索引
整理具備約束的索引的碎片 高度複雜 複雜性適中 簡單 簡單
同時進行多個索引的碎片整理
併發性 中等,取決於冰法用戶活動
中途撤銷 由於不使用事務,存在危險 進程丟失 進程丟失 進程被保留
碎片整理程度 中到低
應用新的填充因子
更新統計

 

4、填充因子FILLFACTOR

   重建索引可以解決碎片的問題,可是重建索引的代碼一來須要常常操做,二來會形成數據阻塞,影響使用。在數據比較少的狀況下,重建索引代價很快,可是當索引比較大的時候,例如超過100M,那麼重建索引的時間會很是長。

   填充因子的做用是控制索引葉子頁面中的空閒空間數量。說白了就是預留一些空間給INSERT和UPDATE。若是知道表上有不少的INSERT查詢或者索引鍵列上有足夠的UPDATE查詢,能夠預先使用填充因子來增長索引葉子頁面的空閒空間已最小化頁面分割。若是表示只讀的,能夠建立一個高填充因子來減小索引頁面的數量。

  默認的填充因子爲0,這意味着頁面將被100%充滿。

   

   填充因子的概念能夠理解爲預留必定的空間存放插入和更新新增長的數據,以免頁拆分:

  

   能夠看出,使用填充因子會減小更新或者插入時的分頁次數,但因爲須要更多的頁,則會對應的損失查找性能.

   填充因子值的選擇:

  如何設置填充因子的值並無一個公式或者理念能夠準確的設置。使用填充因子雖然能夠減小更新或者插入時的分頁,但同時由於須要更多的頁,因此下降了查詢的性能和佔用更多的磁盤空間.如何設置這個值進行trade-off須要根據具體的狀況來看.

    具體狀況要根據對於表的讀寫比例來看,我這裏給出我認爲比較合適的值:

  1. 當讀寫比例大於100:1時,不要設置填充因子,100%填充
  2. 當寫的次數大於讀的次數時,設置50%-70%填充
  3. 當讀寫比例位於二者之間時80%-90%填充
相關文章
相關標籤/搜索