我们都知道数据在存储引擎中是以页的形式组织的,但数据页在不同的组织形式中其中对应的数据行存储是不尽相同的,这里通过实例为大家介绍下堆表的中特有的一种情形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 )操作时应注意时间窗口
结语
任何事物都存在因果,套用数据库系统中,我们应该清楚自己的所作所为,以及带来的效用/影响.合理到位的分析,评估会让我们的工作变得从容.

