SQL Server 存储引擎-剖析Forwarded Records

系统 1782 0
原文: SQL Server 存储引擎-剖析Forwarded Records

我们都知道数据在存储引擎中是以页的形式组织的,但数据页在不同的组织形式中其中对应的数据行存储是不尽相同的,这里通过实例为大家介绍下堆表的中特有的一种情形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 )操作时应注意时间窗口

结语

任何事物都存在因果,套用数据库系统中,我们应该清楚自己的所作所为,以及带来的效用/影响.合理到位的分析,评估会让我们的工作变得从容.

SQL Server 存储引擎-剖析Forwarded Records


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论