1. 程式人生 > >索引碎片、填充因子 [據此優化金蝶 EAS 7.5 SQL SERVER 資料庫]

索引碎片、填充因子 [據此優化金蝶 EAS 7.5 SQL SERVER 資料庫]

在SQL Server中,儲存資料的最小單位是頁,每一頁所能容納的資料為8060位元組.而頁的組織方式是通過B樹結構(表上沒有聚集索引則為堆結構,不在本文討論之列)如下圖:

SQL Server索引中的碎片和填充因子

  在聚集索引B樹中,只有葉子節點實際儲存資料,而其他根節點和中間節點僅僅用於存放查詢葉子節點的資料.

  每一個葉子節點為一頁,每頁是不可分割的. 而SQL Server向每個頁記憶體儲資料的最小單位是表的行(Row).當葉子節點中新插入的行或更新的行使得葉子節點無法容納當前更新或者插入的行時,分頁就產生了.在分頁的過程中,就會產生碎片.


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

  •   頁拆分
  •   碎片

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

  對於非常小的表(比64KB小得多),一個區中的頁面可能屬於多餘一個的索引或表---這被稱為混合區。如果資料庫中有太多的小表,混合區幫助SQL Server節約磁碟空間。

  隨著表(或索引)增長並且請求超過8個頁面,SQL Server建立專用於該表(或索引)的區並且從該區中分配頁面。這樣一個區被稱為統一區,它可以為多達8個相同表或索引的頁面請求服務。

一、碎片

  當資料庫增長,頁拆分,然後刪除資料時,就會產生碎片。從增長的方面看,平衡樹處理得很不錯。但是對於刪除方面,它並沒有太大的作用。最終可能會出現這種情況,一個頁上有一條記錄,而另一個頁上有幾個記錄。在這種情況下,一個頁上儲存的資料量只是它能夠儲存總資料量的一小部分。

  1、碎片會造成空間的浪費,SQL Server每次會分配一個區段,如果一個頁上只有一條記錄,則仍然會分配整個區段。

  2、散佈在各處的資料會造成資料檢索時的額外系統開銷。為了獲取需要的10行記錄,SQL Server不是隻載入一個頁,而是可能必須載入10個頁來獲取相同的資訊。並不只是讀取行導致了這一結果,在讀取行前,SQL Server必須先讀取頁。更多的頁意味著更多的工作量。

  但是碎片也不只是有壞處,比如一個插入非常頻繁的表就很喜歡碎片,因為在插入資料時幾乎不用擔心頁拆分的問題。所以大量的碎片意味著較差的讀取效能,但也意味著極好的插入效能。

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

  外部碎片:

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

  

  新的頁不斷隨資料的增長而產生,而聚集索引要求行之間連續,所以如果聚集索引不是自增列,頁拆分後和原來的頁在磁碟上並不連續-這就是外部碎片。
  由於頁拆分,導致資料在頁之間的移動,所以如果插入更新等操作經常需要分頁,則會大大消耗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下降了不少。

二、元資料函式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個頁面的小表(或索引)不可能從去除碎片的工作中獲益,因為它儲存在混合區上。

  索引說明:

  

三、關於碎片的解決方法

  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
在聚集索引碎片整理時,重建非聚集索引 兩次
丟失索引
整理具有約束的索引的碎片 高度複雜 複雜性適中 簡單 簡單
同時進行多個索引的碎片整理
併發性 中等,取決於冰法使用者活動
中途撤銷 因為不使用事務,存在危險 程序丟失 程序丟失 程序被保留
碎片整理程度 中到低
應用新的填充因子
更新統計

四、填充因子FILLFACTOR

   重建索引能夠解決碎片的問題,但是重建索引的程式碼一來需要經常操作,二來會造成資料阻塞,影響使用。在資料比較少的情況下,重建索引代價很快,但是當索引比較大的時候,例如超過100M,那麼重建索引的時間會非常長。

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

  預設的填充因子為0,這意味著頁面將被100%充滿。

   

   填充因子的概念可以理解為預留一定的空間存放插入和更新新增加的資料,以避免頁拆分:

  

   可以看出,使用填充因子會減少更新或者插入時的分頁次數,但由於需要更多的頁,則會對應的損失查詢效能.

   填充因子值的選擇:

  如何設定填充因子的值並沒有一個公式或者理念可以準確的設定。使用填充因子雖然可以減少更新或者插入時的分頁,但同時因為需要更多的頁,所以降低了查詢的效能和佔用更多的磁碟空間.如何設定這個值進行trade-off需要根據具體的情況來看.

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

  1. 當讀寫比例大於100:1時,不要設定填充因子,100%填充
  2. 當寫的次數大於讀的次數時,設定50%-70%填充
  3. 當讀寫比例位於兩者之間時80%-90%填充