zoukankan      html  css  js  c++  java
  • 堆表修改内幕

          堆的修改需要使用到PFS页(PageFreeSpace)。PFS记录着数据页的空间使用情况。PFS页上使用1个字节(Byte)表示一个页的使用情况。一个PFS页可以表示8088个数据页,于是每8088个数据页就会有一个PFS页。一个数据文件的第二个页就是PFS页。PFS页上1个字节的结构:

    clipboard

    • Bit 1:是否被分配并使用。比如,分配给对象的统一区,并不是区内所有的页都被使用。此位就用标示已分配区中页是否被真正使用。
    • Bit2:表示页是否来自混合区
    • Bit3:表示页是否是一个IAM页(Index Allocation Map)
    • Bit4:表示页中是否有幻影行(Ghost Record)。后台的幻影行清理进程就需要用到这个位了。只有删除索引中的数据时才会产生幻影行。
    • Bit5-7:表示页的空间被使用的情况。取值如上图所示。

    插入数据行

          堆表插入新的数据行时,新行会被分配到任何有可用空间的地方。也就是说插入位置可能是表的任何位置。如果没有页有可用空间,则会从已经分配给表的统一分区中寻找未被使用的页来存储数据。如果所有区的所有页都没有空闲空间,则会分配新的统一分区给表来存储数据。

    删除数据行

          直接删除之,删除方式跟删除索引中的非叶级页一样但跟。

       从堆表删除数据行后,被删除行所在的数据页并不会马上重组数据页以释放空间,只会标示这些空间可用。当插入新行需要连续的可用空间时,才会被回收利用。下面的示例从页中间删除一行:

    CREATE TABLE smallrows
    
    (
    
    a int identity,
    
    b char(10)
    
    );
    
    GO
    
    INSERT INTO smallrows
    
    VALUES ('row 1');
    
    INSERT INTO smallrows
    
    VALUES ('row 2');
    
    INSERT INTO smallrows
    
    VALUES ('row 3');
    
    INSERT INTO smallrows
    
    VALUES ('row 4');
    
    INSERT INTO smallrows
    
    VALUES ('row 5');
    
    go
    
    --get the data page id for dbcc page
    
    SELECT allocated_page_file_id, allocated_page_page_id, page_type_desc
    
    FROM sys.dm_db_database_page_allocations
    
    (db_id('test'), object_id('smallrows'), NULL, NULL, 'DETAILED');
    
    go
    
    dbcc traceon(3604)
    
    dbcc page(test,1,146,1)
    
    go
    
    --delete the row a=3
    
    DELETE FROM smallrows
    
    WHERE a = 3;
    
    GO
    
    dbcc traceon(3604)
    
    dbcc page(test,1,146,1)
    
    go
    
    观察两次dbcc page输出的OFFSET TABLE:
    
    Row - Offset                        
    
    4 (0x4) - 180 (0xb4)                
    
    3 (0x3) - 159 (0x9f)                
    
    2 (0x2) - 138 (0x8a)                
    
    1 (0x1) - 117 (0x75)                
    
    0 (0x0) - 96 (0x60) 
    
    Row - Offset                        
    
    4 (0x4) - 180 (0xb4)                
    
    3 (0x3) - 159 (0x9f)                
    
    2 (0x2) - 0 (0x0) 
    
    1 (0x1) - 117 (0x75)                
    
    0 (0x0) - 96 (0x60)

    可以看出:

    • 删除前后其它行的偏移量没有变化,也就是说没有行被移动。
    • 删除后,被删除行(slot2)偏移量变成了0,表示该slot未被使用。

    其实使用DBCC PAGE(TEST,1,146,2)仍然可以看到row3这一行。

    清空堆表数据页上的数据,它也不会自动的释放这些页。可以通过sys.dm_db_partition_statst和sys.dm_db_

    database_page_allocations观察到页的使用和分配信息是不会有变化的。要想清空页的数据并回收空间:

    • delete时使用表锁:数据页会被释放,但IAM页保留
    • truncate table:这个是针对清空表,会释放所有页,包括IAM页
    • 创建并删除一个聚集索引
    • 使用alter table ...rebuild

    更新数据行

          SQL Server会自动选择最优的数据更新策略。基于受影响行数,访问数据的方式和是否需要修改索引键来选择最优的策略。更新实现方式包括:直接将旧值原地修改为新值和插入新值后删除旧值。

    堆表中的数据行移动

          堆表中数据行的变长列的数据更新为更大尺寸的数据后,原来的数据页不能再存储它,就会发生数据行移动。数据行被移动到新页时,原来的位置上会放置一个转发指针(Forwarding Pointer)。这个指针指向行的现在的地址。这样的好处,就是当发生数据行移动时不需要移动页上所有的数据,只需要移动特定行并生成转发指针即可。

        下面的示例,创建包含变长列的表,然后更新一行的变长列,使得超出原来页的容量。然后观察页的转发情况。

    if OBJECT_ID('bigrows') is  not null
    
      drop  TABLE bigrows
    
    go
    
    CREATE TABLE bigrows
    
    ( a int IDENTITY ,
    
    b varchar(1600),
    
    c varchar(1600));
    
    GO
    
    INSERT INTO bigrows
    
    VALUES (REPLICATE('a', 1600), '');
    
    INSERT INTO bigrows
    
    VALUES (REPLICATE('b', 1600), '');
    
    INSERT INTO bigrows
    
    VALUES (REPLICATE('c', 1600), '');
    
    INSERT INTO bigrows
    
    VALUES (REPLICATE('d', 1600), '');
    
    INSERT INTO bigrows
    
    VALUES (REPLICATE('e', 1600), '');
    
    GO
    
    SELECT allocated_page_file_id, allocated_page_page_id, page_type_desc
    
    FROM sys.dm_db_database_page_allocations
    
    (db_id('test'), object_id('bigrows'), NULL, NULL, 'DETAILED');
    
    go
    
    UPDATE bigrows
    
    SET c = REPLICATE('x', 1600)
    
    WHERE a = 3;
    
    GO
    
    SELECT allocated_page_file_id, allocated_page_page_id, page_type_desc
    
    FROM sys.dm_db_database_page_allocations
    
    (db_id('test'), object_id('bigrows'), NULL, NULL, 'DETAILED');
    
    go
    
    dbcc traceon(3604)
    
    dbcc page(test,1,163,1)
    
    go

    然后观察Slot2的内容,发现记录类型为9个字节的转发存根(Forwarding Stub)。

    Slot 2, Offset 0xcfe, Length 9, DumpStyle BYTE
    
    Record Type =FORWARDING_STUB      Record Attributes =                 Record Size = 9
    
    Memory Dump @0x000000000B4AACFE
    
    0000000000000000:   04a50000 00010000 00

    转发存根的16进制内容可心划分成4个部分,每部分代表的含义如下:

    04-a5000000-0100-0000

    转发存根标志字节位-转记录所在的页号-文件号-Slot编号

    由于SQL Server采用的是Little-Endian字节序来组织字节存放的(也就说看到就是它在内存中存放的顺序),所以我们要看到数据本来的顺序和内容,还需要将之转换成Big-Endian的字节序组织的样子,即做一次高低位转换,然后才能转成10进制表示的内容。

    高低位转换后:04-000000a5-0001-000

    转10进制后:4-165-1-0

    4就是表示这是一条转发存根,转发后记录的位置是:文件1中165号页内的Slot0上。也可以DBCC PAGE查看165号页Slot0长什么样。可以看到Record Type = FORWARDED_RECORD,表示这是一条转发记录。行内容是大量的c和x字符。

    Slot 0, Offset 0x60, Length 3229, DumpStyle BYTE
    
    Record Type =FORWARDED_RECORD     Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
    
    Record Size = 3229                  
    
    Memory Dump @0x000000000B4AA060

    转发指针只存在于堆表上。一个转发指针不会指向另一个转发指针,如果转发记录再次被转发,则原来的转发指针会指向转发记录的新地址。一旦一个转发指针被生成,它会一直存在。有些情况会转发指针会被清除:

    • 转发记录尺寸缩小了,并且原来的页能够存放得下它,它就会回到原来的页,转发指针被清除
    • 收缩数据库。数据文件收缩不会产生任何新的转发指针,它会重新分配书签,并且会删除一些数据页。如果这些页包含转发记录和存根,会被重新组织到其它页,从而消除转发。
    • 使用ALTER Table Rebuild重建堆表
    • 转发行被删除
    • 建立聚集索引,变成聚集索引表

    原地(In Place)更新

          原地更新是SQL Server的更新规则。原地更新只在原来的位置上修改受影响的字节内容。每更新一行就会向事务日志写一条记录。如果表有更新触发器或者行被标记为复制,则更新一行,会写两条事务日志记录。先写一条删除,再写一条插入记录。原地更新发生的两种情况:

    • 不需要用到转发指针的堆表更新
    • 不需要修改聚集键的聚集索引

    聚集索引键存放是有序的,当修改聚集索引键的值,但不影响其排序位置时,也会是原地更新。比如某表的聚集索引列Name的值包括:Allen,Bill,Charlie。如果将Bill更新为Bily,是原地更新,将Bill更新为David,就是非原地升级(可能新行还会存在当前的数据页上)。

    非原地更新

          非原地更新发生在更新聚集索引的索引键时。更新会变成先删除再插入两个操作。更新索引键也有可能是混合更新,即有些行是原地更新,其它行是非原地更新。更新聚集索引键时,SQL Server会生成一个包含删除和插入操作涉及到的所有行的列表。这个列表小的话就存在内存,大的话就存在tempdb。然后根据键值和操作符(删除或者插入)对列表排序。接下来分种情况:

    1. 如果索引键值非唯一,则先删除再插入。
    2. 如果索引键值唯一,则会将删除和插入这两个操作合并成一个更新操作。这样更高效。

    总结

         1. 本文大部分理论基础和例子都参考和引用的《Microsoft SQL Server 2012 Internal》

         2. 有一段时间,经常被人问到”我要修改一下表需要你帮忙评估一下影响”,然后就做了一些基础知识的总结。

  • 相关阅读:
    Uva 10779 collector's problem
    poj 2728 最优比率树(最小生成树问题)
    LA 3126 二分图匹配 最小路径覆盖
    poj 1149 最大流构图
    Step By Step(Java XML篇)
    Step By Step(Java 输入输出篇)
    Step By Step(Java 集合篇)
    Step By Step(Java 线程篇)
    Step By Step(Java 反射篇)
    Step By Step(Java 国际化篇)
  • 原文地址:https://www.cnblogs.com/Joe-T/p/4765726.html
Copyright © 2011-2022 走看看