我們都知道數據在存儲引擎中是以頁的形式組織的,但數據頁在不同的組織形式中其中對應的數據行存儲是不盡相同的,這里通過實例為大家介紹下堆表的中特有的一種情形Forwared Records及處理方式.
概念
堆表中,當對其中的記錄進行更新時,如果當前數據頁無法滿足更新行的容量,此時這行記錄將會轉移到新的數據頁中,而原數據頁中將會留下指針(文件號,頁號,槽號)鏈接到新的數據頁中.
Code 創建測試數據
create database testpage go use testpage go create table testtb ( id int identity ( 1 , 1 ), str1 char ( 100 ) default replicate ( ' a ' , 100 ), str2 varchar ( 2000 ) default replicate ( ' b ' , 500 ), str3 varchar ( 2000 ) default replicate ( ' c ' , 1000 ) ) go insert into testtb default values go 20
Code 查看相關數據頁 如圖1-1
DBCC TRACEON( 3604 ) GO DBCC IND(testpage, ' testtb ' , 1 ) -- -find the data page GO DBCC PAGE( ' testpage ' , 1 , 79 , 3 ) -- ---view data page find slot 2(ID=3) GO
??????????
????????????????????????
??????????? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 圖1-1
現在我們來更新ID=3的數據使當前數據頁(79)無法容納此行數據,然后觀察數據頁,
Code
update dbo.testtb set str2=replicate('t', 1000) where ID=3--update ID=3
GO
DBCC PAGE('testpage', 1, 79, 3)
GO
繼續找到slot 2槽位(ID=3)觀察 如圖1-2所示,此時slot2數據的Record Type = FORWARDING_STUB,也就是此時槽位2只留下RID記錄,數據轉到其他數據頁中了(Forwarding to? =? file 1 page 94 slot 0? )
?
??????????????????????????????????????????????? 圖1-2
?
這里稍微深入的講下RID的存儲內容,實例中根據dbcc page已經給我們展示RID的內容,實際上存儲是16進制的如圖1-2中的黑色部分(045e0000 00010000 00
).具體對應RID內容如圖1-3
??????????????????????????????????????????? 圖1-3
?
我們在找到實際存儲ID=3的數據頁看下數據內容(1:94:0) 如圖1-4
圖中我省去了數據內容
code
DBCC PAGE('testpage', 1, 94, 3)
GO
???????????????????????????????????????? 圖1-4
?
接下來我們繼續更新ID=3讓新的數據頁也無法容納它,然后觀察相應的數據頁如圖1-5(三個dbcc page 合成圖)
(此時ID=3的原始頁94,槽號2指向了新的數據頁位置184)如圖所示1-5所示
code
insert into testtb default values go 20 -- --先插入一些數據 update dbo. Testtb set str2 = replicate ( ' t ' , 2000 ),str3 = replicate ( ' t ' , 2000 ) where ID = 3 GO -- -繼續更新ID=3 DBCC PAGE( ' testpage ' , 1 , 79 , 3 ) -- ------源ID=3,現在執行(1:184:2) GO DBCC PAGE( ' testpage ' , 1 , 94 , 3 ) -- ------第一次修改時ID=3存儲位置(1:94:0),現在slot 0沒有了 GO DBCC PAGE( ' testpage ' , 1 , 184 , 3 ) -- ----目前id=3的數據存儲位置 GO
?
?????????????????????????????????????????????? 圖1-5
?
可以看出id=3的原始頁(1:79:2)的數據再次變更后的由(1:94:0)挪到了(1:184:2)中,
而頁號94槽號0就不存在了.
堆表中的非聚集索引.
當堆表中有非聚集索引存在時,非聚集索引RID指向的原始頁位置
我們通過實例看下
注:關于heap rid我就不做詳細介紹了,實例中通過查詢轉換可以算出10進制對應的RID
Code
CREATE UNIQUE NONCLUSTERED INDEX inx_1 ON testtb (id ) DBCC IND(testpage, ' testtb ' , - 1 ) -- --find the index page (page type 2)115 GO DBCC PAGE( ' testpage ' , 1 , 115 , 3 ) -- -find the heap rid where id=3 heap rid =0x4F00000001000200 DECLARE @HeapRid BINARY ( 8 ) SET @HeapRid = 0x4F00000001000200 SELECT CONVERT ( VARCHAR ( 5 ), CONVERT ( INT , SUBSTRING ( @HeapRid , 6 , 1 ) + SUBSTRING ( @HeapRid , 5 , 1 ))) + ' : ' + CONVERT ( VARCHAR ( 10 ), CONVERT ( INT , SUBSTRING ( @HeapRid , 4 , 1 ) + SUBSTRING ( @HeapRid , 3 , 1 ) + SUBSTRING ( @HeapRid , 2 , 1 ) + SUBSTRING ( @HeapRid , 1 , 1 ))) + ' : ' + CONVERT ( VARCHAR ( 5 ), CONVERT ( INT , SUBSTRING ( @HeapRid , 8 , 1 ) + SUBSTRING ( @HeapRid , 7 , 1 ))) AS ' Fileid:Pageid:slot '
可以看到select 的輸出正好是(1:79:2)我們原始的id=3的位置
?
關于性能
由于forwarded record的存在,當訪問到這種數據行時,會消耗額外的隨機IO,從而影響性能.更有甚者,由于額外的數據頁被放入內存中,造成BP的污染,致使性能下降.
(研發要求對一個頻繁訪問的大堆表更新擴充欄位,執行完了性能依舊下降有木有?)
?
我們通過簡單實例來看下
訪問forwarded record會造成額外IO如圖2-1
Code
set statistics io on select * from testtb where id = 2 select * from testtb where id = 3
??????????????????????????????????? 圖2-1
當表數據量大時,大批量更新擴充欄位會造成對緩沖池的污染
code
create table testbp ( id int identity ( 1 , 1 ), str1 char ( 100 ) default replicate ( ' a ' , 100 ), str2 varchar ( 2000 ) default replicate ( ' b ' , 500 ), str3 varchar ( 2000 ) default replicate ( ' c ' , 1000 ) ) go insert into testbp default values go 10000 dbcc dropcleanbuffers select * from testbp SELECT count ( * ) * 8 / 1024 AS ' Cached Size (MB) ' , CASE database_id WHEN 32767 THEN ' ResourceDb ' ELSE db_name (database_id) END AS ' Database ' FROM sys.dm_os_buffer_descriptors with (nolock) where db_name (database_id) = ' testpage ' GROUP BY db_name (database_id) ,database_id -- ---buffer pool 15MB update dbo. testbp set str2 = replicate ( ' t ' , 1000 ) -- -make forwarded recordes dbcc dropcleanbuffers select * from testbp SELECT count ( * ) * 8 / 1024 AS ' Cached Size (MB) ' , CASE database_id WHEN 32767 THEN ' ResourceDb ' ELSE db_name (database_id) END AS ' Database ' FROM sys.dm_os_buffer_descriptors with (nolock) where db_name (database_id) = ' testpage ' GROUP BY db_name (database_id) ,database_id -- ----31MB
順序執行代碼時可以看出,testpage表更改前后占Buffer Pool的大小分別為15M,31M,對BP影響明顯.
監控/發現
實際生產環境中我們需要監控一些性能指標用來輔助DBA解決問題,保證運維效率,針對這里,我們監控性能計數器中SQL Server Access Methods對象中的forwarded records/sec,如果你設定的了性能Baseline,這個值如果有異常變化,則需要我們關注.
同時我們也可以根據系統的DMF找出特定對象的forwarded records信息.代碼如下
select object_name ( object_id ) as objectName ,index_type_desc ,forwarded_record_count from sys.dm_db_index_physical_stats( db_id (), null , null , null , ' detailed ' ) where object_name ( object_id ) = ' testbp ' -- ----view the forwarded records info
注:可以通過簡單的Batch檢索整個庫甚至實例中的堆表的相關信息,有興趣的朋友自己寫下.
處理
如果發現了因為forwarded Recordes引起的性能問題,我們可以選擇表中創建聚集索引改變數據組織結構(forwarded Recordes只在堆表中存在).如果無法添加聚集索引,也可以選擇重組堆表( alter table heap rebuild )操作時應注意時間窗口
結語
任何事物都存在因果,套用數據庫系統中,我們應該清楚自己的所作所為,以及帶來的效用/影響.合理到位的分析,評估會讓我們的工作變得從容.
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
