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

  • 相关阅读:
    QSet使用及Qt自定义类型使用QHash等算法
    QQueue与QStack使用
    QHash和QMultiHash使用
    【洛谷6633】[ZJOI2020] 抽卡(多项式毒瘤题)
    【洛谷5996】[PA2014] Muzeum(模拟费用流)
    【CF1063F】String Journey(后缀自动机+线段树)
    【BZOJ3640】JC的小苹果(高斯消元)
    【洛谷6478】[NOI Online #2 提高组] 游戏(树形DP+二项式反演)
    【洛谷6730】[WC2020] 猜数游戏(数论)
    【洛谷6186】[NOI Online #1 提高组] 冒泡排序(树状数组)
  • 原文地址:https://www.cnblogs.com/Joe-T/p/4765726.html
Copyright © 2011-2022 走看看