我們都知道數(shù)據(jù)在存儲(chǔ)引擎中是以頁的形式組織的,但數(shù)據(jù)頁在不同的組織形式中其中對(duì)應(yīng)的數(shù)據(jù)行存儲(chǔ)是不盡相同的,這里通過實(shí)例為大家介紹下堆表的中特有的一種情形Forwared Records及處理方式.
概念
堆表中,當(dāng)對(duì)其中的記錄進(jìn)行更新時(shí),如果當(dāng)前數(shù)據(jù)頁無法滿足更新行的容量,此時(shí)這行記錄將會(huì)轉(zhuǎn)移到新的數(shù)據(jù)頁中,而原數(shù)據(jù)頁中將會(huì)留下指針(文件號(hào),頁號(hào),槽號(hào))鏈接到新的數(shù)據(jù)頁中.
Code 創(chuàng)建測(cè)試數(shù)據(jù)
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 查看相關(guān)數(shù)據(jù)頁 如圖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
現(xiàn)在我們來更新ID=3的數(shù)據(jù)使當(dāng)前數(shù)據(jù)頁(79)無法容納此行數(shù)據(jù),然后觀察數(shù)據(jù)頁,
Code
update dbo.testtb set str2=replicate('t', 1000) where ID=3--update ID=3
GO
DBCC PAGE('testpage', 1, 79, 3)
GO
繼續(xù)找到slot 2槽位(ID=3)觀察 如圖1-2所示,此時(shí)slot2數(shù)據(jù)的Record Type = FORWARDING_STUB,也就是此時(shí)槽位2只留下RID記錄,數(shù)據(jù)轉(zhuǎn)到其他數(shù)據(jù)頁中了(Forwarding to? =? file 1 page 94 slot 0? )
?
??????????????????????????????????????????????? 圖1-2
?
這里稍微深入的講下RID的存儲(chǔ)內(nèi)容,實(shí)例中根據(jù)dbcc page已經(jīng)給我們展示RID的內(nèi)容,實(shí)際上存儲(chǔ)是16進(jìn)制的如圖1-2中的黑色部分(045e0000 00010000 00
).具體對(duì)應(yīng)RID內(nèi)容如圖1-3
??????????????????????????????????????????? 圖1-3
?
我們?cè)谡业綄?shí)際存儲(chǔ)ID=3的數(shù)據(jù)頁看下數(shù)據(jù)內(nèi)容(1:94:0) 如圖1-4
圖中我省去了數(shù)據(jù)內(nèi)容
code
DBCC PAGE('testpage', 1, 94, 3)
GO
???????????????????????????????????????? 圖1-4
?
接下來我們繼續(xù)更新ID=3讓新的數(shù)據(jù)頁也無法容納它,然后觀察相應(yīng)的數(shù)據(jù)頁如圖1-5(三個(gè)dbcc page 合成圖)
(此時(shí)ID=3的原始頁94,槽號(hào)2指向了新的數(shù)據(jù)頁位置184)如圖所示1-5所示
code
insert
into
testtb
default
values
go
20
--
--先插入一些數(shù)據(jù)
update
dbo. Testtb
set
str2
=
replicate
(
'
t
'
,
2000
),str3
=
replicate
(
'
t
'
,
2000
)
where
ID
=
3
GO
--
-繼續(xù)更新ID=3
DBCC
PAGE(
'
testpage
'
,
1
,
79
,
3
)
--
------源ID=3,現(xiàn)在執(zhí)行(1:184:2)
GO
DBCC
PAGE(
'
testpage
'
,
1
,
94
,
3
)
--
------第一次修改時(shí)ID=3存儲(chǔ)位置(1:94:0),現(xiàn)在slot 0沒有了
GO
DBCC
PAGE(
'
testpage
'
,
1
,
184
,
3
)
--
----目前id=3的數(shù)據(jù)存儲(chǔ)位置
GO
?
?????????????????????????????????????????????? 圖1-5
?
可以看出id=3的原始頁(1:79:2)的數(shù)據(jù)再次變更后的由(1:94:0)挪到了(1:184:2)中,
而頁號(hào)94槽號(hào)0就不存在了.
堆表中的非聚集索引.
當(dāng)堆表中有非聚集索引存在時(shí),非聚集索引RID指向的原始頁位置
我們通過實(shí)例看下
注:關(guān)于heap rid我就不做詳細(xì)介紹了,實(shí)例中通過查詢轉(zhuǎn)換可以算出10進(jìn)制對(duì)應(yīng)的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)我們?cè)嫉膇d=3的位置
?
關(guān)于性能
由于forwarded record的存在,當(dāng)訪問到這種數(shù)據(jù)行時(shí),會(huì)消耗額外的隨機(jī)IO,從而影響性能.更有甚者,由于額外的數(shù)據(jù)頁被放入內(nèi)存中,造成BP的污染,致使性能下降.
(研發(fā)要求對(duì)一個(gè)頻繁訪問的大堆表更新擴(kuò)充欄位,執(zhí)行完了性能依舊下降有木有?)
?
我們通過簡單實(shí)例來看下
訪問forwarded record會(huì)造成額外IO如圖2-1
Code
set
statistics
io
on
select
*
from
testtb
where
id
=
2
select
*
from
testtb
where
id
=
3
??????????????????????????????????? 圖2-1
當(dāng)表數(shù)據(jù)量大時(shí),大批量更新擴(kuò)充欄位會(huì)造成對(duì)緩沖池的污染
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
順序執(zhí)行代碼時(shí)可以看出,testpage表更改前后占Buffer Pool的大小分別為15M,31M,對(duì)BP影響明顯.
監(jiān)控/發(fā)現(xiàn)
實(shí)際生產(chǎn)環(huán)境中我們需要監(jiān)控一些性能指標(biāo)用來輔助DBA解決問題,保證運(yùn)維效率,針對(duì)這里,我們監(jiān)控性能計(jì)數(shù)器中SQL Server Access Methods對(duì)象中的forwarded records/sec,如果你設(shè)定的了性能Baseline,這個(gè)值如果有異常變化,則需要我們關(guān)注.
同時(shí)我們也可以根據(jù)系統(tǒng)的DMF找出特定對(duì)象的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檢索整個(gè)庫甚至實(shí)例中的堆表的相關(guān)信息,有興趣的朋友自己寫下.
處理
如果發(fā)現(xiàn)了因?yàn)閒orwarded Recordes引起的性能問題,我們可以選擇表中創(chuàng)建聚集索引改變數(shù)據(jù)組織結(jié)構(gòu)(forwarded Recordes只在堆表中存在).如果無法添加聚集索引,也可以選擇重組堆表( alter table heap rebuild )操作時(shí)應(yīng)注意時(shí)間窗口
結(jié)語
任何事物都存在因果,套用數(shù)據(jù)庫系統(tǒng)中,我們應(yīng)該清楚自己的所作所為,以及帶來的效用/影響.合理到位的分析,評(píng)估會(huì)讓我們的工作變得從容.
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元

