SQL Server索引的維護 - 索引碎片、填充因子 <第三篇>
實際上,索引的維護主要包括以下兩個方面:
- 頁拆分
- 碎片
這兩個問題都和頁密度有關,雖然兩者的表現形式在本質上有所區別,但是故障排除工具是一樣的,因為處理是相同的。
對於非常小的表(比64KB小得多),一個區中的頁面可能屬於多余一個的索引或表---這被稱為混合區。如果數據庫中有太多的小表,混合區幫助SQL Server節約磁盤空間。
隨著表(或索引)增長並且請求超過8個頁面,SQL Server創建專用於該表(或索引)的區並且從該區中分配頁面。這樣一個區被稱為統一區,它可以為多達8個相同表或索引的頁面請求服務。
一、碎片
當數據庫增長,頁拆分,然後刪除數據時,就會產生碎片。從增長的方面看,平衡樹處理得很不錯。但是對於刪除方面,它並沒有太大的作用。最終可能會出現這種情況,一個頁上有一條記錄,而另一個頁上有幾個記錄。在這種情況下,一個頁上保存的數據量只是它能夠保存總數據量的一小部分。
1、碎片會造成空間的浪費,SQL Server每次會分配一個區段,如果一個頁上只有一條記錄,則仍然會分配整個區段。
2、散布在各處的數據會造成數據檢索時的額外系統開銷。為了獲取需要的10行記錄,SQL Server不是只加載一個頁,而是可能必須加載10個頁來獲取相同的信息。並不只是讀取行導致了這一結果,在讀取行前,SQL Server必須先讀取頁。更多的頁意味著更多的工作量。
但是碎片也不只是有壞處,比如一個插入非常頻繁的表就很喜歡碎片,因為在插入數據時幾乎不用擔心頁拆分的問題。所以大量的碎片意味著較差的讀取性能,但也意味著極好的插入性能。
關於碎片的理解,找到了數據庫牛人CareySon的這篇文章 T-SQL查詢高級—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 分配單元包含類型為text、ntext、image、varchar(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 分配單元中的總記錄數。
對於 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 |
壓縮頁的數目。
|
分析小表的碎片
不要過分關註小表的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需要根據具體的情況來看.
具體情況要根據對於表的讀寫比例來看,我這裏給出我認為比較合適的值:
- 當讀寫比例大於100:1時,不要設置填充因子,100%填充
- 當寫的次數大於讀的次數時,設置50%-70%填充
- 當讀寫比例位於兩者之間時80%-90%填充
SQL Server索引的維護 - 索引碎片、填充因子 <第三篇>